@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,357 @@
|
|
|
1
|
+
// The hub's front page (總覽): every project as one line on a shared stage
|
|
2
|
+
// grid, with the changes that need the user listed above it.
|
|
3
|
+
|
|
4
|
+
import { el, marker, paintLine, icon, lineBadge, note, activityDot } from '../lib/dom.js';
|
|
5
|
+
import {
|
|
6
|
+
STATIONS,
|
|
7
|
+
SIGNAL_LABEL,
|
|
8
|
+
ACTIVITY_LABEL,
|
|
9
|
+
UNKNOWN_STATION,
|
|
10
|
+
sessionStatus,
|
|
11
|
+
sessionDetail,
|
|
12
|
+
projectActivity,
|
|
13
|
+
stationOf,
|
|
14
|
+
stationIndex,
|
|
15
|
+
stationLabel,
|
|
16
|
+
signalOf,
|
|
17
|
+
lineSignal,
|
|
18
|
+
gapOf,
|
|
19
|
+
unreadableInfo,
|
|
20
|
+
idleDays,
|
|
21
|
+
noticeItems,
|
|
22
|
+
staleDaysOf,
|
|
23
|
+
relTime,
|
|
24
|
+
parseLooseDate,
|
|
25
|
+
} from '../lib/derive.js';
|
|
26
|
+
import { store, dataOf, slotOf, loadDetail } from '../lib/store.js';
|
|
27
|
+
import { routes } from '../lib/routes.js';
|
|
28
|
+
import { openSessionPanel } from './session.js';
|
|
29
|
+
|
|
30
|
+
/** Trains whose detail drawer is open; module-level so a live re-render keeps them open. */
|
|
31
|
+
const expanded = new Set();
|
|
32
|
+
|
|
33
|
+
export function renderHome() {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const page = el('div', { class: 'page page-home' });
|
|
36
|
+
|
|
37
|
+
if (!store.projectsLoaded) {
|
|
38
|
+
page.append(el('h1', { class: 'page-title', text: '總覽' }), note(store.hubConnected ? '正在讀取 project 清單.' : '無法連到 dashboard hub.'));
|
|
39
|
+
return page;
|
|
40
|
+
}
|
|
41
|
+
if (store.projects.length === 0) {
|
|
42
|
+
page.append(renderOnboarding());
|
|
43
|
+
return page;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const changeCount = store.projects.reduce((n, p) => n + ((dataOf(p.key).overview || { changes: [] }).changes.length), 0);
|
|
47
|
+
const sessions = store.projects.flatMap((p) => p.sessions);
|
|
48
|
+
const working = sessions.filter((s) => sessionStatus(s) === 'working').length;
|
|
49
|
+
const sub = [`${store.projects.length} 個 project`, `${sessions.length} 個 session`];
|
|
50
|
+
if (working) sub.push(`${working} 個工作中`);
|
|
51
|
+
sub.push(`${changeCount} 個進行中的 change`);
|
|
52
|
+
page.append(
|
|
53
|
+
el('header', { class: 'page-head' }, [
|
|
54
|
+
el('div', { class: 'page-head-text' }, [
|
|
55
|
+
el('h1', { class: 'page-title', text: '總覽' }),
|
|
56
|
+
el('p', { class: 'page-sub num', text: sub.join(' · ') }),
|
|
57
|
+
]),
|
|
58
|
+
legend(),
|
|
59
|
+
])
|
|
60
|
+
);
|
|
61
|
+
page.append(renderNotices(now));
|
|
62
|
+
page.append(renderMap(now));
|
|
63
|
+
return page;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function legend() {
|
|
67
|
+
const items = ['moving', 'call', 'idle', 'stop', 'parked'].map((signal) =>
|
|
68
|
+
el('li', { class: 'legend-item' }, [marker(signal, 12), el('span', { text: SIGNAL_LABEL[signal] })])
|
|
69
|
+
);
|
|
70
|
+
return el('ul', { class: 'legend', 'aria-label': '標記說明' }, items);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderOnboarding() {
|
|
74
|
+
const stations = el('ol', { class: 'onboard-stations' }, STATIONS.map((s) => el('li', {}, [el('span', { class: 'station-dot' }), el('strong', { text: s.label }), el('span', { class: 'muted', text: s.hint })])));
|
|
75
|
+
return el('section', { class: 'onboard' }, [
|
|
76
|
+
el('h1', { class: 'page-title', text: '還沒有 project' }),
|
|
77
|
+
el('p', { class: 'onboard-lead', text: '在任何一個 tospec project 裡啟動 Claude Code session. tospec init 或 tospec update 寫入 .claude/skills/ 的 tospec-dashboard mod 會把 project 註冊到這個 hub, 它的 change 就會出現在總覽上.' }),
|
|
78
|
+
stations,
|
|
79
|
+
el('p', { class: 'muted', text: '每個 change 依檔案狀態落在一個階段: artifact 寫完就進 apply, task 全部勾完就到 sync, sync 通過就能 archive.' }),
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- notices -------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
const NOTICE_SIGNAL = { stop: 'stop', call: 'call', idle: 'idle', drift: 'idle' };
|
|
86
|
+
|
|
87
|
+
function renderNotices(now) {
|
|
88
|
+
const items = noticeItems(store.projects, store.data, now);
|
|
89
|
+
const section = el('section', { class: 'notices', 'aria-labelledby': 'notices-title' });
|
|
90
|
+
if (items.length === 0) {
|
|
91
|
+
const anyLoaded = store.projects.some((p) => dataOf(p.key).overview);
|
|
92
|
+
section.classList.add('notices-clear');
|
|
93
|
+
section.append(el('h2', { id: 'notices-title', class: 'notices-title', text: anyLoaded ? '沒有需要處理的 change' : '正在讀取各 project 的快照' }));
|
|
94
|
+
return section;
|
|
95
|
+
}
|
|
96
|
+
section.append(el('h2', { id: 'notices-title', class: 'notices-title' }, ['需要處理', el('span', { class: 'count', text: String(items.length) })]));
|
|
97
|
+
const list = el('ul', { class: 'notice-list' });
|
|
98
|
+
for (const item of items) {
|
|
99
|
+
const slot = slotOf(item.project.key);
|
|
100
|
+
if (item.kind === 'wait') {
|
|
101
|
+
list.append(el('li', { class: 'notice notice-wait' }, [
|
|
102
|
+
activityDot('waiting'),
|
|
103
|
+
lineBadge(item.project.name, slot),
|
|
104
|
+
sessionButton(item.project, item.session, 'notice-subject', [el('span', { text: '等你回覆 · session ' }), el('span', { class: 'mono', text: item.session.id.slice(0, 8) })]),
|
|
105
|
+
el('span', { class: 'notice-text', text: item.text }),
|
|
106
|
+
]));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const subject = item.change
|
|
110
|
+
? el('a', { class: 'notice-subject', href: routes.change(item.project.key, item.change.name), text: item.change.name })
|
|
111
|
+
: item.ticket
|
|
112
|
+
? el('a', { class: 'notice-subject', href: routes.project(item.project.key), text: item.ticket.title || item.ticket.file })
|
|
113
|
+
: el('a', { class: 'notice-subject', href: routes.project(item.project.key), text: '整個 project' });
|
|
114
|
+
list.append(
|
|
115
|
+
el('li', { class: 'notice notice-' + item.kind }, [
|
|
116
|
+
marker(NOTICE_SIGNAL[item.kind], 14),
|
|
117
|
+
lineBadge(item.project.name, slot),
|
|
118
|
+
subject,
|
|
119
|
+
el('span', { class: 'notice-text' }, [item.text, item.hint ? el('span', { class: 'notice-hint', text: item.hint }) : null]),
|
|
120
|
+
])
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
section.append(list);
|
|
124
|
+
return section;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- the map -----------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
function renderMap(now) {
|
|
130
|
+
const counts = STATIONS.map(() => 0);
|
|
131
|
+
for (const project of store.projects) {
|
|
132
|
+
const overview = dataOf(project.key).overview;
|
|
133
|
+
if (!overview) continue;
|
|
134
|
+
for (const change of overview.changes) {
|
|
135
|
+
const i = stationIndex(stationOf(change));
|
|
136
|
+
if (i >= 0) counts[i] += 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const head = el('div', { class: 'map-head', 'aria-hidden': 'true' }, [
|
|
140
|
+
el('span', { class: 'map-head-label', text: 'project' }),
|
|
141
|
+
...STATIONS.map((s, i) =>
|
|
142
|
+
el('span', { class: 'map-head-station' }, [
|
|
143
|
+
el('strong', { text: s.label }),
|
|
144
|
+
el('span', { class: 'map-head-hint', text: s.hint }),
|
|
145
|
+
// Always present, so every station label sits on the same baseline.
|
|
146
|
+
el('span', { class: 'map-head-count' + (counts[i] ? '' : ' is-zero'), text: String(counts[i]) }),
|
|
147
|
+
])
|
|
148
|
+
),
|
|
149
|
+
]);
|
|
150
|
+
const map = el('section', { class: 'map', 'aria-label': '各 project 的 change 階段' }, [head]);
|
|
151
|
+
for (const project of store.projects) map.append(renderLine(project, now));
|
|
152
|
+
return map;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** One project's line; also used, alone, at the top of the project page. */
|
|
156
|
+
export function renderLine(project, now, { asTitle = true } = {}) {
|
|
157
|
+
const data = dataOf(project.key);
|
|
158
|
+
const slot = slotOf(project.key);
|
|
159
|
+
const overview = data.overview;
|
|
160
|
+
const staleDays = staleDaysOf(data);
|
|
161
|
+
const row = paintLine(el('article', { class: 'line-row', 'data-key': project.key }), slot);
|
|
162
|
+
if (!project.online) row.classList.add('is-offline');
|
|
163
|
+
|
|
164
|
+
const trainsAt = STATIONS.map(() => []);
|
|
165
|
+
const depot = [];
|
|
166
|
+
if (overview) {
|
|
167
|
+
for (const change of overview.changes) {
|
|
168
|
+
const i = stationIndex(stationOf(change));
|
|
169
|
+
if (i >= 0) trainsAt[i].push(change);
|
|
170
|
+
else depot.push(change);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const running = project.online && overview && overview.changes.some((c) => signalOf(c, staleDays, now) === 'running');
|
|
174
|
+
if (running) row.classList.add('has-running');
|
|
175
|
+
// A session at work right now: the whole line flows, visible from across the map.
|
|
176
|
+
if (projectActivity(project) === 'working') row.classList.add('is-working');
|
|
177
|
+
|
|
178
|
+
const label = el('header', { class: 'line-label' }, [
|
|
179
|
+
asTitle
|
|
180
|
+
? el('h2', { class: 'line-title' }, [el('a', { href: routes.project(project.key) }, [el('span', { class: 'line-swatch', 'aria-hidden': 'true' }), el('span', { text: project.name })])])
|
|
181
|
+
: null,
|
|
182
|
+
// The project page states the status and lists its sessions in its own sections.
|
|
183
|
+
asTitle ? el('p', { class: 'line-status' + (project.online ? '' : ' is-offline') }, statusText(project, now)) : null,
|
|
184
|
+
asTitle ? sessionList(project, now) : null,
|
|
185
|
+
terminalText(data, now),
|
|
186
|
+
]);
|
|
187
|
+
// Before the line starts: changes whose stage the files cannot give.
|
|
188
|
+
if (depot.length) {
|
|
189
|
+
label.append(el('div', { class: 'depot' }, [
|
|
190
|
+
el('p', { class: 'depot-title', text: '無法判斷階段' }),
|
|
191
|
+
el('ul', { class: 'trains' }, depot.map((change) => renderTrain(project, change, data, staleDays, now))),
|
|
192
|
+
]));
|
|
193
|
+
}
|
|
194
|
+
row.append(label);
|
|
195
|
+
|
|
196
|
+
const stations = el('ol', { class: 'stations' }, [
|
|
197
|
+
el('span', { class: 'track', 'aria-hidden': 'true' }),
|
|
198
|
+
el('span', { class: 'run-segment', 'aria-hidden': 'true' }),
|
|
199
|
+
]);
|
|
200
|
+
STATIONS.forEach((station, i) => {
|
|
201
|
+
const trains = trainsAt[i];
|
|
202
|
+
const li = el('li', { class: 'station' + (trains.length ? ' is-occupied' : ''), 'data-station': station.id }, [
|
|
203
|
+
el('span', { class: 'station-dot', 'aria-hidden': 'true' }),
|
|
204
|
+
el('span', { class: 'sr-only', text: `${station.label}: ${trains.length} 個 change` }),
|
|
205
|
+
]);
|
|
206
|
+
if (trains.length) {
|
|
207
|
+
li.append(el('ul', { class: 'trains' }, trains.map((change) => renderTrain(project, change, data, staleDays, now))));
|
|
208
|
+
}
|
|
209
|
+
stations.append(li);
|
|
210
|
+
});
|
|
211
|
+
row.append(stations);
|
|
212
|
+
|
|
213
|
+
if (data.error) {
|
|
214
|
+
row.append(el('p', { class: 'line-note line-error' }, [marker('stop', 12), el('span', { text: '快照讀取失敗: ' + data.error })]));
|
|
215
|
+
} else if (!overview) {
|
|
216
|
+
row.append(el('p', { class: 'line-note', text: '正在讀取快照.' }));
|
|
217
|
+
} else if (overview.changes.length === 0) {
|
|
218
|
+
row.append(el('p', { class: 'line-note', text: '沒有進行中的 change. 用 tospec new change <name> 建立新的 change.' }));
|
|
219
|
+
}
|
|
220
|
+
return row;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function statusText(project, now) {
|
|
224
|
+
const snapshot = relTime(project.updatedAt, now);
|
|
225
|
+
if (!project.online) return [activityDot('offline'), el('span', { class: 'status-word', text: '離線' }), ` · 最後快照 ${snapshot}`];
|
|
226
|
+
const n = project.sessions.length;
|
|
227
|
+
return [activityDot(projectActivity(project)), `${n} 個 session · 快照 ${snapshot}`];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** A button that opens a session's panel: its log, its question, a prompt box. */
|
|
231
|
+
export function sessionButton(project, session, cls, children) {
|
|
232
|
+
const button = el('button', { type: 'button', class: cls, title: `開啟 session ${session.id} 的 log` }, children);
|
|
233
|
+
button.addEventListener('click', () => openSessionPanel(project.key, session.id));
|
|
234
|
+
return button;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Each live session as one line: its state and what it is doing, one click from its log. */
|
|
238
|
+
function sessionList(project, now) {
|
|
239
|
+
if (!project.sessions.length) return null;
|
|
240
|
+
return el('ul', { class: 'line-sessions' }, project.sessions.map((session) => {
|
|
241
|
+
const status = sessionStatus(session);
|
|
242
|
+
const detail = sessionDetail(session, now);
|
|
243
|
+
return el('li', {}, [
|
|
244
|
+
sessionButton(project, session, 'session-chip is-' + status, [
|
|
245
|
+
activityDot(status),
|
|
246
|
+
el('span', { class: 'session-chip-state', text: ACTIVITY_LABEL[status] }),
|
|
247
|
+
detail ? el('span', { class: 'session-chip-detail', text: detail }) : null,
|
|
248
|
+
]),
|
|
249
|
+
]);
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function terminalText(data, now) {
|
|
254
|
+
const archived = data.archive && data.archive.changes;
|
|
255
|
+
if (!archived || archived.length === 0) return null;
|
|
256
|
+
const latest = archived[0];
|
|
257
|
+
const when = parseLooseDate(latest.stamp);
|
|
258
|
+
return el('p', { class: 'line-terminal' }, [`已歸檔 ${archived.length}`, when ? ` · 最近 ${relTime(when, now)}` : '']);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function renderTrain(project, change, data, staleDays, now) {
|
|
262
|
+
const signal = lineSignal(signalOf(change, staleDays, now), project.online);
|
|
263
|
+
const station = stationOf(change);
|
|
264
|
+
const detail = data.details.get(change.name);
|
|
265
|
+
const id = project.key + '/' + change.name;
|
|
266
|
+
const drawerId = 'train-' + project.key + '-' + change.name.replace(/[^a-z0-9_-]/gi, '_');
|
|
267
|
+
const open = expanded.has(id);
|
|
268
|
+
|
|
269
|
+
const toggle = el('button', {
|
|
270
|
+
type: 'button',
|
|
271
|
+
class: 'train-toggle',
|
|
272
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
273
|
+
'aria-controls': drawerId,
|
|
274
|
+
title: open ? '收合細節' : '展開細節',
|
|
275
|
+
}, [icon('chevron', 14), el('span', { class: 'sr-only', text: '細節' })]);
|
|
276
|
+
|
|
277
|
+
const idle = signal === 'idle' ? ` · 閒置 ${idleDays(change, now)} 天` : '';
|
|
278
|
+
const gap = el('p', { class: 'train-gap' }, [el('span', { class: 'sr-only', text: SIGNAL_LABEL[signal] + ': ' })]);
|
|
279
|
+
if (signal === 'stop') {
|
|
280
|
+
// The parser's reason and the fix ride in the notice list and the drawer;
|
|
281
|
+
// the row keeps to the file and line, each kept whole.
|
|
282
|
+
const summary = unreadableInfo(change).summary;
|
|
283
|
+
const space = summary.indexOf(' ');
|
|
284
|
+
gap.append(
|
|
285
|
+
el('span', { class: 'badge-unreadable', text: '無法解析' }),
|
|
286
|
+
' ',
|
|
287
|
+
el('span', { class: 'keep', text: summary.slice(0, space) }),
|
|
288
|
+
' ',
|
|
289
|
+
el('span', { class: 'keep', text: summary.slice(space + 1) })
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
292
|
+
gap.append(gapOf(change, detail) + idle);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const drawer = el('div', { class: 'train-drawer', id: drawerId }, drawerContent(change, detail, now));
|
|
296
|
+
drawer.hidden = !open;
|
|
297
|
+
|
|
298
|
+
const item = el('li', { class: 'train signal-' + signal, 'data-train': id, 'data-station': station }, [
|
|
299
|
+
el('div', { class: 'train-head' }, [
|
|
300
|
+
marker(signal),
|
|
301
|
+
el('a', { class: 'train-name', href: routes.change(project.key, change.name), text: change.name }),
|
|
302
|
+
toggle,
|
|
303
|
+
]),
|
|
304
|
+
miniRoute(station),
|
|
305
|
+
gap,
|
|
306
|
+
drawer,
|
|
307
|
+
]);
|
|
308
|
+
toggle.addEventListener('click', () => {
|
|
309
|
+
const next = drawer.hidden;
|
|
310
|
+
drawer.hidden = !next;
|
|
311
|
+
toggle.setAttribute('aria-expanded', String(next));
|
|
312
|
+
toggle.title = next ? '收合細節' : '展開細節';
|
|
313
|
+
if (next) expanded.add(id);
|
|
314
|
+
else expanded.delete(id);
|
|
315
|
+
// Only planning trains preload their detail; the drawer fills in when it lands.
|
|
316
|
+
if (next && !detail) loadDetail(project.key, change.name);
|
|
317
|
+
});
|
|
318
|
+
return item;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Narrow layouts lose the shared station grid; each train carries its own four-stop strip. */
|
|
322
|
+
function miniRoute(station) {
|
|
323
|
+
const at = stationIndex(station);
|
|
324
|
+
return el('span', { class: 'mini-route' + (station === UNKNOWN_STATION ? ' is-unknown' : ''), 'aria-hidden': 'true' }, [
|
|
325
|
+
...STATIONS.map((s, i) => el('span', { class: 'mini-stop' + (i === at ? ' is-here' : at >= 0 && i < at ? ' is-passed' : '') })),
|
|
326
|
+
el('span', { class: 'mini-label', text: stationLabel(station) }),
|
|
327
|
+
]);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function drawerContent(change, detail, now) {
|
|
331
|
+
const rows = [];
|
|
332
|
+
if (detail && detail.artifacts.length) {
|
|
333
|
+
rows.push(
|
|
334
|
+
el('ul', { class: 'artifact-chips', 'aria-label': 'artifact' }, detail.artifacts.map((a) =>
|
|
335
|
+
el('li', { class: 'artifact-chip status-' + a.status, title: a.missingDeps ? '等 ' + a.missingDeps.join(', ') : a.status }, [
|
|
336
|
+
el('span', { class: 'chip-dot', 'aria-hidden': 'true' }),
|
|
337
|
+
el('span', { text: a.id }),
|
|
338
|
+
el('span', { class: 'sr-only', text: ' ' + a.status }),
|
|
339
|
+
])
|
|
340
|
+
))
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const { completed, total } = change.taskProgress;
|
|
344
|
+
if (total > 0) {
|
|
345
|
+
const meter = el('span', { class: 'meter', 'aria-hidden': 'true' }, [el('span', { class: 'meter-fill' })]);
|
|
346
|
+
meter.firstChild.style.setProperty('width', Math.round((completed / total) * 100) + '%');
|
|
347
|
+
rows.push(el('p', { class: 'drawer-line' }, [el('span', { class: 'drawer-key', text: 'task' }), meter, el('span', { class: 'num', text: `${completed}/${total}` })]));
|
|
348
|
+
}
|
|
349
|
+
rows.push(el('p', { class: 'drawer-line' }, [el('span', { class: 'drawer-key', text: '更新' }), relTime(change.lastModified, now)]));
|
|
350
|
+
if (change.goal) rows.push(el('p', { class: 'drawer-goal', text: change.goal }));
|
|
351
|
+
if (change.unreadable) {
|
|
352
|
+
const info = unreadableInfo(change);
|
|
353
|
+
rows.push(el('p', { class: 'train-reason', text: info.reason }));
|
|
354
|
+
rows.push(el('p', { class: 'drawer-goal' }, ['修正 ', el('code', { text: info.file }), ' 後, 下一次快照就會恢復.']));
|
|
355
|
+
}
|
|
356
|
+
return rows;
|
|
357
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// One project: its line at full width, its live sessions, the four health
|
|
2
|
+
// charts, and the lists the overview leaves out (specs, tickets, archive).
|
|
3
|
+
|
|
4
|
+
import { el, note, paintLine, icon, activityDot } from '../lib/dom.js';
|
|
5
|
+
import { STATIONS, staleDaysOf, relTime, formatStamp, sessionStatus, sessionDetail, ACTIVITY_LABEL } from '../lib/derive.js';
|
|
6
|
+
import { store, dataOf, slotOf, projectByKey, loadDecisions } from '../lib/store.js';
|
|
7
|
+
import { transitChart, archiveRateChart, decisionTimeline, specSizeChart } from '../lib/charts.js';
|
|
8
|
+
import { routes } from '../lib/routes.js';
|
|
9
|
+
import { renderLine, sessionButton } from './home.js';
|
|
10
|
+
|
|
11
|
+
export function projectHeader(project, active) {
|
|
12
|
+
const data = dataOf(project.key);
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const decisionCount = data.decisions ? data.decisions.length : null;
|
|
15
|
+
const head = paintLine(el('header', { class: 'project-head' }), slotOf(project.key));
|
|
16
|
+
head.append(
|
|
17
|
+
el('h1', { class: 'page-title project-title' }, [el('span', { class: 'line-swatch', 'aria-hidden': 'true' }), el('span', { text: project.name })]),
|
|
18
|
+
el('p', { class: 'project-root mono', text: project.root, title: project.root }),
|
|
19
|
+
el('p', { class: 'project-status' + (project.online ? '' : ' is-offline') }, project.online
|
|
20
|
+
? [el('span', { class: 'status-dot', 'aria-hidden': 'true' }), `${project.sessions.length} 個 session 連線中 · 快照 ${relTime(project.updatedAt, now)}`]
|
|
21
|
+
: [el('span', { class: 'status-word', text: '離線' }), ` · 最後快照 ${relTime(project.updatedAt, now)}. 沒有 session 時只能讀快照; 文件內容與勾 task 要等 session 回來.`]),
|
|
22
|
+
el('nav', { class: 'subnav', 'aria-label': 'project' }, [
|
|
23
|
+
el('a', { href: routes.project(project.key), class: 'subnav-link', 'aria-current': active === 'overview' ? 'page' : null, text: '總覽' }),
|
|
24
|
+
el('a', { href: routes.decisions(project.key), class: 'subnav-link', 'aria-current': active === 'decisions' ? 'page' : null }, ['Decisions', decisionCount != null ? el('span', { class: 'count', text: String(decisionCount) }) : null]),
|
|
25
|
+
])
|
|
26
|
+
);
|
|
27
|
+
return head;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function renderProject(route) {
|
|
31
|
+
const project = projectByKey(route.key);
|
|
32
|
+
if (!project) return unknownProject(route.key);
|
|
33
|
+
const data = dataOf(project.key);
|
|
34
|
+
if (!data.decisions && !data.decisionsError) loadDecisions(project.key);
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const page = paintLine(el('div', { class: 'page page-project' }), slotOf(project.key));
|
|
37
|
+
page.append(projectHeader(project, 'overview'));
|
|
38
|
+
|
|
39
|
+
const map = el('section', { class: 'map map-single', 'aria-label': project.name + ' 的 change 階段' }, [
|
|
40
|
+
el('div', { class: 'map-head', 'aria-hidden': 'true' }, [el('span', { class: 'map-head-label' }), ...STATIONS.map((s) => el('span', { class: 'map-head-station' }, [el('strong', { text: s.label }), el('span', { class: 'map-head-hint', text: s.hint })]))]),
|
|
41
|
+
renderLine(project, now, { asTitle: false }),
|
|
42
|
+
]);
|
|
43
|
+
page.append(map);
|
|
44
|
+
page.append(sessionsSection(project, now));
|
|
45
|
+
|
|
46
|
+
const overview = data.overview;
|
|
47
|
+
if (!overview) {
|
|
48
|
+
page.append(note(data.error ? '快照讀取失敗: ' + data.error : '正在讀取快照.'));
|
|
49
|
+
return page;
|
|
50
|
+
}
|
|
51
|
+
if (data.activity && data.activity.available && data.activity.specDrift) {
|
|
52
|
+
page.append(el('p', { class: 'drift-note', text: '近 7 天程式碼有變動, tospec/ 沒有. 實作可能已經走在 spec 前面.' }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const charts = el('section', { class: 'charts', 'aria-label': '圖表' }, [
|
|
56
|
+
transitChart(overview.changes, staleDaysOf(data), now),
|
|
57
|
+
archiveRateChart((data.archive && data.archive.changes) || [], now),
|
|
58
|
+
data.decisions
|
|
59
|
+
? decisionTimeline(project.key, data.decisions, now)
|
|
60
|
+
: el('figure', { class: 'chart chart-empty' }, [el('figcaption', { class: 'chart-caption' }, [el('h3', { class: 'chart-title', text: 'ADR 時間軸' })]), note(data.decisionsError ? '決策讀取失敗: ' + data.decisionsError : '正在讀取決策.')]),
|
|
61
|
+
specSizeChart(project.key, overview.specs),
|
|
62
|
+
]);
|
|
63
|
+
page.append(charts);
|
|
64
|
+
|
|
65
|
+
page.append(
|
|
66
|
+
el('div', { class: 'project-lists' }, [
|
|
67
|
+
specsList(project.key, overview.specs),
|
|
68
|
+
ticketsList(project.key, overview.tickets),
|
|
69
|
+
archiveList(data.archive, now),
|
|
70
|
+
])
|
|
71
|
+
);
|
|
72
|
+
return page;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function unknownProject(key) {
|
|
76
|
+
return el('div', { class: 'page' }, [
|
|
77
|
+
el('h1', { class: 'page-title', text: '找不到這個 project' }),
|
|
78
|
+
note(store.projectsLoaded ? `hub 上沒有 key 為 ${key} 的 project. 它的 session 可能還沒向重新啟動的 hub 註冊.` : '正在讀取 project 清單.'),
|
|
79
|
+
el('p', {}, [el('a', { href: routes.home(), text: '回到總覽' })]),
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function listSection(title, count, body) {
|
|
84
|
+
return el('section', { class: 'list-section' }, [
|
|
85
|
+
el('h2', { class: 'section-title' }, [title, count != null ? el('span', { class: 'count', text: String(count) }) : null]),
|
|
86
|
+
body,
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function specsList(key, specs) {
|
|
91
|
+
if (!specs.length) return listSection('Specs', 0, note('還沒有 spec.'));
|
|
92
|
+
return listSection('Specs', specs.length, el('ul', { class: 'plain-list' }, specs.map((s) =>
|
|
93
|
+
el('li', { class: 'row' }, [el('a', { class: 'row-name', href: routes.spec(key, s.id), text: s.id }), el('span', { class: 'row-meta num', text: `${s.requirementCount} 條 requirement` })])
|
|
94
|
+
)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ticketsList(key, tickets) {
|
|
98
|
+
if (!tickets.length) return listSection('Tickets', 0, note('沒有進行中的 ticket.'));
|
|
99
|
+
return listSection('Tickets', tickets.length, el('ul', { class: 'plain-list' }, tickets.map((t) =>
|
|
100
|
+
el('li', { class: 'row ticket-row' + (t.parseError ? ' row-error' : '') }, [
|
|
101
|
+
el('span', { class: 'row-time mono', text: formatStamp(t.date) }),
|
|
102
|
+
el('span', { class: 'row-name', text: t.title }),
|
|
103
|
+
t.type ? el('span', { class: 'tag', text: t.type }) : null,
|
|
104
|
+
t.change ? el('a', { class: 'ticket-change', href: routes.change(key, t.change), text: t.change }) : null,
|
|
105
|
+
t.parseError
|
|
106
|
+
? el('span', { class: 'badge-unreadable', text: 'frontmatter 無法解析' })
|
|
107
|
+
: t.summary ? el('span', { class: 'ticket-summary', text: t.summary }) : null,
|
|
108
|
+
])
|
|
109
|
+
)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Every live session by its full id, so the person can tell which terminal is
|
|
114
|
+
* which (Claude Code shows the id in `/status`), with its state beside it.
|
|
115
|
+
*/
|
|
116
|
+
function sessionsSection(project, now) {
|
|
117
|
+
const section = el('section', { class: 'sessions-board', 'aria-labelledby': 'sessions-title' }, [
|
|
118
|
+
el('h2', { id: 'sessions-title', class: 'section-title' }, ['Sessions', el('span', { class: 'count', text: String(project.sessions.length) })]),
|
|
119
|
+
]);
|
|
120
|
+
if (!project.sessions.length) {
|
|
121
|
+
section.append(note('沒有 Claude Code session 連線, 畫面是最後一次快照.'));
|
|
122
|
+
return section;
|
|
123
|
+
}
|
|
124
|
+
section.append(el('ul', { class: 'plain-list session-list' }, project.sessions.map((s) => sessionRow(project, s, now))));
|
|
125
|
+
return section;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function sessionRow(project, session, now) {
|
|
129
|
+
const status = sessionStatus(session);
|
|
130
|
+
const copy = el('button', { type: 'button', class: 'icon-button', title: '複製 session id' }, [icon('copy', 14), el('span', { class: 'sr-only', text: '複製 session id' })]);
|
|
131
|
+
copy.addEventListener('click', async () => {
|
|
132
|
+
try {
|
|
133
|
+
await navigator.clipboard.writeText(session.id);
|
|
134
|
+
copy.title = '已複製';
|
|
135
|
+
} catch {
|
|
136
|
+
copy.title = '無法複製, 請手動選取';
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
const detail = sessionDetail(session, now);
|
|
140
|
+
return el('li', { class: 'session-row is-' + status }, [
|
|
141
|
+
el('span', { class: 'session-state' }, [activityDot(status), el('span', { text: ACTIVITY_LABEL[status] })]),
|
|
142
|
+
el('span', { class: 'session-main' }, [
|
|
143
|
+
el('span', { class: 'session-id mono', text: session.id }),
|
|
144
|
+
detail ? el('span', { class: 'session-detail', text: detail }) : null,
|
|
145
|
+
]),
|
|
146
|
+
// Always a cell, so every row's time and buttons line up in their columns.
|
|
147
|
+
session.id === project.updatedBy ? el('span', { class: 'tag tag-strong', text: '目前快照' }) : el('span'),
|
|
148
|
+
el('span', { class: 'row-time', text: '連線於 ' + relTime(session.connectedAt, now) }),
|
|
149
|
+
el('span', { class: 'session-actions' }, [
|
|
150
|
+
copy,
|
|
151
|
+
sessionButton(project, session, 'button button-small', [icon('log', 14), el('span', { text: status === 'waiting' ? '回覆' : 'Log' })]),
|
|
152
|
+
]),
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function archiveList(archive, now) {
|
|
157
|
+
const changes = (archive && archive.changes) || [];
|
|
158
|
+
if (!changes.length) return listSection('Archive', 0, note('還沒有歸檔的 change.'));
|
|
159
|
+
const LIMIT = 12;
|
|
160
|
+
const list = el('ul', { class: 'plain-list' }, changes.slice(0, LIMIT).map((c) =>
|
|
161
|
+
el('li', { class: 'row archive-row' }, [
|
|
162
|
+
el('span', { class: 'row-name', text: c.name || c.dirName }),
|
|
163
|
+
el('span', { class: 'row-time mono', text: formatStamp(c.stamp) }),
|
|
164
|
+
])
|
|
165
|
+
));
|
|
166
|
+
const more = changes.length > LIMIT ? note(`另有 ${changes.length - LIMIT} 個較早的歸檔, 在 tospec/changes/archive/.`) : null;
|
|
167
|
+
return listSection('Archive', changes.length, el('div', {}, [list, more]));
|
|
168
|
+
}
|