@memoarchitect/tools 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ import type { DashboardDTO, DashboardScope } from '@memoarchitect/tools';
2
+ export declare const DASHBOARD_SCOPES: readonly DashboardScope[];
3
+ /** The ignore rule for user dashboards, as written into `.gitignore`. */
4
+ export declare const USER_DASHBOARDS_IGNORE = "dashboards/user/";
5
+ /** Directory holding one scope's dashboard files. */
6
+ export declare function dashboardDir(projectRoot: string, scope: DashboardScope): string;
7
+ /**
8
+ * Make sure the project's `.gitignore` keeps user dashboards out of git.
9
+ *
10
+ * Appends one line, never rewrites the file. Already covered is judged by an
11
+ * exact line match on the common spellings; anything cleverer is the user's
12
+ * own rule to keep.
13
+ */
14
+ export declare function ensureUserDashboardsIgnored(projectRoot: string): void;
15
+ /** Frontmatter `title`, else the first `#` heading, else the id. */
16
+ export declare function dashboardTitle(content: string, id: string): string;
17
+ /** Every dashboard in both scopes, shared first, each sorted by title. */
18
+ export declare function loadDashboards(projectRoot: string): DashboardDTO[];
19
+ export declare function saveDashboard(projectRoot: string, scope: DashboardScope, id: string, content: string): string;
20
+ export declare function deleteDashboard(projectRoot: string, scope: DashboardScope, id: string): boolean;
21
+ /**
22
+ * Move a dashboard between scopes.
23
+ *
24
+ * Refuses to overwrite: sharing a user page onto a shared one of the same id
25
+ * would destroy the team's copy, and that must be a deliberate delete first.
26
+ */
27
+ export declare function moveDashboard(projectRoot: string, id: string, from: DashboardScope, to: DashboardScope): string;
28
+ //# sourceMappingURL=dashboard-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-store.d.ts","sourceRoot":"","sources":["../../src/server/dashboard-store.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEzE,eAAO,MAAM,gBAAgB,EAAE,SAAS,cAAc,EAAuB,CAAC;AAE9E,yEAAyE;AACzE,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AA8BzD,qDAAqD;AACrD,wBAAgB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAI/E;AAwBD;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAOrE;AAaD,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAWlE;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,EAAE,CA4BlE;AAED,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAQ7G;AAED,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAO/F;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,GAAG,MAAM,CAY/G"}
@@ -0,0 +1,199 @@
1
+ // ─── Dashboard Store ─────────────────────────────────────────────────────────
2
+ //
3
+ // File-backed persistence for custom dashboards. A dashboard is one markdown
4
+ // file and nothing else — no index, no settings entry — so a dashboard exists
5
+ // because its file exists, and a `git pull` or a hand edit is a complete way
6
+ // to add one.
7
+ //
8
+ // shared <projectRoot>/dashboards/<id>.md committed with the project
9
+ // user <projectRoot>/dashboards/user/<id>.md one person's, never committed
10
+ //
11
+ // The user folder is visible on purpose — a hidden directory is somewhere people
12
+ // lose work. It is kept out of git by a `dashboards/user/` line in the project's
13
+ // own `.gitignore`, which the store adds the first time it writes a user
14
+ // dashboard, and `dashboards/README.md` says what the two folders are for.
15
+ //
16
+ // Dashboards are presentation, not model: they sit outside the SysML include
17
+ // roots and outside viewpoint packages, and only reference views by id.
18
+ //
19
+ // Design: plans/memo-custom-dashboards.md.
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
22
+ import { relative, resolve } from 'node:path';
23
+ import { parse as parseYaml } from 'yaml';
24
+ export const DASHBOARD_SCOPES = ['shared', 'user'];
25
+ /** The ignore rule for user dashboards, as written into `.gitignore`. */
26
+ export const USER_DASHBOARDS_IGNORE = 'dashboards/user/';
27
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
28
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
29
+ /** Files in a dashboard folder that document it rather than being a dashboard. */
30
+ const NOT_DASHBOARDS = new Set(['readme.md']);
31
+ const README = `# Dashboards
32
+
33
+ Markdown pages with live diagrams, opened in MEMO Architect under **Dashboards**.
34
+
35
+ | Folder | Who sees it | In git? |
36
+ | --- | --- | --- |
37
+ | \`dashboards/\` | everyone on the project | committed |
38
+ | \`dashboards/user/\` | only you, on this machine | ignored — listed in \`.gitignore\` |
39
+
40
+ Each \`.md\` file is one dashboard; its file name is its id. Every viewpoint
41
+ also has a generated dashboard until a file named
42
+ \`viewpoint-<viewpoint id>.md\` replaces it.
43
+
44
+ Embed a live, read-only diagram with a directive on its own line:
45
+
46
+ {{diagram:<view id or name>}}
47
+ {{diagram:<view id or name> height=640}}
48
+
49
+ A file in \`dashboards/user/\` with the same name as one in \`dashboards/\`
50
+ replaces it for you only. **Share** in the app moves a user dashboard up into
51
+ \`dashboards/\`.
52
+ `;
53
+ /** Directory holding one scope's dashboard files. */
54
+ export function dashboardDir(projectRoot, scope) {
55
+ return scope === 'shared'
56
+ ? resolve(projectRoot, 'dashboards')
57
+ : resolve(projectRoot, 'dashboards', 'user');
58
+ }
59
+ function assertScope(scope) {
60
+ if (scope !== 'shared' && scope !== 'user') {
61
+ throw new Error(`Unknown dashboard scope "${String(scope)}" — expected "shared" or "user".`);
62
+ }
63
+ }
64
+ /**
65
+ * Refuse, rather than rewrite, an id that is not a plain filename stem.
66
+ *
67
+ * Silently sanitizing would let two different ids land on one file, and a
68
+ * dashboard saved under an id the user did not type is one they cannot find.
69
+ */
70
+ function assertId(id) {
71
+ if (typeof id !== 'string' || !ID_RE.test(id) || NOT_DASHBOARDS.has(`${id.toLowerCase()}.md`)) {
72
+ throw new Error(`Invalid dashboard id "${String(id)}" — use letters, digits, ".", "_" or "-", starting with a letter or digit.`);
73
+ }
74
+ }
75
+ function filePath(projectRoot, scope, id) {
76
+ return resolve(dashboardDir(projectRoot, scope), `${id}.md`);
77
+ }
78
+ /**
79
+ * Make sure the project's `.gitignore` keeps user dashboards out of git.
80
+ *
81
+ * Appends one line, never rewrites the file. Already covered is judged by an
82
+ * exact line match on the common spellings; anything cleverer is the user's
83
+ * own rule to keep.
84
+ */
85
+ export function ensureUserDashboardsIgnored(projectRoot) {
86
+ const path = resolve(projectRoot, '.gitignore');
87
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
88
+ const spellings = new Set([USER_DASHBOARDS_IGNORE, 'dashboards/user', '/dashboards/user/', '/dashboards/user']);
89
+ if (existing.split(/\r?\n/).some(line => spellings.has(line.trim())))
90
+ return;
91
+ const lead = existing === '' || existing.endsWith('\n') ? '' : '\n';
92
+ writeFileSync(path, `${existing}${lead}\n# MEMO Architect: personal dashboards\n${USER_DASHBOARDS_IGNORE}\n`, 'utf8');
93
+ }
94
+ function ensureDir(projectRoot, scope) {
95
+ const shared = dashboardDir(projectRoot, 'shared');
96
+ if (!existsSync(shared))
97
+ mkdirSync(shared, { recursive: true });
98
+ const readme = resolve(shared, 'README.md');
99
+ if (!existsSync(readme))
100
+ writeFileSync(readme, README, 'utf8');
101
+ const dir = dashboardDir(projectRoot, scope);
102
+ if (!existsSync(dir))
103
+ mkdirSync(dir, { recursive: true });
104
+ if (scope === 'user')
105
+ ensureUserDashboardsIgnored(projectRoot);
106
+ return dir;
107
+ }
108
+ /** Frontmatter `title`, else the first `#` heading, else the id. */
109
+ export function dashboardTitle(content, id) {
110
+ const m = content.match(FRONTMATTER_RE);
111
+ if (m) {
112
+ try {
113
+ const meta = parseYaml(m[1]);
114
+ if (meta && typeof meta.title === 'string' && meta.title.trim())
115
+ return meta.title.trim();
116
+ }
117
+ catch { /* malformed frontmatter falls through to the heading */ }
118
+ }
119
+ const body = m ? content.slice(m[0].length) : content;
120
+ const heading = body.match(/^#\s+(.+)$/m);
121
+ return heading ? heading[1].trim() : id;
122
+ }
123
+ /** Every dashboard in both scopes, shared first, each sorted by title. */
124
+ export function loadDashboards(projectRoot) {
125
+ const out = [];
126
+ for (const scope of DASHBOARD_SCOPES) {
127
+ const dir = dashboardDir(projectRoot, scope);
128
+ if (!existsSync(dir))
129
+ continue;
130
+ const inScope = [];
131
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
132
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
133
+ continue;
134
+ if (NOT_DASHBOARDS.has(entry.name.toLowerCase()))
135
+ continue;
136
+ const id = entry.name.slice(0, -3);
137
+ if (!ID_RE.test(id))
138
+ continue;
139
+ const full = resolve(dir, entry.name);
140
+ try {
141
+ const content = readFileSync(full, 'utf8');
142
+ inScope.push({
143
+ id,
144
+ scope,
145
+ title: dashboardTitle(content, id),
146
+ content,
147
+ path: relative(projectRoot, full),
148
+ updatedAt: statSync(full).mtimeMs,
149
+ });
150
+ }
151
+ catch { /* an unreadable file is skipped, not fatal */ }
152
+ }
153
+ inScope.sort((a, b) => a.title.localeCompare(b.title) || a.id.localeCompare(b.id));
154
+ out.push(...inScope);
155
+ }
156
+ return out;
157
+ }
158
+ export function saveDashboard(projectRoot, scope, id, content) {
159
+ assertScope(scope);
160
+ assertId(id);
161
+ if (typeof content !== 'string')
162
+ throw new Error('Dashboard content must be a string.');
163
+ ensureDir(projectRoot, scope);
164
+ const path = filePath(projectRoot, scope, id);
165
+ writeFileSync(path, content, 'utf8');
166
+ return relative(projectRoot, path);
167
+ }
168
+ export function deleteDashboard(projectRoot, scope, id) {
169
+ assertScope(scope);
170
+ assertId(id);
171
+ const path = filePath(projectRoot, scope, id);
172
+ if (!existsSync(path))
173
+ return false;
174
+ unlinkSync(path);
175
+ return true;
176
+ }
177
+ /**
178
+ * Move a dashboard between scopes.
179
+ *
180
+ * Refuses to overwrite: sharing a user page onto a shared one of the same id
181
+ * would destroy the team's copy, and that must be a deliberate delete first.
182
+ */
183
+ export function moveDashboard(projectRoot, id, from, to) {
184
+ assertScope(from);
185
+ assertScope(to);
186
+ assertId(id);
187
+ if (from === to)
188
+ return relative(projectRoot, filePath(projectRoot, from, id));
189
+ const source = filePath(projectRoot, from, id);
190
+ if (!existsSync(source))
191
+ throw new Error(`No ${from} dashboard "${id}".`);
192
+ const target = filePath(projectRoot, to, id);
193
+ if (existsSync(target))
194
+ throw new Error(`A ${to} dashboard "${id}" already exists — delete or rename it first.`);
195
+ ensureDir(projectRoot, to);
196
+ renameSync(source, target);
197
+ return relative(projectRoot, target);
198
+ }
199
+ //# sourceMappingURL=dashboard-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-store.js","sourceRoot":"","sources":["../../src/server/dashboard-store.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../src/server/dev-server.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACR,aAAa,EAM6B,sBAAsB,EAChE,sBAAsB,EAEzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AA2B9D,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,aAAa,EAAE,CAAC;IACjC,4FAA4F;IAC5F,kBAAkB,CAAC,EAAE,iBAAiB,CAAC;IACvC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,kEAAkE;IAClE,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,qEAAqE;IACrE,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,SAAS;IACtB,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,SAAS,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACjF;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;IACxC,mEAAmE;IACnE,wBAAwB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG;QAChD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,cAAc,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF,uEAAuE;IACvE,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACrD;;;;;;;OAOG;IACH,kBAAkB,CAAC,OAAO,EAAE,sBAAsB,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IACrE,KAAK,IAAI,IAAI,CAAC;CACjB;AAaD,iEAAiE;AACjE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAO3D;AAqED,6EAA6E;AAC7E,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAWtG;AAiCD,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CA2lEnF"}
1
+ {"version":3,"file":"dev-server.d.ts","sourceRoot":"","sources":["../../src/server/dev-server.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACR,aAAa,EAM6B,sBAAsB,EAChE,sBAAsB,EAEzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AA6B9D,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,aAAa,EAAE,CAAC;IACjC,4FAA4F;IAC5F,kBAAkB,CAAC,EAAE,iBAAiB,CAAC;IACvC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,0DAA0D;IAC1D,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,kEAAkE;IAClE,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,qEAAqE;IACrE,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD;AAED,MAAM,WAAW,SAAS;IACtB,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6DAA6D;IAC7D,SAAS,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACjF;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;IACxC,mEAAmE;IACnE,wBAAwB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG;QAChD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAChC,cAAc,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF,uEAAuE;IACvE,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACrD;;;;;;;OAOG;IACH,kBAAkB,CAAC,OAAO,EAAE,sBAAsB,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IACrE,KAAK,IAAI,IAAI,CAAC;CACjB;AAaD,iEAAiE;AACjE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAO3D;AAqED,6EAA6E;AAC7E,wBAAgB,0BAA0B,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAWtG;AAiCD,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,SAAS,CAAC,CAqoEnF"}
@@ -23,6 +23,8 @@ import { loadViewLayouts, saveViewLayout } from './view-layout-store.js';
23
23
  import { updateViewExpose, writeViewDeclaration } from './view-writer.js';
24
24
  import { usageKeyword } from '../model/semantic.js';
25
25
  import { loadDhfDocs, saveDhfDoc, deleteDhfDoc, loadDhfSettings, saveDhfSettings, listRepoTemplates, readRepoTemplate, saveRepoTemplate, } from './dhf-doc-store.js';
26
+ import { loadDashboards, saveDashboard, deleteDashboard, moveDashboard } from './dashboard-store.js';
27
+ import { createDashboardWatcher } from './file-watcher.js';
26
28
  const MODEL_MUTATION_MESSAGES = new Set([
27
29
  'element:update', 'element:create', 'element:delete', 'element:remap-kinds', 'element:move-package',
28
30
  'package:create', 'package:rename', 'package:delete', 'package:move',
@@ -384,6 +386,21 @@ export async function createDevServer(options) {
384
386
  const { WebSocketServer } = await import('ws');
385
387
  const wss = new WebSocketServer({ server });
386
388
  const clients = new Set();
389
+ const broadcastDashboards = () => {
390
+ let message;
391
+ try {
392
+ message = JSON.stringify({ type: 'dashboards', payload: { dashboards: loadDashboards(options.projectRoot) } });
393
+ }
394
+ catch (e) {
395
+ console.error('[Dashboards] reload failed:', e);
396
+ return;
397
+ }
398
+ for (const client of clients) {
399
+ if (client.readyState === 1)
400
+ client.send(message);
401
+ }
402
+ };
403
+ const dashboardWatcher = createDashboardWatcher(options.projectRoot, broadcastDashboards);
387
404
  // Every view is DECLARED in SysML — see `view-writer.ts`. The sidecar at
388
405
  // .memo/user-diagrams.json used to be merged in here as a second class of
389
406
  // view, which is how a view could exist for Architect and for nothing else:
@@ -1117,6 +1134,13 @@ export async function createDevServer(options) {
1117
1134
  if (Object.keys(layouts).length > 0) {
1118
1135
  ws.send(JSON.stringify({ type: 'diagram:layout', payload: { layouts } }));
1119
1136
  }
1137
+ // Send custom dashboards on connect
1138
+ try {
1139
+ ws.send(JSON.stringify({ type: 'dashboards', payload: { dashboards: loadDashboards(options.projectRoot) } }));
1140
+ }
1141
+ catch (e) {
1142
+ console.error('[Dashboards] initial load failed:', e);
1143
+ }
1120
1144
  // Send persisted DHF documents and settings on connect
1121
1145
  try {
1122
1146
  const docs = loadDhfDocs(options.projectRoot);
@@ -2115,6 +2139,30 @@ Return ONLY a JSON array of strings. Each string is a concise, actionable sugges
2115
2139
  ws.send(JSON.stringify({ type: 'llm:suggest:result', payload: { requestId, error: e?.message ?? String(e) } }));
2116
2140
  }
2117
2141
  }
2142
+ else if (msg.type === 'dashboard:save' || msg.type === 'dashboard:delete' || msg.type === 'dashboard:move') {
2143
+ // Dashboards are presentation, not model source: they are
2144
+ // deliberately absent from MODEL_MUTATION_MESSAGES, so a
2145
+ // typo in the SysML never stops a user writing notes.
2146
+ const p = msg.payload ?? {};
2147
+ try {
2148
+ if (msg.type === 'dashboard:save') {
2149
+ saveDashboard(options.projectRoot, p.dashboard?.scope, p.dashboard?.id, p.dashboard?.content);
2150
+ }
2151
+ else if (msg.type === 'dashboard:delete') {
2152
+ deleteDashboard(options.projectRoot, p.scope, p.id);
2153
+ }
2154
+ else {
2155
+ moveDashboard(options.projectRoot, p.id, p.from, p.to);
2156
+ }
2157
+ ws.send(JSON.stringify({ type: 'dashboard:result', payload: { requestId: p.requestId, ok: true } }));
2158
+ // Answer at once rather than waiting on the watcher's
2159
+ // debounce, so the writer sees its own save immediately.
2160
+ broadcastDashboards();
2161
+ }
2162
+ catch (e) {
2163
+ ws.send(JSON.stringify({ type: 'dashboard:result', payload: { requestId: p.requestId, ok: false, error: e?.message ?? String(e) } }));
2164
+ }
2165
+ }
2118
2166
  else if (msg.type === 'dhf:docs:load') {
2119
2167
  const docs = loadDhfDocs(options.projectRoot);
2120
2168
  ws.send(JSON.stringify({ type: 'dhf:docs', payload: { docs } }));
@@ -2295,6 +2343,7 @@ Return ONLY a JSON array of strings. Each string is a concise, actionable sugges
2295
2343
  }
2296
2344
  },
2297
2345
  close() {
2346
+ dashboardWatcher.close();
2298
2347
  wss.close();
2299
2348
  viteServer?.close();
2300
2349
  server.close();