@seanmars/tospec 0.23.0 → 0.26.0-beta
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.
- package/CHANGELOG.md +281 -0
- package/README.md +27 -9
- package/assets/claude-mod/tospec-dashboard/.claude-plugin/plugin.json +7 -0
- package/assets/claude-mod/tospec-dashboard/hooks/hooks.json +1 -0
- package/assets/claude-mod/tospec-dashboard/hooks/register.ts +695 -0
- package/assets/claude-mod/tospec-dashboard/tests/register.test.ts +448 -0
- package/assets/claude-mod/tospec-dashboard/types/index.d.ts +8 -0
- package/assets/dashboard/app.js +218 -1180
- package/assets/dashboard/index.html +31 -22
- package/assets/dashboard/lib/charts.js +355 -0
- package/assets/dashboard/lib/derive.js +375 -0
- package/assets/dashboard/lib/dom.js +121 -0
- package/assets/dashboard/lib/routes.js +52 -0
- package/assets/dashboard/lib/store.js +326 -0
- package/assets/dashboard/style.css +3103 -664
- package/assets/dashboard/views/alerts.js +170 -0
- package/assets/dashboard/views/change.js +234 -0
- package/assets/dashboard/views/docs.js +214 -0
- package/assets/dashboard/views/home.js +357 -0
- package/assets/dashboard/views/project.js +168 -0
- package/assets/dashboard/views/session.js +270 -0
- package/assets/dashboard/views/sidebar.js +131 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +116 -102
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +213 -100
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/dashboard-relay.d.ts +11 -0
- package/dist/commands/dashboard-relay.d.ts.map +1 -0
- package/dist/commands/dashboard-relay.js +88 -0
- package/dist/commands/dashboard-relay.js.map +1 -0
- package/dist/commands/dashboard.d.ts +69 -100
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +420 -598
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/decision.d.ts +38 -1
- package/dist/commands/decision.d.ts.map +1 -1
- package/dist/commands/decision.js +303 -27
- package/dist/commands/decision.js.map +1 -1
- package/dist/commands/metrics.d.ts +3 -4
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +3 -5
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/shared-output.d.ts +24 -0
- package/dist/commands/shared-output.d.ts.map +1 -1
- package/dist/commands/shared-output.js +21 -2
- package/dist/commands/shared-output.js.map +1 -1
- package/dist/commands/show.d.ts.map +1 -1
- package/dist/commands/show.js +12 -9
- package/dist/commands/show.js.map +1 -1
- package/dist/commands/validate.d.ts +19 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +243 -64
- package/dist/commands/validate.js.map +1 -1
- package/dist/commands/workflow/instructions.d.ts +7 -1
- package/dist/commands/workflow/instructions.d.ts.map +1 -1
- package/dist/commands/workflow/instructions.js +299 -81
- package/dist/commands/workflow/instructions.js.map +1 -1
- package/dist/commands/workflow/new-change.d.ts.map +1 -1
- package/dist/commands/workflow/new-change.js +26 -17
- package/dist/commands/workflow/new-change.js.map +1 -1
- package/dist/commands/workflow/prompt-escape.d.ts +25 -0
- package/dist/commands/workflow/prompt-escape.d.ts.map +1 -0
- package/dist/commands/workflow/prompt-escape.js +62 -0
- package/dist/commands/workflow/prompt-escape.js.map +1 -0
- package/dist/commands/workflow/schemas.d.ts.map +1 -1
- package/dist/commands/workflow/schemas.js +34 -16
- package/dist/commands/workflow/schemas.js.map +1 -1
- package/dist/commands/workflow/shared.d.ts +46 -10
- package/dist/commands/workflow/shared.d.ts.map +1 -1
- package/dist/commands/workflow/shared.js +66 -30
- package/dist/commands/workflow/shared.js.map +1 -1
- package/dist/commands/workflow/status.d.ts +5 -2
- package/dist/commands/workflow/status.d.ts.map +1 -1
- package/dist/commands/workflow/status.js +62 -28
- package/dist/commands/workflow/status.js.map +1 -1
- package/dist/core/archive.d.ts +38 -0
- package/dist/core/archive.d.ts.map +1 -1
- package/dist/core/archive.js +492 -75
- package/dist/core/archive.js.map +1 -1
- package/dist/core/artifact-graph/index.d.ts +2 -2
- package/dist/core/artifact-graph/index.d.ts.map +1 -1
- package/dist/core/artifact-graph/index.js +2 -2
- package/dist/core/artifact-graph/index.js.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.d.ts +8 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
- package/dist/core/artifact-graph/instruction-loader.js +58 -36
- package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
- package/dist/core/artifact-graph/resolver.d.ts +39 -12
- package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
- package/dist/core/artifact-graph/resolver.js +82 -36
- package/dist/core/artifact-graph/resolver.js.map +1 -1
- package/dist/core/artifact-graph/schema.d.ts +24 -0
- package/dist/core/artifact-graph/schema.d.ts.map +1 -1
- package/dist/core/artifact-graph/schema.js +32 -1
- package/dist/core/artifact-graph/schema.js.map +1 -1
- package/dist/core/artifact-graph/stub-detection.d.ts +10 -2
- package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
- package/dist/core/artifact-graph/stub-detection.js +18 -11
- package/dist/core/artifact-graph/stub-detection.js.map +1 -1
- package/dist/core/artifact-graph/types.d.ts +2 -2
- package/dist/core/artifact-graph/types.d.ts.map +1 -1
- package/dist/core/artifact-graph/types.js +10 -1
- package/dist/core/artifact-graph/types.js.map +1 -1
- package/dist/core/change-metadata/schema.d.ts.map +1 -1
- package/dist/core/change-metadata/schema.js +18 -4
- package/dist/core/change-metadata/schema.js.map +1 -1
- package/dist/core/change-presenter.d.ts.map +1 -1
- package/dist/core/change-presenter.js +13 -8
- package/dist/core/change-presenter.js.map +1 -1
- package/dist/core/change-status-policy.d.ts.map +1 -1
- package/dist/core/change-status-policy.js +3 -1
- package/dist/core/change-status-policy.js.map +1 -1
- package/dist/core/codex-metrics.d.ts +1 -1
- package/dist/core/codex-metrics.js +1 -1
- package/dist/core/config-schema.d.ts +8 -0
- package/dist/core/config-schema.d.ts.map +1 -1
- package/dist/core/config-schema.js +10 -0
- package/dist/core/config-schema.js.map +1 -1
- package/dist/core/dashboard-activity.d.ts +4 -4
- package/dist/core/dashboard-activity.js +4 -4
- package/dist/core/dashboard-data.d.ts +11 -5
- package/dist/core/dashboard-data.d.ts.map +1 -1
- package/dist/core/dashboard-data.js +54 -28
- package/dist/core/dashboard-data.js.map +1 -1
- package/dist/core/dashboard-hub.d.ts +258 -0
- package/dist/core/dashboard-hub.d.ts.map +1 -0
- package/dist/core/dashboard-hub.js +655 -0
- package/dist/core/dashboard-hub.js.map +1 -0
- package/dist/core/dashboard-routes.d.ts +57 -0
- package/dist/core/dashboard-routes.d.ts.map +1 -0
- package/dist/core/dashboard-routes.js +398 -0
- package/dist/core/dashboard-routes.js.map +1 -0
- package/dist/core/global-config.d.ts +71 -2
- package/dist/core/global-config.d.ts.map +1 -1
- package/dist/core/global-config.js +149 -4
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/id.d.ts +9 -0
- package/dist/core/id.d.ts.map +1 -1
- package/dist/core/id.js +37 -0
- package/dist/core/id.js.map +1 -1
- package/dist/core/init.d.ts +9 -2
- package/dist/core/init.d.ts.map +1 -1
- package/dist/core/init.js +119 -33
- package/dist/core/init.js.map +1 -1
- package/dist/core/list.d.ts.map +1 -1
- package/dist/core/list.js +69 -45
- package/dist/core/list.js.map +1 -1
- package/dist/core/local-server.d.ts +10 -3
- package/dist/core/local-server.d.ts.map +1 -1
- package/dist/core/local-server.js +25 -5
- package/dist/core/local-server.js.map +1 -1
- package/dist/core/markdown-render.d.ts.map +1 -1
- package/dist/core/markdown-render.js +7 -2
- package/dist/core/markdown-render.js.map +1 -1
- package/dist/core/migrate.d.ts +24 -2
- package/dist/core/migrate.d.ts.map +1 -1
- package/dist/core/migrate.js +335 -59
- package/dist/core/migrate.js.map +1 -1
- package/dist/core/parsers/change-parser.d.ts +20 -1
- package/dist/core/parsers/change-parser.d.ts.map +1 -1
- package/dist/core/parsers/change-parser.js +76 -73
- package/dist/core/parsers/change-parser.js.map +1 -1
- package/dist/core/parsers/markdown-parser.d.ts +24 -0
- package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
- package/dist/core/parsers/markdown-parser.js +65 -10
- package/dist/core/parsers/markdown-parser.js.map +1 -1
- package/dist/core/parsers/requirement-blocks.d.ts +127 -6
- package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
- package/dist/core/parsers/requirement-blocks.js +349 -56
- package/dist/core/parsers/requirement-blocks.js.map +1 -1
- package/dist/core/parsers/requirement-text.d.ts +25 -1
- package/dist/core/parsers/requirement-text.d.ts.map +1 -1
- package/dist/core/parsers/requirement-text.js +62 -6
- package/dist/core/parsers/requirement-text.js.map +1 -1
- package/dist/core/parsers/spec-structure.d.ts.map +1 -1
- package/dist/core/parsers/spec-structure.js +36 -24
- package/dist/core/parsers/spec-structure.js.map +1 -1
- package/dist/core/project-config.d.ts +9 -2
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +30 -7
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/project-layout.d.ts +26 -0
- package/dist/core/project-layout.d.ts.map +1 -1
- package/dist/core/project-layout.js +51 -1
- package/dist/core/project-layout.js.map +1 -1
- package/dist/core/schema-names.d.ts +17 -0
- package/dist/core/schema-names.d.ts.map +1 -1
- package/dist/core/schema-names.js +24 -0
- package/dist/core/schema-names.js.map +1 -1
- package/dist/core/shared/dashboard-mod.d.ts +24 -0
- package/dist/core/shared/dashboard-mod.d.ts.map +1 -0
- package/dist/core/shared/dashboard-mod.js +44 -0
- package/dist/core/shared/dashboard-mod.js.map +1 -0
- package/dist/core/shared/index.d.ts +2 -1
- package/dist/core/shared/index.d.ts.map +1 -1
- package/dist/core/shared/index.js +2 -1
- package/dist/core/shared/index.js.map +1 -1
- package/dist/core/shared/rules-generation.d.ts +12 -0
- package/dist/core/shared/rules-generation.d.ts.map +1 -1
- package/dist/core/shared/rules-generation.js +26 -2
- package/dist/core/shared/rules-generation.js.map +1 -1
- package/dist/core/shared/skill-generation.d.ts +7 -0
- package/dist/core/shared/skill-generation.d.ts.map +1 -1
- package/dist/core/shared/skill-generation.js +19 -0
- package/dist/core/shared/skill-generation.js.map +1 -1
- package/dist/core/shared/tool-detection.d.ts +10 -1
- package/dist/core/shared/tool-detection.d.ts.map +1 -1
- package/dist/core/shared/tool-detection.js +37 -6
- package/dist/core/shared/tool-detection.js.map +1 -1
- package/dist/core/spec-presenter.d.ts.map +1 -1
- package/dist/core/spec-presenter.js +23 -1
- package/dist/core/spec-presenter.js.map +1 -1
- package/dist/core/specs-apply.d.ts +98 -1
- package/dist/core/specs-apply.d.ts.map +1 -1
- package/dist/core/specs-apply.js +426 -63
- package/dist/core/specs-apply.js.map +1 -1
- package/dist/core/templates/fragments/change-diff.d.ts +37 -0
- package/dist/core/templates/fragments/change-diff.d.ts.map +1 -0
- package/dist/core/templates/fragments/change-diff.js +37 -0
- package/dist/core/templates/fragments/change-diff.js.map +1 -0
- package/dist/core/templates/fragments/sync-fail.d.ts +8 -0
- package/dist/core/templates/fragments/sync-fail.d.ts.map +1 -0
- package/dist/core/templates/fragments/sync-fail.js +8 -0
- package/dist/core/templates/fragments/sync-fail.js.map +1 -0
- package/dist/core/templates/fragments/validate.d.ts +14 -1
- package/dist/core/templates/fragments/validate.d.ts.map +1 -1
- package/dist/core/templates/fragments/validate.js +14 -1
- package/dist/core/templates/fragments/validate.js.map +1 -1
- package/dist/core/templates/fragments/verify.d.ts +2 -2
- package/dist/core/templates/fragments/verify.d.ts.map +1 -1
- package/dist/core/templates/fragments/verify.js +8 -5
- package/dist/core/templates/fragments/verify.js.map +1 -1
- package/dist/core/templates/workflows/apply.d.ts +1 -1
- package/dist/core/templates/workflows/apply.d.ts.map +1 -1
- package/dist/core/templates/workflows/apply.js +29 -10
- package/dist/core/templates/workflows/apply.js.map +1 -1
- package/dist/core/templates/workflows/archive.d.ts.map +1 -1
- package/dist/core/templates/workflows/archive.js +27 -9
- package/dist/core/templates/workflows/archive.js.map +1 -1
- package/dist/core/templates/workflows/decision.d.ts.map +1 -1
- package/dist/core/templates/workflows/decision.js +10 -4
- package/dist/core/templates/workflows/decision.js.map +1 -1
- package/dist/core/templates/workflows/explore.d.ts +2 -2
- package/dist/core/templates/workflows/explore.d.ts.map +1 -1
- package/dist/core/templates/workflows/explore.js +7 -3
- package/dist/core/templates/workflows/explore.js.map +1 -1
- package/dist/core/templates/workflows/grill.d.ts +2 -2
- package/dist/core/templates/workflows/grill.d.ts.map +1 -1
- package/dist/core/templates/workflows/grill.js +5 -4
- package/dist/core/templates/workflows/grill.js.map +1 -1
- package/dist/core/templates/workflows/issue.d.ts.map +1 -1
- package/dist/core/templates/workflows/issue.js +9 -3
- package/dist/core/templates/workflows/issue.js.map +1 -1
- package/dist/core/templates/workflows/propose.d.ts.map +1 -1
- package/dist/core/templates/workflows/propose.js +4 -1
- package/dist/core/templates/workflows/propose.js.map +1 -1
- package/dist/core/templates/workflows/sync.d.ts.map +1 -1
- package/dist/core/templates/workflows/sync.js +25 -11
- package/dist/core/templates/workflows/sync.js.map +1 -1
- package/dist/core/templates/workflows/update.d.ts.map +1 -1
- package/dist/core/templates/workflows/update.js +17 -10
- package/dist/core/templates/workflows/update.js.map +1 -1
- package/dist/core/update.d.ts +5 -0
- package/dist/core/update.d.ts.map +1 -1
- package/dist/core/update.js +69 -29
- package/dist/core/update.js.map +1 -1
- package/dist/core/validation/constants.d.ts +7 -1
- package/dist/core/validation/constants.d.ts.map +1 -1
- package/dist/core/validation/constants.js +16 -2
- package/dist/core/validation/constants.js.map +1 -1
- package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
- package/dist/core/validation/purpose-placeholder.js +35 -11
- package/dist/core/validation/purpose-placeholder.js.map +1 -1
- package/dist/core/validation/section-validator.d.ts +7 -0
- package/dist/core/validation/section-validator.d.ts.map +1 -1
- package/dist/core/validation/section-validator.js +22 -2
- package/dist/core/validation/section-validator.js.map +1 -1
- package/dist/core/validation/sister-changes.d.ts +93 -0
- package/dist/core/validation/sister-changes.d.ts.map +1 -0
- package/dist/core/validation/sister-changes.js +180 -0
- package/dist/core/validation/sister-changes.js.map +1 -0
- package/dist/core/validation/task-markers.d.ts +9 -0
- package/dist/core/validation/task-markers.d.ts.map +1 -0
- package/dist/core/validation/task-markers.js +71 -0
- package/dist/core/validation/task-markers.js.map +1 -0
- package/dist/core/validation/types.d.ts +58 -0
- package/dist/core/validation/types.d.ts.map +1 -1
- package/dist/core/validation/types.js +22 -0
- package/dist/core/validation/types.js.map +1 -1
- package/dist/core/validation/validator.d.ts +79 -2
- package/dist/core/validation/validator.d.ts.map +1 -1
- package/dist/core/validation/validator.js +565 -84
- package/dist/core/validation/validator.js.map +1 -1
- package/dist/utils/change-metadata.d.ts +62 -5
- package/dist/utils/change-metadata.d.ts.map +1 -1
- package/dist/utils/change-metadata.js +193 -31
- package/dist/utils/change-metadata.js.map +1 -1
- package/dist/utils/change-utils.d.ts +21 -0
- package/dist/utils/change-utils.d.ts.map +1 -1
- package/dist/utils/change-utils.js +168 -53
- package/dist/utils/change-utils.js.map +1 -1
- package/dist/utils/command-line.d.ts +11 -0
- package/dist/utils/command-line.d.ts.map +1 -0
- package/dist/utils/command-line.js +43 -0
- package/dist/utils/command-line.js.map +1 -0
- package/dist/utils/display-width.d.ts +10 -0
- package/dist/utils/display-width.d.ts.map +1 -0
- package/dist/utils/display-width.js +15 -0
- package/dist/utils/display-width.js.map +1 -0
- package/dist/utils/file-system.d.ts +16 -0
- package/dist/utils/file-system.d.ts.map +1 -1
- package/dist/utils/file-system.js +49 -2
- package/dist/utils/file-system.js.map +1 -1
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +11 -5
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/interactive.d.ts.map +1 -1
- package/dist/utils/interactive.js +3 -1
- package/dist/utils/interactive.js.map +1 -1
- package/dist/utils/item-discovery.d.ts +3 -2
- package/dist/utils/item-discovery.d.ts.map +1 -1
- package/dist/utils/item-discovery.js +34 -16
- package/dist/utils/item-discovery.js.map +1 -1
- package/dist/utils/link.d.ts +0 -1
- package/dist/utils/link.d.ts.map +1 -1
- package/dist/utils/link.js +0 -1
- package/dist/utils/link.js.map +1 -1
- package/dist/utils/match.d.ts.map +1 -1
- package/dist/utils/match.js +10 -1
- package/dist/utils/match.js.map +1 -1
- package/dist/utils/option-flags.d.ts +9 -0
- package/dist/utils/option-flags.d.ts.map +1 -0
- package/dist/utils/option-flags.js +12 -0
- package/dist/utils/option-flags.js.map +1 -0
- package/dist/utils/sentence.d.ts +9 -0
- package/dist/utils/sentence.d.ts.map +1 -0
- package/dist/utils/sentence.js +12 -0
- package/dist/utils/sentence.js.map +1 -0
- package/dist/utils/spec-files.d.ts +84 -1
- package/dist/utils/spec-files.d.ts.map +1 -1
- package/dist/utils/spec-files.js +192 -3
- package/dist/utils/spec-files.js.map +1 -1
- package/dist/utils/task-progress.d.ts.map +1 -1
- package/dist/utils/task-progress.js +32 -9
- package/dist/utils/task-progress.js.map +1 -1
- package/dist/utils/timestamp.d.ts +27 -0
- package/dist/utils/timestamp.d.ts.map +1 -1
- package/dist/utils/timestamp.js +37 -0
- package/dist/utils/timestamp.js.map +1 -1
- package/package.json +6 -2
- package/schemas/decision/templates/decision.md +2 -2
- package/schemas/issue/schema.yaml +12 -6
- package/schemas/issue/templates/task.md +1 -1
- package/schemas/sdd/schema.yaml +21 -10
- package/schemas/sdd/templates/proposal.md +1 -1
- package/assets/dashboard/chart.umd.js +0 -14
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// Everything the page knows, fetched from the hub. The hub answers every
|
|
2
|
+
// per-project read from the snapshot the project's session last pushed, so an
|
|
3
|
+
// offline project still reads; only /render and the task write need a live
|
|
4
|
+
// session.
|
|
5
|
+
|
|
6
|
+
import { assignLineSlots, stationOf } from './derive.js';
|
|
7
|
+
|
|
8
|
+
const SLOT_KEY = 'tospec-line-slots';
|
|
9
|
+
|
|
10
|
+
export const store = {
|
|
11
|
+
hubConnected: true,
|
|
12
|
+
projectsLoaded: false,
|
|
13
|
+
/** False when the hub is bound beyond loopback: the page may watch, not answer sessions or tick tasks. */
|
|
14
|
+
interactive: false,
|
|
15
|
+
/** /api/projects, sorted by name so rows do not jump when a push re-sorts the hub's list. */
|
|
16
|
+
projects: [],
|
|
17
|
+
/** key -> { overview, activity, archive, details: Map<name, detail>, decisions, error, loaded } */
|
|
18
|
+
data: new Map(),
|
|
19
|
+
slots: new Map(),
|
|
20
|
+
/** `<key>/<session>` -> { events, error }, for sessions whose log the page has open. */
|
|
21
|
+
logs: new Map(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const listeners = new Set();
|
|
25
|
+
let pending = false;
|
|
26
|
+
|
|
27
|
+
/** Coalesces bursts (a push refreshes several requests) into one render. */
|
|
28
|
+
function emit() {
|
|
29
|
+
if (pending) return;
|
|
30
|
+
pending = true;
|
|
31
|
+
queueMicrotask(() => {
|
|
32
|
+
pending = false;
|
|
33
|
+
for (const fn of listeners) fn();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function onStoreChange(fn) {
|
|
38
|
+
listeners.add(fn);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class ApiError extends Error {
|
|
42
|
+
constructor(message, status) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.status = status;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function api(path, options) {
|
|
49
|
+
const res = await fetch(path, options);
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
let detail = 'HTTP ' + res.status;
|
|
52
|
+
try {
|
|
53
|
+
const body = await res.json();
|
|
54
|
+
// `fix` rides along on a project-side refusal (a file where the layout
|
|
55
|
+
// expects a directory); without it the page names the problem only.
|
|
56
|
+
if (body && body.error) detail = body.fix ? body.error + ' ' + body.fix : body.error;
|
|
57
|
+
} catch {
|
|
58
|
+
// No JSON body; keep the status line.
|
|
59
|
+
}
|
|
60
|
+
throw new ApiError(detail, res.status);
|
|
61
|
+
}
|
|
62
|
+
return res.json();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function projectApi(key, path, options) {
|
|
66
|
+
return api('/api/p/' + key + path, options);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function projectByKey(key) {
|
|
70
|
+
return store.projects.find((p) => p.key === key) || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function dataOf(key) {
|
|
74
|
+
let data = store.data.get(key);
|
|
75
|
+
if (!data) {
|
|
76
|
+
data = { overview: null, activity: null, archive: null, details: new Map(), decisions: null, error: null, loaded: false };
|
|
77
|
+
store.data.set(key, data);
|
|
78
|
+
}
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function setConnected(ok) {
|
|
83
|
+
if (store.hubConnected !== ok) {
|
|
84
|
+
store.hubConnected = ok;
|
|
85
|
+
emit();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readSavedSlots() {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(localStorage.getItem(SLOT_KEY) || '{}');
|
|
92
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
93
|
+
} catch {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function refreshSlots() {
|
|
99
|
+
const saved = readSavedSlots();
|
|
100
|
+
store.slots = assignLineSlots(store.projects.map((p) => p.key), saved);
|
|
101
|
+
for (const [key, slot] of store.slots) saved[key] = slot;
|
|
102
|
+
try {
|
|
103
|
+
localStorage.setItem(SLOT_KEY, JSON.stringify(saved));
|
|
104
|
+
} catch {
|
|
105
|
+
// Storage full or blocked: slots still hold for this page load.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function slotOf(key) {
|
|
110
|
+
return store.slots.get(key) || 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function loadProjects() {
|
|
114
|
+
try {
|
|
115
|
+
const { projects, interactive } = await api('/api/projects');
|
|
116
|
+
store.interactive = interactive === true;
|
|
117
|
+
store.projects = projects.slice().sort((a, b) => a.name.localeCompare(b.name) || a.key.localeCompare(b.key));
|
|
118
|
+
store.projectsLoaded = true;
|
|
119
|
+
refreshSlots();
|
|
120
|
+
setConnected(true);
|
|
121
|
+
emit();
|
|
122
|
+
} catch {
|
|
123
|
+
setConnected(false);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The reads the route map needs for one project, rendered as each lands.
|
|
129
|
+
*
|
|
130
|
+
* Usually every read is a snapshot hit. Right after a task write it is not:
|
|
131
|
+
* the hub empties the project's cache until the session pushes again, so each
|
|
132
|
+
* read becomes a relayed `tospec dashboard query`, one at a time. Waiting for
|
|
133
|
+
* all of them before drawing anything held the whole map for seconds; the
|
|
134
|
+
* overview alone places every train, so it draws first and the rest fill in.
|
|
135
|
+
*/
|
|
136
|
+
export async function loadProject(key) {
|
|
137
|
+
const data = dataOf(key);
|
|
138
|
+
let overview;
|
|
139
|
+
try {
|
|
140
|
+
overview = await projectApi(key, '/overview');
|
|
141
|
+
} catch (e) {
|
|
142
|
+
data.error = e.status ? e.message : '無法連到 hub';
|
|
143
|
+
data.loaded = true;
|
|
144
|
+
if (!e.status) setConnected(false);
|
|
145
|
+
emit();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
Object.assign(data, { overview, error: null, loaded: true });
|
|
149
|
+
setConnected(true);
|
|
150
|
+
emit();
|
|
151
|
+
await Promise.all([
|
|
152
|
+
projectApi(key, '/activity?days=30').then((activity) => { data.activity = activity; emit(); }, () => {}),
|
|
153
|
+
projectApi(key, '/archive').then((archive) => { data.archive = archive; emit(); }, () => {}),
|
|
154
|
+
// Only a planning train's gap names artifacts; the rest load on demand.
|
|
155
|
+
...overview.changes.filter((c) => stationOf(c) === 'plan').map((c) => loadDetail(key, c.name, true)),
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const detailsInFlight = new Set();
|
|
160
|
+
|
|
161
|
+
/** One change's artifacts and files. `force` re-reads a detail already held. */
|
|
162
|
+
export function loadDetail(key, name, force = false) {
|
|
163
|
+
const data = dataOf(key);
|
|
164
|
+
const id = key + '/' + name;
|
|
165
|
+
if (detailsInFlight.has(id) || (!force && data.details.has(name))) return Promise.resolve();
|
|
166
|
+
detailsInFlight.add(id);
|
|
167
|
+
return projectApi(key, '/changes/' + encodeURIComponent(name))
|
|
168
|
+
.then((detail) => {
|
|
169
|
+
data.details.set(name, detail);
|
|
170
|
+
data.detailErrors && data.detailErrors.delete(name);
|
|
171
|
+
}, (e) => {
|
|
172
|
+
(data.detailErrors ||= new Map()).set(name, e.status ? e.message : '無法連到 hub');
|
|
173
|
+
})
|
|
174
|
+
.finally(() => {
|
|
175
|
+
detailsInFlight.delete(id);
|
|
176
|
+
emit();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function loadDecisions(key) {
|
|
181
|
+
const data = dataOf(key);
|
|
182
|
+
try {
|
|
183
|
+
data.decisions = await projectApi(key, '/decisions');
|
|
184
|
+
data.decisionsError = null;
|
|
185
|
+
} catch (e) {
|
|
186
|
+
data.decisionsError = e.status ? e.message : '無法連到 hub';
|
|
187
|
+
}
|
|
188
|
+
emit();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function loadAllProjects() {
|
|
192
|
+
return Promise.all(store.projects.map((p) => loadProject(p.key)));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Asks live sessions for fresh snapshots; their pushes arrive as `change` events. */
|
|
196
|
+
export function requestRefresh(keys) {
|
|
197
|
+
for (const key of keys) {
|
|
198
|
+
projectApi(key, '/refresh', {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: { 'content-type': 'application/json' },
|
|
201
|
+
body: '{}',
|
|
202
|
+
}).catch(() => {});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function postTask(key, file, line, done) {
|
|
207
|
+
return projectApi(key, '/task', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'content-type': 'application/json' },
|
|
210
|
+
body: JSON.stringify({ file, line, done }),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// --- sessions --------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
export function logId(key, session) {
|
|
217
|
+
return key + '/' + session;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function sessionApi(key, session, action, body) {
|
|
221
|
+
return projectApi(key, '/sessions/' + encodeURIComponent(session) + '/' + action, body === undefined ? undefined : {
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: { 'content-type': 'application/json' },
|
|
224
|
+
body: JSON.stringify(body),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Reads a session's log whole; live events append to it from then on. */
|
|
229
|
+
export async function loadSessionLog(key, session) {
|
|
230
|
+
const id = logId(key, session);
|
|
231
|
+
try {
|
|
232
|
+
const { events } = await sessionApi(key, session, 'log');
|
|
233
|
+
store.logs.set(id, { events, error: null });
|
|
234
|
+
} catch (e) {
|
|
235
|
+
store.logs.set(id, { events: (store.logs.get(id) || { events: [] }).events, error: e.status ? e.message : '無法連到 hub' });
|
|
236
|
+
}
|
|
237
|
+
emit();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function forgetSessionLog(key, session) {
|
|
241
|
+
store.logs.delete(logId(key, session));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function answerAsk(key, session, id, answer) {
|
|
245
|
+
return sessionApi(key, session, 'answer', { id, answer });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A session reported: its summary replaces the one in the project list (no
|
|
250
|
+
* refetch for a status or an action), and its new events extend the log when
|
|
251
|
+
* the page has that log open. Events arrive in order; one already held (the
|
|
252
|
+
* log was read while it was on its way) is skipped by its number.
|
|
253
|
+
*/
|
|
254
|
+
function applyActivity({ project: key, session, events }) {
|
|
255
|
+
const project = projectByKey(key);
|
|
256
|
+
if (project) {
|
|
257
|
+
const at = project.sessions.findIndex((s) => s.id === session.id);
|
|
258
|
+
if (at >= 0) project.sessions[at] = session;
|
|
259
|
+
}
|
|
260
|
+
const log = store.logs.get(logId(key, session.id));
|
|
261
|
+
if (log) {
|
|
262
|
+
const last = log.events.length ? log.events[log.events.length - 1].seq : 0;
|
|
263
|
+
for (const event of events) if (event.seq > last) log.events.push(event);
|
|
264
|
+
if (log.events.length > 200) log.events.splice(0, log.events.length - 200);
|
|
265
|
+
}
|
|
266
|
+
emit();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Live updates: `change` when a session pushes (or a task write lands),
|
|
271
|
+
* `projects` when one registers or goes on- or offline. EventSource
|
|
272
|
+
* reconnects by itself; the page only reflects the link state.
|
|
273
|
+
*/
|
|
274
|
+
export function subscribe(onProjectChange) {
|
|
275
|
+
let es = null;
|
|
276
|
+
const connect = () => {
|
|
277
|
+
es = new EventSource('/api/events');
|
|
278
|
+
es.addEventListener('change', (event) => {
|
|
279
|
+
let key = null;
|
|
280
|
+
try {
|
|
281
|
+
key = JSON.parse(event.data).project;
|
|
282
|
+
} catch {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!key) return;
|
|
286
|
+
onProjectChange(key);
|
|
287
|
+
loadProject(key);
|
|
288
|
+
});
|
|
289
|
+
es.addEventListener('projects', async () => {
|
|
290
|
+
const before = new Set(store.projects.map((p) => p.key));
|
|
291
|
+
await loadProjects();
|
|
292
|
+
await Promise.all(store.projects.filter((p) => !before.has(p.key)).map((p) => loadProject(p.key)));
|
|
293
|
+
});
|
|
294
|
+
es.addEventListener('activity', (event) => {
|
|
295
|
+
try {
|
|
296
|
+
applyActivity(JSON.parse(event.data));
|
|
297
|
+
} catch {
|
|
298
|
+
// A malformed event changes nothing; the next one carries the whole summary.
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
es.onopen = () => {
|
|
302
|
+
setConnected(true);
|
|
303
|
+
// Events sent while the stream was down are gone: read open logs afresh.
|
|
304
|
+
for (const id of store.logs.keys()) {
|
|
305
|
+
const slash = id.indexOf('/');
|
|
306
|
+
loadSessionLog(id.slice(0, slash), id.slice(slash + 1));
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
es.onerror = () => setConnected(false);
|
|
310
|
+
};
|
|
311
|
+
connect();
|
|
312
|
+
// A page parked in the back/forward cache would keep its stream open, and
|
|
313
|
+
// the browser allows only six connections per host: a few parked tabs and
|
|
314
|
+
// a fresh page's reads queue behind them. Close on the way out, reopen and
|
|
315
|
+
// catch up on the way back.
|
|
316
|
+
window.addEventListener('pagehide', () => es.close());
|
|
317
|
+
window.addEventListener('pageshow', (event) => {
|
|
318
|
+
if (!event.persisted) return;
|
|
319
|
+
connect();
|
|
320
|
+
loadProjects().then(loadAllProjects);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function notify() {
|
|
325
|
+
emit();
|
|
326
|
+
}
|