diff --git a/.agent/knowledge/api-cheatsheet.md b/.agent/knowledge/api-cheatsheet.md new file mode 100644 index 0000000000..2336cd30aa --- /dev/null +++ b/.agent/knowledge/api-cheatsheet.md @@ -0,0 +1,81 @@ +# `openmct.*` Cheat Sheet + +## Bootstrapping + +```js +openmct.install(plugin); +openmct.start(elOrSelector); +``` + +## Types + +```js +openmct.types.addType(key, { + name, description, cssClass, creatable, initialize +}); +openmct.types.get(key); +``` + +## Objects + +```js +openmct.objects.addRoot(idOrFn, priority); +openmct.objects.addProvider(namespace, { get(identifier) }); +openmct.objects.get(identifier); +openmct.objects.mutate(obj, path, value); +``` + +## Composition + +```js +openmct.composition.addProvider({ appliesTo(obj), load(obj) }); +openmct.composition.get(obj); // returns CompositionCollection +``` + +## Telemetry + +```js +openmct.telemetry.addProvider({ + supportsRequest(o, opts), request(o, opts), + supportsSubscribe(o), subscribe(o, cb, opts), + supportsMetadata(o), getMetadata(o), + supportsLimits(o), getLimitEvaluator(o) +}); +openmct.telemetry.addFormat({ key, format, parse, validate }); +openmct.telemetry.request(obj, opts); +openmct.telemetry.subscribe(obj, cb, opts); +``` + +## Time + +```js +openmct.time.addTimeSystem(sys); +openmct.time.setTimeSystem(key, bounds); +openmct.time.getTimeSystem(); + +openmct.time.addClock(clk); +openmct.time.setClock(clk, offsets); +openmct.time.getClock(); + +openmct.time.setBounds(b); openmct.time.getBounds(); +openmct.time.setClockOffsets(o); openmct.time.getClockOffsets(); +openmct.time.setMode('realtime' | 'fixed'); +openmct.time.getMode(); +openmct.time.isRealTime(); +openmct.time.isFixed(); +``` + +Events: `boundsChanged`, `timeSystemChanged`, `clockChanged`, +`clockOffsetsChanged`, `modeChanged`. + +## Indicators + +```js +const ind = openmct.indicators.simpleIndicator(); +ind.text('…').iconClass('icon-info'); +openmct.indicators.add(ind); // or add({ element }) +``` + +## Priority + +`openmct.priority.HIGHEST | HIGH | DEFAULT | LOW | LOWEST` diff --git a/.agent/knowledge/architecture.md b/.agent/knowledge/architecture.md new file mode 100644 index 0000000000..2ef2bffaf4 --- /dev/null +++ b/.agent/knowledge/architecture.md @@ -0,0 +1,32 @@ +# Architecture — Mental Model + +Open MCT is a **framework core** + **plugin registry**. + +Core (`src/MCT.js` → `src/api/*`) owns: + +- object registry & providers +- composition graph +- telemetry pipeline +- time system + clocks +- view / action / indicator / form / menu / notification registries +- priority scheme + +Plugins add: + +- types (kinds of objects) +- object providers (how to fetch models by identifier) +- composition providers (what children an object has) +- telemetry providers (historical + realtime data) +- view providers (how to render an object) +- action providers (context-menu / toolbar operations) +- indicators, formatters, time systems, clocks + +Data flow for a plot: + +1. Time bounds change → view calls `openmct.telemetry.request(obj, opts)`. +2. Matching provider returns `Datum[]` → view renders history. +3. Provider is also subscribed → new datums pushed → view appends live. + +Domain-object identity: +`{ namespace, key }` — namespace usually maps 1:1 to a persistence store +or a plugin's synthetic root. diff --git a/.agent/knowledge/glossary.md b/.agent/knowledge/glossary.md new file mode 100644 index 0000000000..ea99f6e318 --- /dev/null +++ b/.agent/knowledge/glossary.md @@ -0,0 +1,18 @@ +# Glossary + +- **domain object** — anything shown in the tree; `{ namespace, key }` identity. +- **model** — JSON-serializable state of a domain object. +- **composition** — array of child identifiers. +- **namespace** — persistence / provider partition. +- **type** — registered kind of domain object. +- **provider** — plugin implementation of an extension point. +- **datum** — one telemetry sample; a plain JS object keyed by value-metadata + keys. +- **time system** — defines how numeric time values are interpreted and + formatted. +- **clock** — ticking source of "now" values in a time system. +- **bounds** — `{ start, end }` window in the active time system. +- **offsets** — relative `{ start<0, end>=0 }` used with clocks in realtime + mode. +- **view provider** — object with `{ key, canView, view(o, path) }`. +- **action provider** — context-menu / toolbar operation registration. diff --git a/.agent/knowledge/plugin-anatomy.md b/.agent/knowledge/plugin-anatomy.md new file mode 100644 index 0000000000..fd24392023 --- /dev/null +++ b/.agent/knowledge/plugin-anatomy.md @@ -0,0 +1,40 @@ +# Plugin Anatomy + +## Minimum + +```js +export default function MyPlugin(options) { + return function install(openmct) { + /* register types, providers, views, actions */ + }; +} +``` + +## Recommended layout (feature-first) + +```txt +src/plugins/myPlugin/ + plugin.js + pluginSpec.js + README.md + MyProvider.js + MyProviderSpec.js + components/ + MyView.vue + myPlugin.scss +``` + +## Registration + +In `src/plugins/plugins.js`: + +```js +import MyPlugin from './myPlugin/plugin.js'; +plugins.MyPlugin = MyPlugin; +``` + +## Install (host page or test) + +```js +openmct.install(openmct.plugins.MyPlugin(optionalConfig)); +``` diff --git a/.agent/knowledge/reference-plugins.md b/.agent/knowledge/reference-plugins.md new file mode 100644 index 0000000000..523f762106 --- /dev/null +++ b/.agent/knowledge/reference-plugins.md @@ -0,0 +1,15 @@ +# Reference Plugins (Guided Tours) + +- `src/plugins/conjunctionSSA/` — full example: types, root, object provider, + composition tree, telemetry (subscribe + request), condition set, layout. + Read its `README.md` for the walkthrough. +- `src/plugins/telemetryTable/` — view-heavy plugin backed by a telemetry + source. Good template for tabular views. +- `src/plugins/gauge/` — smaller view provider example with a composition + policy and a substantial spec file (`GaugePluginSpec.js`). +- `src/plugins/plot/` — largest view plugin; look here for time-series + rendering patterns and `minmax` strategy handling. +- `src/plugins/condition/` — rule-engine style; useful for derived state. +- `src/plugins/utcTimeSystem/` — the canonical time-system + clock plugin. +- `example/generator/` — synthetic telemetry source; ideal for scaffolding + new telemetry providers. diff --git a/.agent/knowledge/telemetry-datum-shapes.md b/.agent/knowledge/telemetry-datum-shapes.md new file mode 100644 index 0000000000..5790e17697 --- /dev/null +++ b/.agent/knowledge/telemetry-datum-shapes.md @@ -0,0 +1,43 @@ +# Telemetry Metadata & Datums + +## Telemetry object + +```js +{ + identifier: { namespace, key }, + name, + type, + telemetry: { + values: [ + { key: 'utc', source: 'timestamp', format: 'utc', hints: { domain: 1 } }, + { + key: 'value', + name: 'Value', + unit: 'kg', + format: 'float', + min: 0, + max: 100, + hints: { range: 1 } + } + ] + } +} +``` + +## Datum shape + +Keys match `telemetry.values[].source` (or `key` if no `source`): + +```js +{ timestamp: 1712345678000, value: 42 } +``` + +## Rules + +- Exactly one value MUST have `hints.domain` and must correspond to the active + time system's key (map with `source` if the raw field name differs). +- `request()` returns `Promise`, sorted ascending by domain. +- `subscribe()` returns an unsubscribe function; each callback receives + exactly one datum. +- For enums: `format: 'enum'` + `enumerations: [{ value, string }]`. +- For arrays: `format: 'number[]'` or `format: 'string[]'`. diff --git a/.agent/knowledge/time-api.md b/.agent/knowledge/time-api.md new file mode 100644 index 0000000000..f94578567c --- /dev/null +++ b/.agent/knowledge/time-api.md @@ -0,0 +1,41 @@ +# Time API Notes + +Modes: `'realtime'` | `'fixed'`. + +- Realtime uses a clock + offsets. +- Fixed uses absolute bounds. + +Offsets: `{ start: , end: >= 0 }` — relative to +`clock.currentValue()`. + +## Custom clock + +```js +{ + key, name, cssClass, description, + on(event, cb), // event === 'tick' + off(event, cb), + currentValue() +} +``` + +Register: `openmct.time.addClock(clk)`. + +## Custom time system + +```js +{ + key, + name, + cssClass, + timeFormat, // key of a registered format + durationFormat, // key of a registered format + isUTCBased +} +``` + +## Deprecated — do not use in new code + +- Methods: `timeSystem()`, `bounds()`, `clock()`, `clockOffsets()`, + `stopClock()` +- Events: `'bounds'`, `'timeSystem'`, `'clock'`, `'clockOffsets'` diff --git a/.cspell.json b/.cspell.json index f8710f7fc7..efadfa2c29 100644 --- a/.cspell.json +++ b/.cspell.json @@ -2,6 +2,19 @@ "version": "0.2", "language": "en,en-us", "words": [ + "alfano", + "envisat", + "gmst", + "keplerian", + "luni", + "myorg", + "perifocal", + "raan", + "starlink", + "tesserals", + "tles", + "zarya", + "zonals", "gress", "doctoc", "minmax", diff --git a/.devin/rules/00-project-overview.md b/.devin/rules/00-project-overview.md new file mode 100644 index 0000000000..dd82cb1b10 --- /dev/null +++ b/.devin/rules/00-project-overview.md @@ -0,0 +1,22 @@ +--- +trigger: always_on +--- + +# Open MCT — Project Overview + +- Framework for time-series / telemetry dashboards. Entry: `openmct.js` → `src/MCT.js`. +- Composed of ~66 built-in plugins under `src/plugins/`, aggregated in `src/plugins/plugins.js`. +- Extension points live in `src/api/` (objects, composition, telemetry, time, + types, actions, forms, indicators, priority, user, menu, notifications, + tooltips, overlays, annotation, status). +- Demo host: `index.html` (served by `npm start`). +- Build: webpack configs in `.webpack/`. Types generated by `tsc` from JSDoc. +- Unit tests: Karma + Jasmine, `*Spec.js` colocated. E2E: Playwright under `e2e/`. + +Vocabulary (see `.agent/knowledge/glossary.md`): + +- **Domain object**: any item in the tree. Identified by `{namespace, key}`. +- **Model**: JSON-serializable state of a domain object. +- **Composition**: parent→children relation. +- **Type**: registered kind of domain object. +- **Provider**: plugin-supplied implementation (object/composition/telemetry/etc.). diff --git a/.devin/rules/10-code-style.md b/.devin/rules/10-code-style.md new file mode 100644 index 0000000000..b4e070dd34 --- /dev/null +++ b/.devin/rules/10-code-style.md @@ -0,0 +1,38 @@ +--- +trigger: always_on +--- + +# Code Style (enforced by ESLint + Prettier) + +Formatting (Prettier config: `.prettierrc`): + +- Single quotes, no trailing commas, `printWidth: 100`, `endOfLine: auto`. + +Language: + +- ES modules only. `import` at top of file. No `require` in `src/`. +- `const`/`let` only, never `var`. Prefer `const`. +- `===` / `!==` only. `curly` always. No nested ternaries. No bitwise ops. +- Prefer named function declarations over arrow-assigned functions + (`func-style: declaration`). +- One class per file (`max-classes-per-file: 1`). +- Prefer ES6 classes over prototypal patterns. +- Avoid magic numbers — hoist to named `const`s in `UPPER_SNAKE_CASE`. +- No lodash/underscore where a builtin exists (plugin + `you-dont-need-lodash-underscore`). +- No unsanitized DOM writes (`no-unsanitized/DOM`). +- Imports must be sortable (`simple-import-sort/imports`). + +Filenames (`unicorn/filename-case`): + +- JS: camelCase for utility modules; PascalCase for files exporting a class or + Vue component. +- `.vue` and files exporting classes → PascalCase (`MyThing.vue`, `MyThing.js`). + +Organization: **by feature, not by type** (see CONTRIBUTING.md example). + +Do NOT: + +- Add or remove comments/JSDoc unless asked. +- Modify eslint/prettier config to make code pass. +- Introduce Angular, RxJS, or other frameworks. Vue 3 only. diff --git a/.devin/rules/20-plugin-authoring.md b/.devin/rules/20-plugin-authoring.md new file mode 100644 index 0000000000..c184d7a450 --- /dev/null +++ b/.devin/rules/20-plugin-authoring.md @@ -0,0 +1,47 @@ +--- +trigger: glob +globs: src/plugins/** +--- + +# Plugin Authoring Rules + +A plugin is a factory returning an install function: + +```js +export default function MyPlugin(options) { + return function install(openmct) { + // register types, providers, views, actions... + }; +} +``` + +Structure: + +- Folder per plugin: `src/plugins//`. +- Entry: `plugin.js` exporting `default` factory. +- Register in `src/plugins/plugins.js` as `plugins.MyPlugin = MyPlugin;`. +- Colocate `pluginSpec.js` at the plugin root. Split when it grows too large. +- Vue SFCs in `components/` subfolder; PascalCase filenames. +- SCSS colocated (e.g. `myPlugin.scss`), imported from JS where needed. + +Namespaces & identifiers: + +- Always namespace your type keys (e.g. `myorg.thing`) to avoid collisions. +- Object identifiers are `{ namespace, key }`; keys are unique within a namespace. + +Providers to know: + +- `openmct.types.addType(key, def)` +- `openmct.objects.addRoot(idOrFn, priority)` + + `openmct.objects.addProvider(namespace, { get })` +- `openmct.composition.addProvider({ appliesTo, load })` +- `openmct.telemetry.addProvider({ supportsRequest, request, supportsSubscribe, subscribe, ... })` +- `openmct.time.addTimeSystem(...)`, `openmct.time.addClock(...)` +- `openmct.indicators.add(...)`, `openmct.priority.HIGH|LOW|...` + +Reference plugins: + +- `src/plugins/conjunctionSSA/` — full end-to-end example (types, root, + object provider, composition, telemetry, condition set, layout). +- `src/plugins/telemetryTable/` — view-heavy plugin backed by a telemetry source. +- `src/plugins/gauge/` — smaller view provider with composition policy. diff --git a/.devin/rules/21-api-usage.md b/.devin/rules/21-api-usage.md new file mode 100644 index 0000000000..f4ce31331d --- /dev/null +++ b/.devin/rules/21-api-usage.md @@ -0,0 +1,22 @@ +--- +trigger: glob +globs: src/**/*.js,src/**/*.vue +--- + +# Open MCT API Usage + +- Access APIs through the `openmct` instance passed to plugin installers or via + the framework — do NOT import from `src/api/*` directly in plugin code. +- Telemetry datums must include a value keyed to the active time system's `key` + (default `utc`). Metadata declares this via `telemetry.values[].hints.domain`. +- Telemetry providers must return arrays sorted ascending by domain from + `request()`, and must return an unsubscribe function from `subscribe()`. +- View providers should use the `renderWhenVisible(fn)` helper from + `viewOptions` instead of raw `requestAnimationFrame` (see `API.md`). +- Prefer `openmct.priority.*` constants over raw numeric priorities. +- Time API: use `getTimeSystem`/`setTimeSystem`, `getBounds`/`setBounds`, + `getClock`/`setClock`, `getMode`/`setMode`. Deprecated single-name methods + (`timeSystem()`, `bounds()`, `clock()`, `clockOffsets()`, `stopClock()`) + must not be used in new code. +- Events: `boundsChanged`, `timeSystemChanged`, `clockChanged`, + `clockOffsetsChanged`, `modeChanged` — the un-suffixed versions are deprecated. diff --git a/.devin/rules/30-testing-unit.md b/.devin/rules/30-testing-unit.md new file mode 100644 index 0000000000..646da72ca8 --- /dev/null +++ b/.devin/rules/30-testing-unit.md @@ -0,0 +1,22 @@ +--- +trigger: glob +globs: **/*Spec.js +--- + +# Unit Testing (Karma + Jasmine) + +- Colocate `FooSpec.js` next to `Foo.js`. For plugins, start with + `pluginSpec.js` at the plugin root; split as it grows. +- Prefer public API + browser builtins. Do NOT reach into private modules. +- Install the plugin under test in `beforeEach` via `createOpenMct()` from + `src/utils/testing.js`. Tear down with `resetApplicationState(openmct)` in + `afterEach`. +- Declare state inside block scope; initialize in `beforeEach` (not + `beforeAll`) to avoid state leakage between tests. +- Use `spyOnBuiltins` + `clearBuiltinSpies` (called via + `resetApplicationState`) when wrapping `window`/globals. +- Use `getMockObjects`, `getMockTelemetry`, `getLatestTelemetry` helpers where + they fit — extend `setMockObjects()` for new mock shapes. +- Test the plugin's public effects (types added, objects available, telemetry + callbacks invoked) — not internal implementation details. +- Run: `npm test`. Debug: `npm run test:debug`. diff --git a/.devin/rules/31-testing-e2e.md b/.devin/rules/31-testing-e2e.md new file mode 100644 index 0000000000..5e324c425e --- /dev/null +++ b/.devin/rules/31-testing-e2e.md @@ -0,0 +1,19 @@ +--- +trigger: glob +globs: e2e/**/*.js +--- + +# E2E Testing (Playwright) + +- Tests live under `e2e/tests/{functional,visual-a11y,performance,mobile}`. +- Filename pattern: `*.e2e.spec.js`. +- Use existing fixtures from `e2e/baseFixtures.js` and helpers in `e2e/helper/`. +- Tag tests: `@a11y`, `@snapshot`, `@couchdb`, `@generatedata`, `@mobile` — CI + filters on these tags. +- Do NOT interleave visual and functional assertions in the same spec (visual + goes through Percy and won't run everywhere). +- For local iteration: `npm run test:e2e:local -- --debug`. +- CI parity: `npm run test:e2e:ci`. +- Prefer role/label locators over CSS selectors. +- Do not add arbitrary `waitForTimeout`. Prefer `expect(...).toBeVisible()` or + `waitFor` on observable state. diff --git a/.devin/rules/40-vue-components.md b/.devin/rules/40-vue-components.md new file mode 100644 index 0000000000..4985e42c5e --- /dev/null +++ b/.devin/rules/40-vue-components.md @@ -0,0 +1,14 @@ +--- +trigger: glob +globs: **/*.vue +--- + +# Vue 3 Component Rules + +- Vue 3 SFCs. Use `