sprinty-mcp 0.1.0 → 0.1.2
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/README.md +113 -24
- package/clients/claude/.claude-plugin/plugin.json +13 -0
- package/clients/codex/README.md +42 -0
- package/clients/gemini/GEMINI.md +36 -0
- package/clients/gemini/gemini-extension.json +9 -0
- package/dist/dashboard/page.d.ts +1 -1
- package/dist/dashboard/page.js +203 -13
- package/dist/dashboard/page.js.map +1 -1
- package/dist/dashboard/server.js +59 -1
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/ui/model.d.ts +84 -0
- package/dist/dashboard/ui/model.js +109 -0
- package/dist/dashboard/ui/model.js.map +1 -0
- package/dist/dashboard/ui/src/main.d.ts +1 -0
- package/dist/dashboard/ui/src/main.js +6 -0
- package/dist/dashboard/ui/src/main.js.map +1 -0
- package/dist/dashboard-ui/assets/index-BsA3aPfs.css +1 -0
- package/dist/dashboard-ui/assets/index-C2yPOgFO.js +66 -0
- package/dist/dashboard-ui/index.html +13 -0
- package/dist/domain/change-map.d.ts +299 -0
- package/dist/domain/change-map.js +138 -0
- package/dist/domain/change-map.js.map +1 -0
- package/dist/domain/changelog.d.ts +2 -0
- package/dist/domain/changelog.js +54 -0
- package/dist/domain/changelog.js.map +1 -0
- package/dist/domain/coverage.d.ts +105 -0
- package/dist/domain/coverage.js +66 -0
- package/dist/domain/coverage.js.map +1 -0
- package/dist/domain/enums.d.ts +6 -4
- package/dist/domain/enums.js +8 -4
- package/dist/domain/enums.js.map +1 -1
- package/dist/domain/events.d.ts +1263 -0
- package/dist/domain/events.js +68 -2
- package/dist/domain/events.js.map +1 -1
- package/dist/domain/gates.d.ts +9 -0
- package/dist/domain/gates.js +3 -0
- package/dist/domain/gates.js.map +1 -1
- package/dist/domain/graph.d.ts +25 -0
- package/dist/domain/graph.js +89 -0
- package/dist/domain/graph.js.map +1 -0
- package/dist/domain/projection.d.ts +65 -1
- package/dist/domain/projection.js +182 -10
- package/dist/domain/projection.js.map +1 -1
- package/dist/domain/search.js +8 -0
- package/dist/domain/search.js.map +1 -1
- package/dist/gates/run.d.ts +1 -1
- package/dist/gates/run.js +10 -5
- package/dist/gates/run.js.map +1 -1
- package/dist/git/git.d.ts +6 -0
- package/dist/git/git.js +14 -0
- package/dist/git/git.js.map +1 -1
- package/dist/ledger/book.d.ts +2 -2
- package/dist/ledger/book.js +2 -2
- package/dist/ledger/book.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.js +112 -10
- package/dist/server.js.map +1 -1
- package/dist/store/store.d.ts +81 -4
- package/dist/store/store.js +304 -21
- package/dist/store/store.js.map +1 -1
- package/dist/tools/current.d.ts +6 -1
- package/dist/tools/current.js +42 -3
- package/dist/tools/current.js.map +1 -1
- package/dist/tools/register.d.ts +6 -1
- package/dist/tools/register.js +28 -14
- package/dist/tools/register.js.map +1 -1
- package/dist/tools/schemas.d.ts +281 -1
- package/dist/tools/schemas.js +54 -2
- package/dist/tools/schemas.js.map +1 -1
- package/package.json +49 -11
- package/skills/how-to-run-a-sprint/SKILL.md +38 -13
- package/skills/using-sprinty/SKILL.md +53 -14
package/README.md
CHANGED
|
@@ -3,29 +3,43 @@
|
|
|
3
3
|
A disciplined-sprint MCP server for AI coding agents — **Claude Code, Codex, and Gemini**.
|
|
4
4
|
|
|
5
5
|
Sprinty gives an agent first-class tools to run a sprint with structure that can't silently rot:
|
|
6
|
-
structured **sprint → subsprint → item** objects,
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
structured **sprint → subsprint → item** objects, dependency graphs with topological ordering and
|
|
7
|
+
cycle detection, an **immutable append-only ledger** anchored to real git commits,
|
|
8
|
+
Git-backed **change maps**, durable **artifacts**, bug-backed **follow-ups**, feature-sized
|
|
9
|
+
**spikes**, Markdown changelogs with file tables, **programmatic close-gates**
|
|
10
|
+
that re-run your tests and require coverage evidence before a sprint can close, a **regex search**
|
|
11
|
+
over the record, and a **live follow-along dashboard**.
|
|
9
12
|
|
|
10
13
|
The point: the agent doesn't drift, and the record doesn't lie. IDs are minted server-side, items
|
|
11
|
-
can't exist without gates, `done` rejects a commit that doesn't exist
|
|
12
|
-
to close while anything is
|
|
14
|
+
can't exist without gates, `done` rejects a commit that doesn't exist or lacks a semver changelog
|
|
15
|
+
line, and `sprint_close` refuses to close while anything is open, coverage is missing, or a gate
|
|
16
|
+
fails.
|
|
13
17
|
|
|
14
18
|
## Install
|
|
15
19
|
|
|
16
|
-
Sprinty
|
|
17
|
-
(the npm package is `sprinty-mcp`; the server, tools, and plugins are all named `sprinty`).
|
|
20
|
+
Sprinty has two layers:
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
- the MCP server, which runs from npm as `npx -y sprinty-mcp`;
|
|
23
|
+
- optional client manifests under `clients/` for agents that support plugins or extensions.
|
|
24
|
+
|
|
25
|
+
The npm package is `sprinty-mcp`. The server, MCP tool namespace, and client manifests are named
|
|
26
|
+
`sprinty`. The tarball ships the canonical top-level `skills/` directory; client directories in a
|
|
27
|
+
Git checkout may symlink to it, but package consumers should treat the top-level `skills/` directory
|
|
28
|
+
as authoritative.
|
|
29
|
+
|
|
30
|
+
### Claude Code
|
|
31
|
+
|
|
32
|
+
Add the MCP server directly:
|
|
21
33
|
|
|
22
34
|
```bash
|
|
23
35
|
claude mcp add sprinty -- npx -y sprinty-mcp
|
|
24
36
|
```
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
The repo also includes a Claude plugin manifest in `clients/claude/`.
|
|
39
|
+
|
|
40
|
+
### Codex
|
|
41
|
+
|
|
42
|
+
Fast path: add the MCP server to `~/.codex/config.toml`:
|
|
29
43
|
|
|
30
44
|
```toml
|
|
31
45
|
[mcp_servers.sprinty]
|
|
@@ -33,34 +47,109 @@ command = "npx"
|
|
|
33
47
|
args = ["-y", "sprinty-mcp"]
|
|
34
48
|
```
|
|
35
49
|
|
|
36
|
-
|
|
50
|
+
Sprinty does not guess from the MCP server process cwd. Start a sprint with explicit paths:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"goal": "Build the catalogue MCP",
|
|
55
|
+
"git_dir": "/absolute/path/to/your/repo",
|
|
56
|
+
"data_dir": "/absolute/path/to/your/repo/.sprinty",
|
|
57
|
+
"context_notes": ["optional context"]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`git_dir` is where commits, gates, coverage, and change maps run. `data_dir` is where Sprinty stores
|
|
62
|
+
the `current` pointer and append-only JSONL ledgers. For read-only tools before `sprint_new`, you may
|
|
63
|
+
pre-bind the MCP server with `SPRINTY_GIT_DIR` and `SPRINTY_DATA_DIR` or `--git-dir` and
|
|
64
|
+
`--data-dir`; both are required together. `SPRINTY_REPO_DIR` and `SPRINTY_WORKTREE` remain accepted
|
|
65
|
+
as legacy aliases for `git_dir` only when a `data_dir` is also supplied.
|
|
66
|
+
|
|
67
|
+
Codex CLI plugin path: install the repo-local marketplace from a repository checkout:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
codex plugin marketplace add .
|
|
71
|
+
codex plugin add sprinty@sprinty-local
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The installable plugin bundle is `plugins/sprinty/`, and the marketplace index is
|
|
75
|
+
`.agents/plugins/marketplace.json`. This marketplace layout is for Git/repo installs, not the npm
|
|
76
|
+
tarball. The plugin uses a `plugins/sprinty/skills` symlink to the canonical top-level `skills/`
|
|
77
|
+
directory; there is no duplicated Codex-only skill copy.
|
|
78
|
+
|
|
79
|
+
After installation, ask Codex to use Sprinty for a non-trivial task. It should call `sprint_new`
|
|
80
|
+
before implementation and `sprint_close` before claiming the sprint is done.
|
|
81
|
+
|
|
82
|
+
### Gemini CLI
|
|
83
|
+
|
|
84
|
+
`clients/gemini/` is a Gemini extension (`gemini-extension.json` + `GEMINI.md` + skills):
|
|
37
85
|
|
|
38
86
|
```bash
|
|
39
87
|
gemini extensions install ./clients/gemini
|
|
40
88
|
```
|
|
41
89
|
|
|
42
|
-
The
|
|
43
|
-
|
|
90
|
+
The skill guidance is authored once in `skills/`. Client packages should reference or symlink that
|
|
91
|
+
content rather than copying it.
|
|
44
92
|
|
|
45
93
|
## The loop
|
|
46
94
|
|
|
47
95
|
```
|
|
48
|
-
sprint_new(goal)
|
|
49
|
-
->
|
|
50
|
-
->
|
|
51
|
-
->
|
|
52
|
-
->
|
|
96
|
+
sprint_new(goal, git_dir, data_dir, context_notes?)
|
|
97
|
+
-> dashboard()
|
|
98
|
+
-> subsprint_new(description, goals[], gates[], dependencies?)
|
|
99
|
+
-> spike(description, goals[], gates[], dependencies?)
|
|
100
|
+
-> add(subsprint, description, code_locations[], gates[], dependencies?)
|
|
101
|
+
-> artifact_add/list/amend/deprecate(...)
|
|
102
|
+
-> follow_up(target, description, bug_id|bug_ids)
|
|
103
|
+
-> dependencies(target, dependencies[])
|
|
104
|
+
-> done(commit_id, gate_results[], changelog) | split(...) | deprecate(reason)
|
|
105
|
+
-> spike_conclude(subsprint, conclusion) | spike_deprecate(subsprint, reason)
|
|
106
|
+
-> changelog()
|
|
107
|
+
-> sprint_close(coverage: { path, format: "lcov", command? })
|
|
53
108
|
```
|
|
54
109
|
|
|
55
110
|
Full tool reference: [`skills/using-sprinty/SKILL.md`](skills/using-sprinty/SKILL.md).
|
|
56
111
|
How to run a sprint: [`skills/how-to-run-a-sprint/SKILL.md`](skills/how-to-run-a-sprint/SKILL.md).
|
|
57
112
|
|
|
113
|
+
## Watching the Dashboard
|
|
114
|
+
|
|
115
|
+
The dashboard is for the human sitting next to the agent.
|
|
116
|
+
|
|
117
|
+
1. Ask the agent to call `dashboard()`.
|
|
118
|
+
2. Open the returned `http://127.0.0.1:<port>` URL in a browser.
|
|
119
|
+
3. Leave it open while the sprint runs; it refreshes every two seconds.
|
|
120
|
+
|
|
121
|
+
Agents should call `dashboard()` right after `sprint_new()` and show the URL to the human. The
|
|
122
|
+
dashboard shows the sprint goal, explicit git/data paths, branch/worktree, artifact shelf, sprint progress, item status
|
|
123
|
+
distribution, code churn, subsprint progress, open items, gate evidence, dependency graph state,
|
|
124
|
+
commit ids and changelog lines for completed items, changed-file hotspots, and a paginated ledger
|
|
125
|
+
from the immutable timeline.
|
|
126
|
+
|
|
127
|
+
The dashboard server binds to `127.0.0.1` on an ephemeral port and is read-only. It lives only for
|
|
128
|
+
the running MCP server process.
|
|
129
|
+
|
|
130
|
+
## Proof Model
|
|
131
|
+
|
|
132
|
+
Sprinty records timestamps on every event and projects them into the sprint timeline. Completed
|
|
133
|
+
items require a real git commit id and controlled changelog entry when `done()` is called, and
|
|
134
|
+
`sprint_close()` checks that the commit still resolves before closing. `done()` also records a
|
|
135
|
+
Git-backed change map for the commit: file, language, directory, additions, deletions, net change,
|
|
136
|
+
churn, item ids, and commit ids. `changelog()` renders a Markdown release note with semver sections,
|
|
137
|
+
coverage, and change-map tables. `done()` also requires passing evidence for every declared item
|
|
138
|
+
gate, including manual gates. Dependencies are stored as a real graph: `current()` returns nodes,
|
|
139
|
+
edges, adjacency indexes, topological order, and cycle information; writes reject cycles. At close,
|
|
140
|
+
executable gates are re-run by Sprinty and `sprint_close()` requires an LCOV coverage report path.
|
|
141
|
+
Artifacts are append-only too: amendments and deprecations are separate ledger events, never
|
|
142
|
+
in-place edits or deletes. Follow-ups require bug ids. Spikes are subsprints with a `spike` flag:
|
|
143
|
+
they can have normal items, but must be concluded or deprecated with a reason, and spike work is
|
|
144
|
+
kept out of release changelog output.
|
|
145
|
+
|
|
58
146
|
## Storage
|
|
59
147
|
|
|
60
|
-
One append-only JSONL ledger file per sprint under
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
it gitignored
|
|
148
|
+
One append-only JSONL ledger file per sprint under the explicit `data_dir`, with a `current` pointer
|
|
149
|
+
naming the active sprint (this enforces one-open-sprint unicity for that binding). `git_dir` and
|
|
150
|
+
`data_dir` are intentionally separate so agents can run gates against one checkout while storing
|
|
151
|
+
Sprinty state somewhere deliberate. It is local state — keep it gitignored when `data_dir` lives
|
|
152
|
+
inside a repository.
|
|
64
153
|
|
|
65
154
|
## Develop
|
|
66
155
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sprinty",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Disciplined sprint workflow: dashboard-at-start, feature subsprints, artifacts, bug-id follow-ups, spike investigations, an immutable git-anchored ledger, and programmatic close gates.",
|
|
5
|
+
"author": { "name": "Elie Bursztein" },
|
|
6
|
+
"homepage": "https://github.com/ebursztein/sprinty",
|
|
7
|
+
"repository": "https://github.com/ebursztein/sprinty",
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"keywords": ["sprint", "discipline", "ledger", "mcp"],
|
|
10
|
+
"mcpServers": {
|
|
11
|
+
"sprinty": { "command": "npx", "args": ["-y", "sprinty-mcp"] }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Sprinty for Codex CLI
|
|
2
|
+
|
|
3
|
+
The supported command-line Codex plugin lives at `plugins/sprinty/`, with the repository-local
|
|
4
|
+
marketplace at `.agents/plugins/marketplace.json`. This directory is documentation only.
|
|
5
|
+
|
|
6
|
+
Install from a repository checkout:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
codex plugin marketplace add .
|
|
10
|
+
codex plugin add sprinty@sprinty-local
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then start a new Codex CLI thread so the plugin skills and MCP server are loaded.
|
|
14
|
+
|
|
15
|
+
This marketplace layout is for Git/repo installs, not the npm tarball. The npm package provides the
|
|
16
|
+
MCP server used by the plugin (`npx -y sprinty-mcp`).
|
|
17
|
+
|
|
18
|
+
The installed plugin provides:
|
|
19
|
+
|
|
20
|
+
- Sprinty MCP tools through `npx -y sprinty-mcp`
|
|
21
|
+
- shared Sprinty skills from the canonical repository `skills/` directory
|
|
22
|
+
|
|
23
|
+
The npm package includes the top-level `skills/` directory for MCP resource serving. Client
|
|
24
|
+
directories may contain symlinks to that canonical directory in a Git checkout; npm tarballs do not
|
|
25
|
+
need per-client skill copies.
|
|
26
|
+
|
|
27
|
+
MCP-only setup without plugin skills:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
codex mcp add sprinty -- npx -y sprinty-mcp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Sprinty does not infer state from Codex's MCP launch cwd. Start every sprint with explicit
|
|
34
|
+
`git_dir` and `data_dir`; `git_dir` is where commits, gates, coverage, and change maps run, while
|
|
35
|
+
`data_dir` stores the `current` pointer and JSONL ledgers. For read-only tools before `sprint_new`,
|
|
36
|
+
you may pre-bind with both `SPRINTY_GIT_DIR` and `SPRINTY_DATA_DIR` or both `--git-dir` and
|
|
37
|
+
`--data-dir`.
|
|
38
|
+
|
|
39
|
+
Do not copy skill files into client directories; keep them in the top-level `skills/` directory.
|
|
40
|
+
|
|
41
|
+
For human visibility during a sprint, ask Codex to call `dashboard()` and open the returned
|
|
42
|
+
`http://127.0.0.1:<port>` URL in a browser.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Sprinty
|
|
2
|
+
|
|
3
|
+
When a task is non-trivial, run a disciplined sprint with the sprinty MCP.
|
|
4
|
+
|
|
5
|
+
See `skills/how-to-run-a-sprint` and `skills/using-sprinty`. The loop:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
sprint_new(goal, git_dir, data_dir, context_notes?)
|
|
9
|
+
-> dashboard()
|
|
10
|
+
-> subsprint_new(description, goals[], gates[], dependencies?)
|
|
11
|
+
-> spike(description, goals[], gates[], dependencies?)
|
|
12
|
+
-> add(subsprint, description, code_locations[], gates[], dependencies?)
|
|
13
|
+
-> artifact_add/list/amend/deprecate(...)
|
|
14
|
+
-> follow_up(target, description, bug_id|bug_ids)
|
|
15
|
+
-> dependencies(target, dependencies[])
|
|
16
|
+
-> done(commit_id, gate_results[], changelog) | split(...) | deprecate(reason)
|
|
17
|
+
-> spike_conclude(subsprint, conclusion) | spike_deprecate(subsprint, reason)
|
|
18
|
+
-> changelog()
|
|
19
|
+
-> sprint_close(coverage: { path, format: "lcov", command? })
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Rules: IDs are minted by the server (`S01`, `S01-001`) — never invent them. Start with explicit
|
|
23
|
+
absolute `git_dir` and `data_dir`; `git_dir` is where commits/gates/coverage run, and `data_dir`
|
|
24
|
+
stores Sprinty's `current` pointer and JSONL ledgers. Every item needs a
|
|
25
|
+
description, at least one code location, and at least one gate. `done` requires a real commit,
|
|
26
|
+
passing evidence for every declared item gate, and a semver-style changelog line with a verb such as
|
|
27
|
+
`added`, `fixed`, or `removed`. `split` and `deprecate` are terminal non-code exits. Each
|
|
28
|
+
subsprint should be one feature; use `spike()` for feature investigations, then close the spike with
|
|
29
|
+
`spike_conclude()` or `spike_deprecate()`. Use `artifact_add/list/amend/deprecate` for durable
|
|
30
|
+
outputs and `follow_up()` with bug ids for bugs found while moving fast. `current()` returns the
|
|
31
|
+
dependency graph, including topological order and cycles, plus relevant artifacts, recent artifacts,
|
|
32
|
+
and recent activity. `done` records a Git-backed change map. `changelog()` returns Markdown with
|
|
33
|
+
changelog and change-map tables. `sprint_close` rechecks commits, re-runs executable gates, requires
|
|
34
|
+
an LCOV coverage report path, and refuses to close on any blocker. Use `search(pattern,
|
|
35
|
+
context_lines)` to query the immutable ledger. Use `dashboard()` immediately after `sprint_new()`,
|
|
36
|
+
and give the returned localhost URL to the human so they can watch the sprint timeline.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sprinty",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Disciplined sprint workflow with dashboard-at-start, feature subsprints, artifacts, bug-id follow-ups, spike investigations, an immutable git-anchored ledger, and close gates.",
|
|
5
|
+
"contextFileName": "GEMINI.md",
|
|
6
|
+
"mcpServers": {
|
|
7
|
+
"sprinty": { "command": "npx", "args": ["-y", "sprinty-mcp"] }
|
|
8
|
+
}
|
|
9
|
+
}
|
package/dist/dashboard/page.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PAGE = "<!doctype html>\n<html><head><meta charset=\"utf-8\"><title>sprinty</title>\n<style>body{font:14px ui-monospace,monospace;margin:2rem;background:#0b0b0c;color:#e6e6e6}\n.sub{border:1px solid #333;border-radius:8px;padding:1rem;margin:1rem 0}\n.done{opacity:.55}.gate{color:#7aa2f7}.bad{color:#f7768e}.ok{color:#9ece6a}h1{font-size:1rem}</style></head>\n<body><h1 id=\"goal\">loading\u2026</h1><div id=\"root\"></div>\n<script>\nasync function tick(){\n const s=await (await fetch('/state')).json();\n document.getElementById('goal').textContent='Sprint: '+s.goal+' ['+s.status+'] @'+s.branch;\n document.getElementById('root').innerHTML=s.subsprints.map(function(ss){\n return '<div class=\"sub\"><b>'+ss.id+'</b> '+ss.description+' <i>['+ss.status+']</i>'+\n ss.items.map(function(i){return '<div class=\"'+(i.status==='resolved'?'done':'')+'\">'+\n i.id+' \u2014 '+i.description+(i.disposition?' ('+i.disposition+')':'')+'</div>';}).join('')+'</div>';\n }).join('');\n}\ntick();setInterval(tick,2000);\n</script></body></html>";
|
|
1
|
+
export declare const PAGE = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>sprinty dashboard</title>\n<style>\n:root{color-scheme:dark;--bg:#101112;--panel:#191b1f;--panel2:#20242a;--line:#333842;--text:#f0f2f4;--muted:#9ca6b3;--blue:#80b8ff;--green:#7bd88f;--red:#ff8b8b;--yellow:#f4c96b}\n*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.45 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",sans-serif}\nheader{display:grid;grid-template-columns:1fr auto;gap:1rem;align-items:end;padding:1.25rem 1.5rem;border-bottom:1px solid var(--line);background:#15171a}\nh1{margin:0;font-size:1.25rem;font-weight:700;letter-spacing:0}.meta{display:flex;flex-wrap:wrap;gap:.6rem;color:var(--muted);font-size:.85rem}.pill{border:1px solid var(--line);border-radius:999px;padding:.2rem .55rem;background:var(--panel2);color:var(--muted)}\n.pill.active{color:var(--green);border-color:#3b7049}.pill.closed{color:var(--blue);border-color:#355c88}\nmain{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(22rem,.75fr);gap:1rem;padding:1rem;max-width:1400px;margin:0 auto}\nsection{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:1rem;min-width:0}section+section{margin-top:1rem}\nh2{margin:0 0 .75rem;font-size:.82rem;text-transform:uppercase;color:var(--muted);letter-spacing:.04em}\n.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.6rem}.metric{background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:.75rem}.metric b{display:block;font-size:1.3rem}.metric span{color:var(--muted);font-size:.8rem}\n.sub{border-top:1px solid var(--line);padding:1rem 0}.sub:first-child{border-top:0;padding-top:0}.row{display:flex;gap:.6rem;align-items:center;justify-content:space-between}.title{font-weight:650}.desc{color:var(--muted);margin:.15rem 0 .6rem}.items{display:grid;gap:.55rem}\n.item{border:1px solid var(--line);background:var(--panel2);border-radius:6px;padding:.7rem}.item.terminal{opacity:.72}.item-head{display:flex;flex-wrap:wrap;gap:.45rem;align-items:center;margin-bottom:.3rem}.id{font:12px ui-monospace,monospace;color:var(--blue)}\n.status{font-size:.75rem;color:var(--muted)}.open{color:var(--yellow)}.completed{color:var(--green)}.split{color:var(--blue)}.deprecated,.failed{color:var(--red)}\n.gates{display:grid;gap:.25rem;margin-top:.45rem}.gate{font:12px ui-monospace,monospace;color:var(--muted);overflow-wrap:anywhere}.gate.pass{color:var(--green)}.gate.fail{color:var(--red)}\n.timeline{display:grid;gap:.5rem}.event{display:grid;grid-template-columns:8.5rem minmax(0,1fr);gap:.7rem;border-top:1px solid var(--line);padding-top:.5rem}.event:first-child{border-top:0;padding-top:0}.time{color:var(--muted);font:12px ui-monospace,monospace}.event b{font-size:.82rem}.event p{margin:.12rem 0 0;color:var(--muted);overflow-wrap:anywhere}\n.empty{color:var(--muted);padding:.6rem 0}.error{color:var(--red)}\n@media(max-width:850px){header{grid-template-columns:1fr}main{grid-template-columns:1fr}.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.event{grid-template-columns:1fr}}\n</style>\n</head>\n<body>\n<header>\n <div>\n <h1 id=\"goal\">Loading sprint...</h1>\n <div class=\"meta\">\n <span id=\"branch\"></span>\n <span id=\"worktree\"></span>\n <span id=\"created\"></span>\n </div>\n </div>\n <span id=\"status\" class=\"pill\">loading</span>\n</header>\n<main>\n <div>\n <section>\n <h2>Progress</h2>\n <div class=\"metrics\">\n <div class=\"metric\"><b id=\"metric-subsprints\">0</b><span>subsprints</span></div>\n <div class=\"metric\"><b id=\"metric-items\">0</b><span>items</span></div>\n <div class=\"metric\"><b id=\"metric-open\">0</b><span>open</span></div>\n <div class=\"metric\"><b id=\"metric-gates\">0</b><span>gate results</span></div>\n <div class=\"metric\"><b id=\"metric-coverage\">--</b><span>line coverage</span></div>\n </div>\n </section>\n <section>\n <h2>Active Items</h2>\n <div id=\"active-items\" class=\"items\"></div>\n </section>\n <section>\n <h2>Subsprints</h2>\n <div id=\"subsprints\"></div>\n </section>\n </div>\n <aside>\n <section>\n <h2>Hotspots</h2>\n <div id=\"hotspots\" class=\"items\"></div>\n </section>\n <section>\n <h2>Timeline</h2>\n <div id=\"timeline\" class=\"timeline\"></div>\n </section>\n </aside>\n</main>\n<script>\nfunction byId(id){return document.getElementById(id)}\nfunction clear(node){node.replaceChildren()}\nfunction text(tag, className, value){\n const node=document.createElement(tag);\n if(className)node.className=className;\n node.textContent=value == null ? \"\" : String(value);\n return node;\n}\nfunction fmt(ts){\n if(!ts)return \"\";\n const d=new Date(ts);\n return Number.isNaN(d.valueOf()) ? ts : d.toLocaleString();\n}\nfunction summarize(s){\n const subs=s.subsprints||[];\n const items=subs.flatMap(function(ss){return ss.items||[]});\n const gateResults=items.reduce(function(n,i){return n+(i.gate_results||[]).length},0);\n return {subs:subs,items:items,open:items.filter(function(i){return i.status===\"open\"}),gateResults:gateResults};\n}\nfunction renderGateResults(item, target){\n const gates=document.createElement(\"div\");\n gates.className=\"gates\";\n const results=item.gate_results||[];\n if(results.length===0){\n gates.append(text(\"div\",\"gate\",\"pending evidence\"));\n } else {\n results.forEach(function(g){\n gates.append(text(\"div\",\"gate \"+(g.passed?\"pass\":\"fail\"),(g.passed?\"pass \":\"fail \")+g.kind+\": \"+g.spec+\" - \"+g.evidence));\n });\n }\n target.append(gates);\n}\nfunction renderItem(item){\n const node=document.createElement(\"div\");\n node.className=\"item \"+(item.status!==\"open\"?\"terminal\":\"\");\n const head=document.createElement(\"div\");\n head.className=\"item-head\";\n head.append(text(\"span\",\"id\",item.id));\n head.append(text(\"span\",\"status \"+item.status,item.disposition||item.status));\n if(item.commit_id)head.append(text(\"span\",\"status\",\"commit \"+item.commit_id));\n if(item.changelog)head.append(text(\"span\",\"status\",item.changelog.verb+\": \"+item.changelog.line));\n head.append(text(\"span\",\"status\",fmt(item.created_at)));\n node.append(head);\n node.append(text(\"div\",\"title\",item.description));\n if(item.code_locations&&item.code_locations.length)node.append(text(\"div\",\"desc\",item.code_locations.join(\", \")));\n renderGateResults(item,node);\n return node;\n}\nfunction renderSubsprint(ss){\n const node=document.createElement(\"div\");\n node.className=\"sub\";\n const row=document.createElement(\"div\");\n row.className=\"row\";\n row.append(text(\"div\",\"title\",ss.id+\" - \"+ss.description));\n row.append(text(\"span\",\"pill \"+ss.status,ss.status));\n node.append(row);\n node.append(text(\"div\",\"desc\",(ss.goals||[]).join(\"; \")));\n const items=document.createElement(\"div\");\n items.className=\"items\";\n (ss.items||[]).forEach(function(item){items.append(renderItem(item))});\n if(!items.childNodes.length)items.append(text(\"div\",\"empty\",\"No items yet.\"));\n node.append(items);\n return node;\n}\nfunction renderTimeline(s){\n const timeline=byId(\"timeline\");\n clear(timeline);\n const events=(s.timeline||[]).slice(-30).reverse();\n if(events.length===0){timeline.append(text(\"div\",\"empty\",\"No events yet.\"));return}\n events.forEach(function(e){\n const node=document.createElement(\"div\");\n node.className=\"event\";\n node.append(text(\"div\",\"time\",fmt(e.ts)));\n const body=document.createElement(\"div\");\n body.append(text(\"b\",\"\",e.id+\" / \"+e.type));\n body.append(text(\"p\",\"\",e.text));\n node.append(body);\n timeline.append(node);\n });\n}\nfunction renderHotspots(s){\n const target=byId(\"hotspots\");\n clear(target);\n const rows=((s.change_map&&s.change_map.hotspots)||[]).slice(0,8);\n if(rows.length===0){target.append(text(\"div\",\"empty\",\"No changed files yet.\"));return}\n rows.forEach(function(row){\n const node=document.createElement(\"div\");\n node.className=\"item\";\n const head=document.createElement(\"div\");\n head.className=\"item-head\";\n head.append(text(\"span\",\"id\",row.file));\n head.append(text(\"span\",\"status\",\"churn \"+row.churn));\n head.append(text(\"span\",\"status\",\"+\"+row.additions+\" -\"+row.deletions));\n node.append(head);\n node.append(text(\"div\",\"desc\",row.language+\" / \"+row.directory+\" / \"+(row.items||[]).join(\", \")));\n target.append(node);\n });\n}\nfunction render(s){\n const summary=summarize(s);\n byId(\"goal\").textContent=s.goal||\"(no sprint)\";\n byId(\"branch\").textContent=s.branch ? \"branch \"+s.branch : \"\";\n byId(\"worktree\").textContent=\"git \"+(s.dir||s.worktree||\"\")+\" data \"+(s.data_dir||\"\");\n byId(\"created\").textContent=s.created_at ? \"started \"+fmt(s.created_at) : \"\";\n const status=byId(\"status\");\n status.className=\"pill \"+(s.status||\"active\");\n status.textContent=s.status||\"active\";\n byId(\"metric-subsprints\").textContent=String(summary.subs.length);\n byId(\"metric-items\").textContent=String(summary.items.length);\n byId(\"metric-open\").textContent=String(summary.open.length);\n byId(\"metric-gates\").textContent=String(summary.gateResults);\n byId(\"metric-coverage\").textContent=s.coverage&&s.coverage.lines ? String(s.coverage.lines.percent)+\"%\" : \"--\";\n const active=byId(\"active-items\");\n clear(active);\n summary.open.forEach(function(item){active.append(renderItem(item))});\n if(!active.childNodes.length)active.append(text(\"div\",\"empty\",\"No open items.\"));\n const subsprints=byId(\"subsprints\");\n clear(subsprints);\n summary.subs.forEach(function(ss){subsprints.append(renderSubsprint(ss))});\n if(!subsprints.childNodes.length)subsprints.append(text(\"div\",\"empty\",\"No subsprints yet.\"));\n renderHotspots(s);\n renderTimeline(s);\n}\nasync function tick(){\n try{\n const response=await fetch(\"/state\");\n render(await response.json());\n }catch(err){\n byId(\"goal\").textContent=\"Dashboard disconnected\";\n const active=byId(\"active-items\");\n clear(active);\n active.append(text(\"div\",\"error\",err && err.message ? err.message : \"Unable to load state.\"));\n }\n}\ntick();setInterval(tick,2000);\n</script>\n</body>\n</html>";
|
package/dist/dashboard/page.js
CHANGED
|
@@ -1,19 +1,209 @@
|
|
|
1
1
|
export const PAGE = `<!doctype html>
|
|
2
|
-
<html
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
<
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>sprinty dashboard</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{color-scheme:dark;--bg:#101112;--panel:#191b1f;--panel2:#20242a;--line:#333842;--text:#f0f2f4;--muted:#9ca6b3;--blue:#80b8ff;--green:#7bd88f;--red:#ff8b8b;--yellow:#f4c96b}
|
|
9
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.45 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
10
|
+
header{display:grid;grid-template-columns:1fr auto;gap:1rem;align-items:end;padding:1.25rem 1.5rem;border-bottom:1px solid var(--line);background:#15171a}
|
|
11
|
+
h1{margin:0;font-size:1.25rem;font-weight:700;letter-spacing:0}.meta{display:flex;flex-wrap:wrap;gap:.6rem;color:var(--muted);font-size:.85rem}.pill{border:1px solid var(--line);border-radius:999px;padding:.2rem .55rem;background:var(--panel2);color:var(--muted)}
|
|
12
|
+
.pill.active{color:var(--green);border-color:#3b7049}.pill.closed{color:var(--blue);border-color:#355c88}
|
|
13
|
+
main{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(22rem,.75fr);gap:1rem;padding:1rem;max-width:1400px;margin:0 auto}
|
|
14
|
+
section{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:1rem;min-width:0}section+section{margin-top:1rem}
|
|
15
|
+
h2{margin:0 0 .75rem;font-size:.82rem;text-transform:uppercase;color:var(--muted);letter-spacing:.04em}
|
|
16
|
+
.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.6rem}.metric{background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:.75rem}.metric b{display:block;font-size:1.3rem}.metric span{color:var(--muted);font-size:.8rem}
|
|
17
|
+
.sub{border-top:1px solid var(--line);padding:1rem 0}.sub:first-child{border-top:0;padding-top:0}.row{display:flex;gap:.6rem;align-items:center;justify-content:space-between}.title{font-weight:650}.desc{color:var(--muted);margin:.15rem 0 .6rem}.items{display:grid;gap:.55rem}
|
|
18
|
+
.item{border:1px solid var(--line);background:var(--panel2);border-radius:6px;padding:.7rem}.item.terminal{opacity:.72}.item-head{display:flex;flex-wrap:wrap;gap:.45rem;align-items:center;margin-bottom:.3rem}.id{font:12px ui-monospace,monospace;color:var(--blue)}
|
|
19
|
+
.status{font-size:.75rem;color:var(--muted)}.open{color:var(--yellow)}.completed{color:var(--green)}.split{color:var(--blue)}.deprecated,.failed{color:var(--red)}
|
|
20
|
+
.gates{display:grid;gap:.25rem;margin-top:.45rem}.gate{font:12px ui-monospace,monospace;color:var(--muted);overflow-wrap:anywhere}.gate.pass{color:var(--green)}.gate.fail{color:var(--red)}
|
|
21
|
+
.timeline{display:grid;gap:.5rem}.event{display:grid;grid-template-columns:8.5rem minmax(0,1fr);gap:.7rem;border-top:1px solid var(--line);padding-top:.5rem}.event:first-child{border-top:0;padding-top:0}.time{color:var(--muted);font:12px ui-monospace,monospace}.event b{font-size:.82rem}.event p{margin:.12rem 0 0;color:var(--muted);overflow-wrap:anywhere}
|
|
22
|
+
.empty{color:var(--muted);padding:.6rem 0}.error{color:var(--red)}
|
|
23
|
+
@media(max-width:850px){header{grid-template-columns:1fr}main{grid-template-columns:1fr}.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.event{grid-template-columns:1fr}}
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<header>
|
|
28
|
+
<div>
|
|
29
|
+
<h1 id="goal">Loading sprint...</h1>
|
|
30
|
+
<div class="meta">
|
|
31
|
+
<span id="branch"></span>
|
|
32
|
+
<span id="worktree"></span>
|
|
33
|
+
<span id="created"></span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<span id="status" class="pill">loading</span>
|
|
37
|
+
</header>
|
|
38
|
+
<main>
|
|
39
|
+
<div>
|
|
40
|
+
<section>
|
|
41
|
+
<h2>Progress</h2>
|
|
42
|
+
<div class="metrics">
|
|
43
|
+
<div class="metric"><b id="metric-subsprints">0</b><span>subsprints</span></div>
|
|
44
|
+
<div class="metric"><b id="metric-items">0</b><span>items</span></div>
|
|
45
|
+
<div class="metric"><b id="metric-open">0</b><span>open</span></div>
|
|
46
|
+
<div class="metric"><b id="metric-gates">0</b><span>gate results</span></div>
|
|
47
|
+
<div class="metric"><b id="metric-coverage">--</b><span>line coverage</span></div>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
<section>
|
|
51
|
+
<h2>Active Items</h2>
|
|
52
|
+
<div id="active-items" class="items"></div>
|
|
53
|
+
</section>
|
|
54
|
+
<section>
|
|
55
|
+
<h2>Subsprints</h2>
|
|
56
|
+
<div id="subsprints"></div>
|
|
57
|
+
</section>
|
|
58
|
+
</div>
|
|
59
|
+
<aside>
|
|
60
|
+
<section>
|
|
61
|
+
<h2>Hotspots</h2>
|
|
62
|
+
<div id="hotspots" class="items"></div>
|
|
63
|
+
</section>
|
|
64
|
+
<section>
|
|
65
|
+
<h2>Timeline</h2>
|
|
66
|
+
<div id="timeline" class="timeline"></div>
|
|
67
|
+
</section>
|
|
68
|
+
</aside>
|
|
69
|
+
</main>
|
|
7
70
|
<script>
|
|
71
|
+
function byId(id){return document.getElementById(id)}
|
|
72
|
+
function clear(node){node.replaceChildren()}
|
|
73
|
+
function text(tag, className, value){
|
|
74
|
+
const node=document.createElement(tag);
|
|
75
|
+
if(className)node.className=className;
|
|
76
|
+
node.textContent=value == null ? "" : String(value);
|
|
77
|
+
return node;
|
|
78
|
+
}
|
|
79
|
+
function fmt(ts){
|
|
80
|
+
if(!ts)return "";
|
|
81
|
+
const d=new Date(ts);
|
|
82
|
+
return Number.isNaN(d.valueOf()) ? ts : d.toLocaleString();
|
|
83
|
+
}
|
|
84
|
+
function summarize(s){
|
|
85
|
+
const subs=s.subsprints||[];
|
|
86
|
+
const items=subs.flatMap(function(ss){return ss.items||[]});
|
|
87
|
+
const gateResults=items.reduce(function(n,i){return n+(i.gate_results||[]).length},0);
|
|
88
|
+
return {subs:subs,items:items,open:items.filter(function(i){return i.status==="open"}),gateResults:gateResults};
|
|
89
|
+
}
|
|
90
|
+
function renderGateResults(item, target){
|
|
91
|
+
const gates=document.createElement("div");
|
|
92
|
+
gates.className="gates";
|
|
93
|
+
const results=item.gate_results||[];
|
|
94
|
+
if(results.length===0){
|
|
95
|
+
gates.append(text("div","gate","pending evidence"));
|
|
96
|
+
} else {
|
|
97
|
+
results.forEach(function(g){
|
|
98
|
+
gates.append(text("div","gate "+(g.passed?"pass":"fail"),(g.passed?"pass ":"fail ")+g.kind+": "+g.spec+" - "+g.evidence));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
target.append(gates);
|
|
102
|
+
}
|
|
103
|
+
function renderItem(item){
|
|
104
|
+
const node=document.createElement("div");
|
|
105
|
+
node.className="item "+(item.status!=="open"?"terminal":"");
|
|
106
|
+
const head=document.createElement("div");
|
|
107
|
+
head.className="item-head";
|
|
108
|
+
head.append(text("span","id",item.id));
|
|
109
|
+
head.append(text("span","status "+item.status,item.disposition||item.status));
|
|
110
|
+
if(item.commit_id)head.append(text("span","status","commit "+item.commit_id));
|
|
111
|
+
if(item.changelog)head.append(text("span","status",item.changelog.verb+": "+item.changelog.line));
|
|
112
|
+
head.append(text("span","status",fmt(item.created_at)));
|
|
113
|
+
node.append(head);
|
|
114
|
+
node.append(text("div","title",item.description));
|
|
115
|
+
if(item.code_locations&&item.code_locations.length)node.append(text("div","desc",item.code_locations.join(", ")));
|
|
116
|
+
renderGateResults(item,node);
|
|
117
|
+
return node;
|
|
118
|
+
}
|
|
119
|
+
function renderSubsprint(ss){
|
|
120
|
+
const node=document.createElement("div");
|
|
121
|
+
node.className="sub";
|
|
122
|
+
const row=document.createElement("div");
|
|
123
|
+
row.className="row";
|
|
124
|
+
row.append(text("div","title",ss.id+" - "+ss.description));
|
|
125
|
+
row.append(text("span","pill "+ss.status,ss.status));
|
|
126
|
+
node.append(row);
|
|
127
|
+
node.append(text("div","desc",(ss.goals||[]).join("; ")));
|
|
128
|
+
const items=document.createElement("div");
|
|
129
|
+
items.className="items";
|
|
130
|
+
(ss.items||[]).forEach(function(item){items.append(renderItem(item))});
|
|
131
|
+
if(!items.childNodes.length)items.append(text("div","empty","No items yet."));
|
|
132
|
+
node.append(items);
|
|
133
|
+
return node;
|
|
134
|
+
}
|
|
135
|
+
function renderTimeline(s){
|
|
136
|
+
const timeline=byId("timeline");
|
|
137
|
+
clear(timeline);
|
|
138
|
+
const events=(s.timeline||[]).slice(-30).reverse();
|
|
139
|
+
if(events.length===0){timeline.append(text("div","empty","No events yet."));return}
|
|
140
|
+
events.forEach(function(e){
|
|
141
|
+
const node=document.createElement("div");
|
|
142
|
+
node.className="event";
|
|
143
|
+
node.append(text("div","time",fmt(e.ts)));
|
|
144
|
+
const body=document.createElement("div");
|
|
145
|
+
body.append(text("b","",e.id+" / "+e.type));
|
|
146
|
+
body.append(text("p","",e.text));
|
|
147
|
+
node.append(body);
|
|
148
|
+
timeline.append(node);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function renderHotspots(s){
|
|
152
|
+
const target=byId("hotspots");
|
|
153
|
+
clear(target);
|
|
154
|
+
const rows=((s.change_map&&s.change_map.hotspots)||[]).slice(0,8);
|
|
155
|
+
if(rows.length===0){target.append(text("div","empty","No changed files yet."));return}
|
|
156
|
+
rows.forEach(function(row){
|
|
157
|
+
const node=document.createElement("div");
|
|
158
|
+
node.className="item";
|
|
159
|
+
const head=document.createElement("div");
|
|
160
|
+
head.className="item-head";
|
|
161
|
+
head.append(text("span","id",row.file));
|
|
162
|
+
head.append(text("span","status","churn "+row.churn));
|
|
163
|
+
head.append(text("span","status","+"+row.additions+" -"+row.deletions));
|
|
164
|
+
node.append(head);
|
|
165
|
+
node.append(text("div","desc",row.language+" / "+row.directory+" / "+(row.items||[]).join(", ")));
|
|
166
|
+
target.append(node);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function render(s){
|
|
170
|
+
const summary=summarize(s);
|
|
171
|
+
byId("goal").textContent=s.goal||"(no sprint)";
|
|
172
|
+
byId("branch").textContent=s.branch ? "branch "+s.branch : "";
|
|
173
|
+
byId("worktree").textContent="git "+(s.dir||s.worktree||"")+" data "+(s.data_dir||"");
|
|
174
|
+
byId("created").textContent=s.created_at ? "started "+fmt(s.created_at) : "";
|
|
175
|
+
const status=byId("status");
|
|
176
|
+
status.className="pill "+(s.status||"active");
|
|
177
|
+
status.textContent=s.status||"active";
|
|
178
|
+
byId("metric-subsprints").textContent=String(summary.subs.length);
|
|
179
|
+
byId("metric-items").textContent=String(summary.items.length);
|
|
180
|
+
byId("metric-open").textContent=String(summary.open.length);
|
|
181
|
+
byId("metric-gates").textContent=String(summary.gateResults);
|
|
182
|
+
byId("metric-coverage").textContent=s.coverage&&s.coverage.lines ? String(s.coverage.lines.percent)+"%" : "--";
|
|
183
|
+
const active=byId("active-items");
|
|
184
|
+
clear(active);
|
|
185
|
+
summary.open.forEach(function(item){active.append(renderItem(item))});
|
|
186
|
+
if(!active.childNodes.length)active.append(text("div","empty","No open items."));
|
|
187
|
+
const subsprints=byId("subsprints");
|
|
188
|
+
clear(subsprints);
|
|
189
|
+
summary.subs.forEach(function(ss){subsprints.append(renderSubsprint(ss))});
|
|
190
|
+
if(!subsprints.childNodes.length)subsprints.append(text("div","empty","No subsprints yet."));
|
|
191
|
+
renderHotspots(s);
|
|
192
|
+
renderTimeline(s);
|
|
193
|
+
}
|
|
8
194
|
async function tick(){
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
195
|
+
try{
|
|
196
|
+
const response=await fetch("/state");
|
|
197
|
+
render(await response.json());
|
|
198
|
+
}catch(err){
|
|
199
|
+
byId("goal").textContent="Dashboard disconnected";
|
|
200
|
+
const active=byId("active-items");
|
|
201
|
+
clear(active);
|
|
202
|
+
active.append(text("div","error",err && err.message ? err.message : "Unable to load state."));
|
|
203
|
+
}
|
|
16
204
|
}
|
|
17
205
|
tick();setInterval(tick,2000);
|
|
18
|
-
</script
|
|
206
|
+
</script>
|
|
207
|
+
</body>
|
|
208
|
+
</html>`;
|
|
19
209
|
//# sourceMappingURL=page.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/dashboard/page.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,IAAI,GAAG
|
|
1
|
+
{"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/dashboard/page.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA+MZ,CAAC"}
|
package/dist/dashboard/server.js
CHANGED
|
@@ -1,10 +1,49 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join, normalize } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
2
5
|
import { PAGE } from "./page.js";
|
|
3
6
|
export function startDashboard(getState) {
|
|
7
|
+
const staticRoot = dashboardStaticRoot();
|
|
4
8
|
const server = createServer((req, res) => {
|
|
5
9
|
if (req.url === "/state") {
|
|
6
10
|
res.writeHead(200, { "content-type": "application/json" });
|
|
7
|
-
res.end(JSON.stringify(getState() ?? {
|
|
11
|
+
res.end(JSON.stringify(getState() ?? {
|
|
12
|
+
goal: "(no sprint)",
|
|
13
|
+
worktree: "",
|
|
14
|
+
branch: "",
|
|
15
|
+
dir: "",
|
|
16
|
+
data_dir: "",
|
|
17
|
+
created_at: new Date().toISOString(),
|
|
18
|
+
closed_at: null,
|
|
19
|
+
status: "active",
|
|
20
|
+
subsprints: [],
|
|
21
|
+
timeline: [],
|
|
22
|
+
context_notes: [],
|
|
23
|
+
graph: { nodes: [], edges: [], blocked_by: {}, unblocks: {}, topological_order: [], cycles: [] },
|
|
24
|
+
artifacts: [],
|
|
25
|
+
changelog: [],
|
|
26
|
+
change_map: { by_file: [], by_directory: [], by_language: [], hotspots: [] },
|
|
27
|
+
coverage: null,
|
|
28
|
+
}));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (req.url?.startsWith("/assets/")) {
|
|
32
|
+
const pathname = decodeURIComponent(new URL(req.url, "http://127.0.0.1").pathname);
|
|
33
|
+
const asset = normalize(join(staticRoot, pathname));
|
|
34
|
+
if (!asset.startsWith(staticRoot) || !existsSync(asset)) {
|
|
35
|
+
res.writeHead(404);
|
|
36
|
+
res.end("not found");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
res.writeHead(200, { "content-type": contentType(asset) });
|
|
40
|
+
res.end(readFileSync(asset));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const html = join(staticRoot, "index.html");
|
|
44
|
+
if (existsSync(html)) {
|
|
45
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
46
|
+
res.end(readFileSync(html, "utf8"));
|
|
8
47
|
return;
|
|
9
48
|
}
|
|
10
49
|
res.writeHead(200, { "content-type": "text/html" });
|
|
@@ -20,4 +59,23 @@ export function startDashboard(getState) {
|
|
|
20
59
|
});
|
|
21
60
|
});
|
|
22
61
|
}
|
|
62
|
+
function contentType(path) {
|
|
63
|
+
if (path.endsWith(".js"))
|
|
64
|
+
return "text/javascript";
|
|
65
|
+
if (path.endsWith(".css"))
|
|
66
|
+
return "text/css";
|
|
67
|
+
if (path.endsWith(".svg"))
|
|
68
|
+
return "image/svg+xml";
|
|
69
|
+
if (path.endsWith(".png"))
|
|
70
|
+
return "image/png";
|
|
71
|
+
return "application/octet-stream";
|
|
72
|
+
}
|
|
73
|
+
function dashboardStaticRoot() {
|
|
74
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
75
|
+
const candidates = [
|
|
76
|
+
join(here, "..", "dashboard-ui"),
|
|
77
|
+
join(process.cwd(), "dist", "dashboard-ui"),
|
|
78
|
+
];
|
|
79
|
+
return candidates.find((candidate) => existsSync(join(candidate, "index.html"))) ?? candidates[0];
|
|
80
|
+
}
|
|
23
81
|
//# sourceMappingURL=server.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAe,MAAM,WAAW,CAAC;AAEtD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAKjC,MAAM,UAAU,cAAc,CAAC,QAAiC;IAC9D,MAAM,MAAM,GAAW,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC/C,IAAI,GAAG,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;YACzB,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;YAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"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"}
|