spectoflow 0.17.5 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/package.json +1 -1
- package/templates/AGENTS.md +29 -3
- package/templates/README.md +5 -1
- package/templates/agents/framework-curator.md +94 -0
- package/templates/capabilities.md +10 -1
- package/templates/dashboard/custom/.gitkeep +3 -0
- package/templates/dashboard/public/app.js +183 -4
- package/templates/dashboard/public/i18n.js +30 -0
- package/templates/dashboard/public/index.html +25 -17
- package/templates/dashboard/public/styles.css +27 -0
- package/templates/dashboard/server.js +7 -1
- package/templates/lib/custom-dashboard.js +76 -0
- package/templates/lib/store.js +29 -5
- package/templates/skills/generate-agent/SKILL.md +135 -0
- package/templates/skills/generate-dashboard/SKILL.md +152 -0
- package/templates/skills/generate-skill/SKILL.md +153 -0
- package/templates/skills/propose-customizations/SKILL.md +72 -0
package/README.md
CHANGED
|
@@ -158,6 +158,17 @@ Either surface can also **Orchestrate** the enabled workflow: each step runs its
|
|
|
158
158
|
/api/agentfile?path=` (scoped to `.spectoflow/agents/**` + `.spectoflow/skills/**`,
|
|
159
159
|
path-traversal-safe) — the framework's only other server surface is unchanged.
|
|
160
160
|
|
|
161
|
+
**Customize.** Settings → **Customize** lets you extend the project's own spectoflow install: add a
|
|
162
|
+
dashboard, a skill, or an agent by describing what you want (or hit **Auto** to have the agent survey
|
|
163
|
+
the project and propose candidates), and it clarifies first if the ask is ambiguous. Dashboards are
|
|
164
|
+
never raw HTML — they're a small **declarative block spec** (`markdown`, `kpi-row`, `chart-bars`,
|
|
165
|
+
`chart-donut`, `table`, `list`, `stat-tile-row`) rendered by the same components the built-in Board
|
|
166
|
+
uses, so a generated dashboard automatically matches whatever design is active, in both themes, and
|
|
167
|
+
keeps matching if you switch designs later. Blocks can bind live to project stats (`bind:
|
|
168
|
+
"phases.0.pct"`) or hold a static value. Generated skills and agents follow the same gold-standard
|
|
169
|
+
shape as the shipped ones, cite real domain standards (OWASP, WCAG, C4/ADR, …) instead of generic
|
|
170
|
+
advice, and are marked `origin: user-generated` so they're easy to tell apart in the UI.
|
|
171
|
+
|
|
161
172
|
## Agents vs skills
|
|
162
173
|
|
|
163
174
|
Agents (`.spectoflow/agents/`) are **stable team personas** (Product Manager, Developer, QA Engineer…).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spectoflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Agent-agnostic spec-driven development framework + real-time local control plane. Markdown artifacts, intent router, workflow-by-scope.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spec-driven-development",
|
package/templates/AGENTS.md
CHANGED
|
@@ -45,9 +45,11 @@ whole file. This lets the dashboard and you co-edit without clobbering. Reflect
|
|
|
45
45
|
|
|
46
46
|
## The Router (run internally on every request)
|
|
47
47
|
|
|
48
|
-
1. **Intake** — known task ("develop T-012") → load it from `plans/*.md`.
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
1. **Intake** — known task ("develop T-012") → load it from `plans/*.md`. A request to **extend
|
|
49
|
+
spectoflow itself** — add a custom dashboard, a new skill, or a new agent — routes to
|
|
50
|
+
**Customize** (see below), not the normal delivery pipeline. New request or tweak → clarify
|
|
51
|
+
(step 2) then classify. Explicit override ("just do it quick" / "full change") → forced level,
|
|
52
|
+
**policy still applies**.
|
|
51
53
|
2. **Clarify (before classifying)** — if the request is ambiguous or under-specified (a vague symptom
|
|
52
54
|
like "login doesn't work" or "displays badly", missing acceptance, several plausible readings,
|
|
53
55
|
unclear scope/users), **do not guess and do not start**. Reflect it back in one sentence, then **ask
|
|
@@ -77,6 +79,30 @@ don't leave the user unsure what to do.** Keep it to a few lines:
|
|
|
77
79
|
1. Say what you want to build (plain language — no ceremonial command needed).
|
|
78
80
|
2. The dashboard: tell them it's at its URL (see Dashboard below), and whether it's already running.
|
|
79
81
|
|
|
82
|
+
## Customize spectoflow itself — dashboards, skills, agents
|
|
83
|
+
|
|
84
|
+
The dashboard's **Settings → Customize** page (or a direct request in the same shape) lets the user
|
|
85
|
+
extend the framework for *this* project: a purpose-built dashboard page, a new skill, or a new agent.
|
|
86
|
+
Recognize this as its own request shape — distinct from a normal feature/bug request — whenever it
|
|
87
|
+
asks to **add/create a dashboard, a skill, or an agent** for the project (e.g. "add a dashboard that
|
|
88
|
+
shows my architecture", "create a skill for security review grounded in OWASP", "propose dashboards
|
|
89
|
+
worth building" for the Auto mode). Hand it to the `framework-curator` agent (capability
|
|
90
|
+
`customization`, see `.spectoflow/capabilities.md`), which runs one of:
|
|
91
|
+
|
|
92
|
+
- **`generate-dashboard`** — a declarative block-spec page (never raw HTML/CSS/JS — see
|
|
93
|
+
`.spectoflow/skills/generate-dashboard` for why), written to
|
|
94
|
+
`.spectoflow/dashboard/custom/<id>.json`.
|
|
95
|
+
- **`generate-skill`** — a new `.spectoflow/skills/<slug>/SKILL.md`, grounded in real, cited domain
|
|
96
|
+
standards, following the gold-standard shape.
|
|
97
|
+
- **`generate-agent`** — a new `.spectoflow/agents/<slug>.md` persona, same shape discipline.
|
|
98
|
+
- **`propose-customizations`** — the "Auto" mode: analyzes the project and proposes a short list of
|
|
99
|
+
candidates (with a one-line rationale each) instead of generating from a description.
|
|
100
|
+
|
|
101
|
+
**Still clarify first** (step 2 above) when the ask is vague — this is exactly the kind of request
|
|
102
|
+
`clarify` exists for. **Still gated by mode and policy** like any other change; no special-casing.
|
|
103
|
+
Report progress through the group chat as usual, so the requester watches it happen and answers any
|
|
104
|
+
clarifying question there.
|
|
105
|
+
|
|
80
106
|
## Workflow, capabilities, agents, skills
|
|
81
107
|
|
|
82
108
|
- The **active workflow** is `.spectoflow/workflow.md` — a checklist of enabled steps, editable (also
|
package/templates/README.md
CHANGED
|
@@ -30,6 +30,10 @@ sit at the project root and just point back here.
|
|
|
30
30
|
agents` / `spectoflow skills` / `spectoflow workflow`. Append `-h` to any command for its help.
|
|
31
31
|
- **Change how it runs** in the dashboard's **Settings** tab (autonomy mode, output language, and the
|
|
32
32
|
dashboard **design**), or by editing `config.json`.
|
|
33
|
+
- **Extend spectoflow itself** from Settings → **Customize**: describe a project-specific dashboard,
|
|
34
|
+
skill, or agent (or hit **Auto** to have it propose candidates from your project), and it's generated
|
|
35
|
+
for you — a dashboard appears in the nav immediately, a skill/agent follows the same gold-standard
|
|
36
|
+
shape as the shipped ones and is marked `origin: user-generated`.
|
|
33
37
|
- **Update the framework** to a newer kit: `spectoflow update` (preserves your edits; a file you
|
|
34
38
|
changed is kept and its new version is written next to it as `*.new`).
|
|
35
39
|
|
|
@@ -53,7 +57,7 @@ Your **artifacts are markdown, and they live at the project root, not in here**:
|
|
|
53
57
|
| `config.json` | Your settings: `mode`, `language`, active `agent`, `runners`, `design`, plans/specs dir. **Yours to edit** — `update` never overwrites it. |
|
|
54
58
|
| `agents/` | **Stable team personas** (product-manager, developer, qa-engineer, code-reviewer, spec-source-guardian…) — the *who*. |
|
|
55
59
|
| `skills/` | **Evolving procedures** (clarify, brainstorm, write-spec, write-plan, implement, write-e2e-tests, code-review, audit-source…) — the *how*. A workflow step → a capability → its agent → runs a skill. |
|
|
56
|
-
| `dashboard/` | The zero-dependency control plane: `server.js` (SSE + file-watch), `runner.js`, `orchestrator.js`,
|
|
60
|
+
| `dashboard/` | The zero-dependency control plane: `server.js` (SSE + file-watch), `runner.js`, `orchestrator.js`, `public/` (the UI, charts, designs, fonts), and `custom/` (your generated dashboards, one JSON spec per file). |
|
|
57
61
|
| `lib/` | The markdown storage engine (`store.js`) and helpers (e.g. `spec-drift.js` for the spec-source-guardian). |
|
|
58
62
|
| `hooks/` | Optional Claude Code hooks you can wire in yourself (e.g. `spec-drift.js`, a `Stop` hook that surfaces source-of-truth drift to the Attention tab). |
|
|
59
63
|
| `runtime.json` | **Volatile execution state** (running agents, orchestration, group-chat messages, attention items, history). Gitignored — safe to delete; it's rebuilt. |
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framework-curator
|
|
3
|
+
title: Framework Curator
|
|
4
|
+
capability: customization
|
|
5
|
+
uses: [generate-dashboard, generate-skill, generate-agent, propose-customizations]
|
|
6
|
+
description: Extends spectoflow itself for this project — custom dashboards, skills and agents, generated from a description or proposed automatically.
|
|
7
|
+
standards: [gold-standard agents & skills shape, declarative UI generation]
|
|
8
|
+
---
|
|
9
|
+
# Framework Curator
|
|
10
|
+
|
|
11
|
+
Stable team persona (the "who") for the `customization` capability — the only capability that
|
|
12
|
+
extends **the framework itself**, not the product being delivered. The *how* lives in four skills
|
|
13
|
+
(see `uses`): `generate-dashboard`, `generate-skill`, `generate-agent` turn a description (or an
|
|
14
|
+
auto-analysis) into a real, working extension; `propose-customizations` is the "Auto" mode that
|
|
15
|
+
suggests candidates instead of requiring a description. Delegate here whenever the request is to add
|
|
16
|
+
a dashboard page, a skill, or an agent to *this* project's copy of spectoflow — from the dashboard's
|
|
17
|
+
Settings → Customize page, or asked directly in chat.
|
|
18
|
+
|
|
19
|
+
## Mandate
|
|
20
|
+
|
|
21
|
+
Grow spectoflow to fit the project it's installed in, without ever degrading what's already there.
|
|
22
|
+
Every dashboard this role generates must look and behave as if the framework's own authors built it —
|
|
23
|
+
same design-token discipline, same responsiveness, same restraint. Every skill or agent it generates
|
|
24
|
+
must earn its place next to the shipped roster: grounded in a real, named standard, not generic
|
|
25
|
+
advice dressed up as a procedure. This role does not build product features; it builds the tools the
|
|
26
|
+
project's own team will use to build product features.
|
|
27
|
+
|
|
28
|
+
## Operating standards
|
|
29
|
+
|
|
30
|
+
- **Declarative dashboards, never raw markup (see `generate-dashboard`).** A custom dashboard is
|
|
31
|
+
produced as a block spec chosen from the framework's fixed vocabulary
|
|
32
|
+
(`.spectoflow/lib/custom-dashboard.js`), rendered by the exact same token-driven components the
|
|
33
|
+
built-in Board uses. Why: this is what guarantees a generated dashboard matches the *active* design
|
|
34
|
+
and every future one the user switches to, with zero page-specific CSS to keep in sync, and no
|
|
35
|
+
arbitrary generated code ever executing in the dashboard.
|
|
36
|
+
- **Gold-standard shape for skills and agents (`docs/agents-skills-standard.md`).** A generated
|
|
37
|
+
`SKILL.md` or agent `.md` follows the exact same front-matter and heading structure as every
|
|
38
|
+
shipped one — `## When to use` / `## Method` / `## Output contract` / `## Quality bar` /
|
|
39
|
+
`## References` for a skill; `## Mandate` / `## Operating standards` / `## Definition of done` /
|
|
40
|
+
`## Handoff` / `## Guardrails` / `## References` for an agent. Why: a skill or agent that doesn't
|
|
41
|
+
match the shape the dashboard's Agents & Skills tab and the rest of the framework expect degrades
|
|
42
|
+
the whole system's consistency, not just its own file.
|
|
43
|
+
- **Ground every generated skill in a real, current, cited standard for its domain** — a security
|
|
44
|
+
skill cites OWASP (ASVS/Top 10) or an equivalent named authority, an architecture skill cites C4/ADR
|
|
45
|
+
or an equivalent, and so on (see `generate-skill`'s Method for how to identify and verify the right
|
|
46
|
+
one). Why: the whole point of a skill is to encode a domain's actual best practice, not the model's
|
|
47
|
+
unaided guess at what "good" looks like — the same discipline the framework's own shipped skills
|
|
48
|
+
already follow (see any of them for a worked example).
|
|
49
|
+
- **Clarify before generating, using the existing reflex.** A vague ask ("add a dashboard for my
|
|
50
|
+
project") is exactly what `.spectoflow/skills/clarify` exists for — reflect it back, ask one
|
|
51
|
+
targeted question at a time with a recommendation, converge, then generate. Never guess a
|
|
52
|
+
dashboard's content or a skill's domain from a one-line request.
|
|
53
|
+
- **Offer Auto when the user doesn't know what they want yet.** `propose-customizations` reads the
|
|
54
|
+
project (specs, plans, existing agents/skills/dashboards, project type) and proposes a short,
|
|
55
|
+
concrete, justified list — not a generic menu — so a user who doesn't know exactly what to ask for
|
|
56
|
+
still gets somewhere useful in one step.
|
|
57
|
+
|
|
58
|
+
## Definition of done
|
|
59
|
+
|
|
60
|
+
A generated dashboard renders correctly in every shipped design (light and dark) without a single
|
|
61
|
+
hardcoded color or manual style — verified by construction, since only the declarative block
|
|
62
|
+
vocabulary was used. A generated skill or agent passes the same quality bar the framework's own
|
|
63
|
+
shipped files are held to: real citations in `## References`, a checkable `## Quality bar` /
|
|
64
|
+
`## Definition of done`, and front-matter that `templates/lib/store.js`'s flat parser can read
|
|
65
|
+
unchanged. The new dashboard tab, skill, or agent is visible in the dashboard (Board's nav / Agents &
|
|
66
|
+
Skills tab) on the very next SSE tick — no manual refresh, no extra registration step.
|
|
67
|
+
|
|
68
|
+
## Handoff
|
|
69
|
+
|
|
70
|
+
Writes the generated file(s) directly (`.spectoflow/dashboard/custom/<id>.json`,
|
|
71
|
+
`.spectoflow/skills/<slug>/SKILL.md`, or `.spectoflow/agents/<slug>.md`) and reports through the
|
|
72
|
+
`::spectoflow` sentinel (see each skill's Output contract for its exact syntax) so the requester sees
|
|
73
|
+
it land in the group chat and the dashboard picks it up live. A dashboard spec that fails
|
|
74
|
+
`.spectoflow/lib/custom-dashboard.js`'s validation, or a skill/agent file whose front-matter the flat
|
|
75
|
+
parser can't read, is not done — fix it before reporting completion, never leave a broken file for the
|
|
76
|
+
dashboard to silently skip.
|
|
77
|
+
|
|
78
|
+
## Guardrails
|
|
79
|
+
|
|
80
|
+
- Never generates a dashboard block that isn't in the vocabulary `generate-dashboard` documents — an
|
|
81
|
+
unrecognized block type is invisible to the renderer, not a graceful degrade.
|
|
82
|
+
- Never removes or renames `name`, `capability`, `uses`, `description` (agents) or `name`,
|
|
83
|
+
`description` (skills) — only adds keys, per `docs/agents-skills-standard.md`'s front-matter rules.
|
|
84
|
+
- Never invents a "standard" to cite — if no real, verifiable authority exists for the requested
|
|
85
|
+
domain, say so and generate the skill's method from first principles instead, flagged as such,
|
|
86
|
+
rather than fabricating a citation.
|
|
87
|
+
- Never overwrites an existing custom dashboard/skill/agent silently on a regeneration — confirm with
|
|
88
|
+
the requester first (per mode gating) when a chosen id/slug already exists.
|
|
89
|
+
|
|
90
|
+
## References
|
|
91
|
+
|
|
92
|
+
- `docs/agents-skills-standard.md` — the gold-standard shape this role's output must match.
|
|
93
|
+
- `.spectoflow/lib/custom-dashboard.js` — the declarative block vocabulary and its validator.
|
|
94
|
+
- `.spectoflow/skills/clarify` — the reflex this role leans on before generating from an ambiguous ask.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A capability is a role; an agent implements it; a skill is the procedure it runs. Workflows ask for a
|
|
4
4
|
capability, never a named agent — this keeps spectoflow agent-agnostic.
|
|
5
5
|
|
|
6
|
-
Palette: intake · research · analysis · architecture · planning · testing · implementation · security · quality · design · operations · governance.
|
|
6
|
+
Palette: intake · research · analysis · architecture · planning · testing · implementation · security · quality · design · operations · governance · customization.
|
|
7
7
|
|
|
8
8
|
`governance` is the odd one out: it is **advisory, not a workflow step**. The `spec-source-guardian`
|
|
9
9
|
(skill `audit-source`) watches that the spec (intent) and the code/tests (reality) stay coherent, and
|
|
@@ -14,6 +14,15 @@ agent reflects it back and asks **one targeted question at a time** (each with a
|
|
|
14
14
|
the need is crisp, then proceeds — it feeds the workflow, never replaces it. See `skills/clarify` and
|
|
15
15
|
the Clarify step in `AGENTS.md`.
|
|
16
16
|
|
|
17
|
+
`customization` is also **not a workflow step** — it is triggered explicitly, either from the
|
|
18
|
+
dashboard's Settings → Customize page or by a direct request ("add a dashboard for…", "create a skill
|
|
19
|
+
for…", "create an agent for…"). The `framework-curator` agent owns it, running one of four skills:
|
|
20
|
+
`generate-dashboard` (a declarative block-spec page — see `templates/lib/custom-dashboard.js`),
|
|
21
|
+
`generate-skill`, `generate-agent` (both follow `docs/agents-skills-standard.md`'s gold-standard
|
|
22
|
+
shape, grounded in real, cited domain standards), and `propose-customizations` (the "Auto" mode:
|
|
23
|
+
analyzes the project and proposes candidates instead of taking a description). Still gated by mode
|
|
24
|
+
and policy like any other change — no special-casing.
|
|
25
|
+
|
|
17
26
|
| Project type | Active capabilities |
|
|
18
27
|
|---|---|
|
|
19
28
|
| app / web / API | all |
|
|
@@ -141,6 +141,7 @@ function render(){
|
|
|
141
141
|
renderOverview(); renderBoard(); renderBacklog(); renderWorkflow(); renderTeam();
|
|
142
142
|
renderChatLog($('#chatLog')); renderChatLog($('#chatTabLog'));
|
|
143
143
|
renderSidebar(); renderRequests(); renderAttention(); renderInfo(); renderSettings();
|
|
144
|
+
renderCustomDashboards(); // adds/removes nav tabs + panels before applyActiveTab() below reads them
|
|
144
145
|
applyActiveTab(); // re-apply the current tab so an SSE-driven re-render never resets to Board
|
|
145
146
|
applyI18nStatic(); // re-translate the static markup (nav, headers, placeholders…) for this tick's language
|
|
146
147
|
}
|
|
@@ -729,6 +730,7 @@ function renderSettings(){
|
|
|
729
730
|
rows.forEach(([k,v])=>{ const r=el('div','settings-ro-row'); r.append(el('span','settings-ro-k',k), el('span','settings-ro-v',String(v))); box.append(r); });
|
|
730
731
|
}
|
|
731
732
|
const fv=$('#footerVer'); if(fv) fv.textContent = (P&&P.version) ? ('v'+P.version) : '';
|
|
733
|
+
renderCustomize();
|
|
732
734
|
}
|
|
733
735
|
async function saveSettings(){
|
|
734
736
|
flash();
|
|
@@ -737,13 +739,190 @@ async function saveSettings(){
|
|
|
737
739
|
const s=$('#settingsSaved'); if(s){ s.hidden=false; setTimeout(()=>{ s.hidden=true; },1500); }
|
|
738
740
|
}
|
|
739
741
|
|
|
742
|
+
// ---- Custom dashboards (Customize page → generate-dashboard skill) ---------------------------
|
|
743
|
+
// A custom dashboard is a DECLARATIVE block spec (.spectoflow/dashboard/custom/<id>.json, embedded
|
|
744
|
+
// in P.customDashboards by the server) — never raw HTML/CSS/JS. Every block below reuses the exact
|
|
745
|
+
// same components the built-in Board renders with (kpiCard/ocard/bars/donut/statTile/mdLite/el), so a
|
|
746
|
+
// generated dashboard automatically matches the active design — and any design switched to later —
|
|
747
|
+
// with zero page-specific styling. Mirrors the schema in .spectoflow/lib/custom-dashboard.js (the
|
|
748
|
+
// Node-side validator); this is the independent browser-side reader for the same shape.
|
|
749
|
+
function resolveBind(s,bindPath,fallback){
|
|
750
|
+
if(bindPath==null) return fallback;
|
|
751
|
+
let v=s;
|
|
752
|
+
for(const p of String(bindPath).split('.')){ if(v==null) return fallback; v=v[p]; }
|
|
753
|
+
return v==null?fallback:v;
|
|
754
|
+
}
|
|
755
|
+
function renderCustomBlock(b,s){
|
|
756
|
+
switch(b.type){
|
|
757
|
+
case 'markdown': return htmlBlock('cd-markdown', mdLite(b.content||''));
|
|
758
|
+
case 'kpi-row': {
|
|
759
|
+
const row=el('div','kpi-row');
|
|
760
|
+
(b.items||[]).forEach(it=>{
|
|
761
|
+
const val=it.bind!=null?resolveBind(s,it.bind,it.value):it.value;
|
|
762
|
+
row.append(kpiCard(it.label||'', numBlock(val==null?'—':val,it.color||'var(--signal)'), it.sub||'', cssv(it.colorVar||'--signal')));
|
|
763
|
+
});
|
|
764
|
+
return row;
|
|
765
|
+
}
|
|
766
|
+
case 'chart-bars': {
|
|
767
|
+
const rows=(b.rows||[]).map(r=>({label:r.label||'', pct:r.bind!=null?(resolveBind(s,r.bind,r.pct||0)):(r.pct||0), sub:r.sub||''}));
|
|
768
|
+
return ocard(b.title||'', bars(rows));
|
|
769
|
+
}
|
|
770
|
+
case 'chart-donut': {
|
|
771
|
+
const segs=(b.segments||[]).map(g=>({key:g.key||'', value:g.bind!=null?(resolveBind(s,g.bind,g.value||0)):(g.value||0), color:cssv(g.colorVar||'--muted')}));
|
|
772
|
+
const total=segs.reduce((a,x)=>a+(Number(x.value)||0),0);
|
|
773
|
+
const d=donut(segs,140,{center:String(total),sub:t('chart.tasksSub')});
|
|
774
|
+
const legend=el('div','legend');
|
|
775
|
+
segs.forEach(seg=>{ const item=el('div','legend-item'); const sw=el('span','legend-swatch'); sw.style.background=seg.color; item.append(sw, el('span','legend-label',seg.key), el('span','legend-count',String(seg.value))); legend.append(item); });
|
|
776
|
+
const row=el('div','donut-row'); row.append(d.wrap,legend);
|
|
777
|
+
return ocard(b.title||'', row);
|
|
778
|
+
}
|
|
779
|
+
case 'table': {
|
|
780
|
+
const wrap=el('div','table-scroll'); const tbl=el('table','backlog-table');
|
|
781
|
+
const thead=el('thead'); const htr=el('tr'); (b.columns||[]).forEach(c=>htr.append(el('th',null,String(c)))); thead.append(htr); tbl.append(thead);
|
|
782
|
+
const tbody=el('tbody'); (b.rows||[]).forEach(r=>{ const tr=el('tr'); (r||[]).forEach(c=>tr.append(el('td',null,String(c)))); tbody.append(tr); }); tbl.append(tbody);
|
|
783
|
+
wrap.append(tbl);
|
|
784
|
+
return b.title? ocard(b.title,wrap) : wrap;
|
|
785
|
+
}
|
|
786
|
+
case 'list': {
|
|
787
|
+
const ul=el('ul','flatlist'); (b.items||[]).forEach(x=>ul.append(li(null,String(x))));
|
|
788
|
+
return b.title? ocard(b.title,ul) : ul;
|
|
789
|
+
}
|
|
790
|
+
case 'stat-tile-row': {
|
|
791
|
+
const row=el('div','stat-tiles');
|
|
792
|
+
(b.items||[]).forEach(it=>{ const val=it.bind!=null?resolveBind(s,it.bind,it.value):it.value; row.append(statTile(val==null?'—':String(val), it.label||'', it.sub||'')); });
|
|
793
|
+
return row;
|
|
794
|
+
}
|
|
795
|
+
default: return el('div','empty','Unknown block type: '+b.type);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
function customDashboardTabs(){ return $$('#tabs .tab[data-tab^="custom:"]'); }
|
|
799
|
+
// Adds/removes nav tabs + panels to match P.customDashboards, and refreshes an existing tab's label
|
|
800
|
+
// if the dashboard was regenerated with a new title. Console's rail and Orbit's radial menu both read
|
|
801
|
+
// #tabs live, so a custom tab appears in either design's navigation with no design-specific change.
|
|
802
|
+
function syncCustomTabs(list){
|
|
803
|
+
const tabsNav=$('#tabs'); if(!tabsNav) return;
|
|
804
|
+
const wanted=new Set(list.map(spec=>'custom:'+spec.id));
|
|
805
|
+
customDashboardTabs().forEach(btn=>{
|
|
806
|
+
const id=btn.dataset.tab;
|
|
807
|
+
if(!wanted.has(id)){ const panel=$('.panel[data-panel="'+id+'"]'); if(panel) panel.remove(); btn.remove(); }
|
|
808
|
+
});
|
|
809
|
+
list.forEach(spec=>{
|
|
810
|
+
const id='custom:'+spec.id;
|
|
811
|
+
let btn=$('#tabs .tab[data-tab="'+id+'"]');
|
|
812
|
+
if(!btn){
|
|
813
|
+
btn=el('button','tab'); btn.dataset.tab=id;
|
|
814
|
+
const icoKey=spec.icon||'info';
|
|
815
|
+
const ico=el('span','tab-ico'); ico.dataset.icon=icoKey; if(typeof ICON!=='undefined'&&ICON[icoKey]) ico.innerHTML=ICON[icoKey];
|
|
816
|
+
btn.append(ico, el('span','tab-label',spec.title||spec.id));
|
|
817
|
+
btn.addEventListener('click',()=>navigateTab(id));
|
|
818
|
+
tabsNav.append(btn);
|
|
819
|
+
const panel=el('section','panel'); panel.dataset.panel=id;
|
|
820
|
+
const wrap=el('div','custom-dash-wrap');
|
|
821
|
+
wrap.append(el('h2','panel-title',spec.title||spec.id));
|
|
822
|
+
wrap.append(el('div','custom-dash-body'));
|
|
823
|
+
panel.append(wrap);
|
|
824
|
+
$('.stage').append(panel);
|
|
825
|
+
} else {
|
|
826
|
+
const lbl=btn.querySelector('.tab-label'); if(lbl) lbl.textContent=spec.title||spec.id;
|
|
827
|
+
const title=$('.panel[data-panel="'+id+'"] .panel-title'); if(title) title.textContent=spec.title||spec.id;
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
function renderCustomDashboards(){
|
|
832
|
+
const list=P.customDashboards||[];
|
|
833
|
+
syncCustomTabs(list);
|
|
834
|
+
const s=SpectoStats.stats(P);
|
|
835
|
+
list.forEach(spec=>{
|
|
836
|
+
const box=$('.panel[data-panel="custom:'+spec.id+'"] .custom-dash-body'); if(!box) return;
|
|
837
|
+
box.innerHTML='';
|
|
838
|
+
(spec.blocks||[]).forEach(b=> box.append(renderCustomBlock(b,s)));
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ---- Settings → Customize: add dashboards/skills/agents by description, or "Auto" -------------
|
|
843
|
+
// Generation itself is real agent work (research, clarify, write a file) — this UI never does it
|
|
844
|
+
// client-side. It just constructs a plain-language prompt (recognized by AGENTS.md's Router, which
|
|
845
|
+
// hands it to framework-curator) and sends it through the SAME /api/run + group-chat pipeline every
|
|
846
|
+
// other "Run" already uses, then jumps to Chat so the requester watches it happen and can answer any
|
|
847
|
+
// clarifying question there — no separate conversational UI to build or keep in sync.
|
|
848
|
+
const CZ_KINDS=[
|
|
849
|
+
{ kind:'dashboard', promptAdd:(d)=>'Add a custom dashboard: '+d, promptAuto:'Propose dashboard candidates for this project (Auto customize)' },
|
|
850
|
+
{ kind:'skill', promptAdd:(d)=>'Create a new skill: '+d, promptAuto:'Propose skill candidates for this project (Auto customize)' },
|
|
851
|
+
{ kind:'agent', promptAdd:(d)=>'Create a new agent: '+d, promptAuto:'Propose agent candidates for this project (Auto customize)' },
|
|
852
|
+
];
|
|
853
|
+
function czItemsFor(kind){
|
|
854
|
+
if(kind==='dashboard') return (P.customDashboards||[]).map(spec=>({ title:spec.title||spec.id, sub:(spec.blocks||[]).length+' '+t('customize.blocksSub'), open:()=>navigateTab('custom:'+spec.id) }));
|
|
855
|
+
if(kind==='skill') return (P.skills||[]).filter(s=>s.custom).map(s=>({ title:s.name, sub:s.description||'', open:()=>openFileDrawer('skill',s) }));
|
|
856
|
+
return (P.agents||[]).filter(a=>a.custom).map(a=>({ title:a.title||a.name, sub:a.description||'', open:()=>openFileDrawer('agent',a) }));
|
|
857
|
+
}
|
|
858
|
+
function renderCustomize(){
|
|
859
|
+
const root=$('#czRoot'); if(!root) return;
|
|
860
|
+
const openKind=root.dataset.open||'';
|
|
861
|
+
root.innerHTML='';
|
|
862
|
+
CZ_KINDS.forEach(({kind})=>{
|
|
863
|
+
const items=czItemsFor(kind);
|
|
864
|
+
const block=el('div','cz-block');
|
|
865
|
+
const head=el('div','cz-head');
|
|
866
|
+
head.append(el('h3',null,t('customize.'+kind+'s')+' ('+items.length+')'));
|
|
867
|
+
const addBtn=el('button','btn cz-add',t('customize.add.'+kind));
|
|
868
|
+
addBtn.setAttribute('aria-expanded',String(openKind===kind));
|
|
869
|
+
addBtn.addEventListener('click',()=>{ root.dataset.open=(openKind===kind)?'':kind; renderCustomize(); });
|
|
870
|
+
head.append(addBtn); block.append(head);
|
|
871
|
+
const list=el('div','cz-list');
|
|
872
|
+
if(!items.length) list.append(el('div','empty',t('customize.empty.'+kind)));
|
|
873
|
+
items.forEach(it=>{
|
|
874
|
+
const row=el('div','cz-item'); row.tabIndex=0;
|
|
875
|
+
row.append(el('span','cz-item-title',it.title));
|
|
876
|
+
if(it.sub) row.append(el('span','cz-item-sub',it.sub));
|
|
877
|
+
row.addEventListener('click',it.open);
|
|
878
|
+
row.addEventListener('keydown',(e)=>{ if(e.key==='Enter') it.open(); });
|
|
879
|
+
list.append(row);
|
|
880
|
+
});
|
|
881
|
+
block.append(list);
|
|
882
|
+
if(openKind===kind){
|
|
883
|
+
const form=el('div','cz-form');
|
|
884
|
+
const ta=el('textarea','chat-ta'); ta.placeholder=t('customize.describePh');
|
|
885
|
+
const sel=el('select','chat-agent');
|
|
886
|
+
const runners=Object.keys((P.config&&P.config.runners)||{claude:1});
|
|
887
|
+
runners.forEach((k)=>{ const o=document.createElement('option'); o.value=k; o.textContent=k; sel.append(o); });
|
|
888
|
+
if(P.config&&P.config.agent) sel.value=P.config.agent;
|
|
889
|
+
const actions=el('div','cz-form-actions');
|
|
890
|
+
const autoBtn=el('button','btn',t('customize.auto'));
|
|
891
|
+
const goBtn=el('button','btn primary',t('customize.generate'));
|
|
892
|
+
autoBtn.addEventListener('click',()=>czSubmit(kind,null,sel.value));
|
|
893
|
+
goBtn.addEventListener('click',()=>{ const v=ta.value.trim(); if(v) czSubmit(kind,v,sel.value); });
|
|
894
|
+
actions.append(sel,autoBtn,goBtn);
|
|
895
|
+
form.append(ta,actions);
|
|
896
|
+
block.append(form);
|
|
897
|
+
}
|
|
898
|
+
root.append(block);
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
async function czSubmit(kind,description,agent){
|
|
902
|
+
const cfg=CZ_KINDS.find((c)=>c.kind===kind);
|
|
903
|
+
const prompt=description?cfg.promptAdd(description):cfg.promptAuto;
|
|
904
|
+
await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({prompt,agent})});
|
|
905
|
+
const root=$('#czRoot'); if(root) root.dataset.open='';
|
|
906
|
+
navigateTab('chat');
|
|
907
|
+
}
|
|
908
|
+
|
|
740
909
|
// ---- client-side routing: /<tab>[/<taskId>] via the History API ------------
|
|
910
|
+
// A custom dashboard (Customize page) gets its own tab id "custom:<id>" and its own URL shape
|
|
911
|
+
// /custom/<id> — kept out of ROUTES (a fixed list) since the set of custom ids is dynamic; recognized
|
|
912
|
+
// by a dedicated branch in tabFromPath()/navigateTab() instead.
|
|
741
913
|
const ROUTES=['board','requests','attention','backlog','workflow','team','chat','info','settings'];
|
|
742
|
-
function tabFromPath(){
|
|
914
|
+
function tabFromPath(){
|
|
915
|
+
const s=location.pathname.split('/').filter(Boolean);
|
|
916
|
+
if(s[0]==='custom'&&s[1]) return 'custom:'+decodeURIComponent(s[1]);
|
|
917
|
+
return ROUTES.includes(s[0])?s[0]:null;
|
|
918
|
+
}
|
|
743
919
|
function taskFromPath(){ const s=location.pathname.split('/').filter(Boolean); return (ROUTES.includes(s[0])&&s[1])?decodeURIComponent(s[1]):null; }
|
|
744
|
-
function navigateTab(
|
|
745
|
-
activeTab=
|
|
746
|
-
if(push!==false)
|
|
920
|
+
function navigateTab(tabId,push){
|
|
921
|
+
activeTab=tabId; try{ localStorage.setItem('spf-tab',tabId); }catch{}
|
|
922
|
+
if(push!==false){
|
|
923
|
+
const isCustom=tabId.indexOf('custom:')===0;
|
|
924
|
+
history.pushState(null,'', isCustom ? '/custom/'+encodeURIComponent(tabId.slice(7)) : '/'+tabId);
|
|
925
|
+
}
|
|
747
926
|
applyActiveTab();
|
|
748
927
|
closeNav(); // a tab pick closes the mobile menu
|
|
749
928
|
}
|
|
@@ -107,6 +107,11 @@ en: {
|
|
|
107
107
|
'task.failingPassing':'{f} failing, {p} passing','task.comments':'Comments','task.noComments':'No comments.',
|
|
108
108
|
'task.addCommentPlaceholder':'Add a comment, a remark, feedback…',
|
|
109
109
|
'task.toAnalyzeHint':'"To analyze" moves the task back so the agent picks it up next round.',
|
|
110
|
+
'customize.title':'Customize','customize.sub':'Add project-specific dashboards, skills and agents — describe what you want, or let Auto propose candidates from this project.',
|
|
111
|
+
'customize.dashboards':'Dashboards','customize.skills':'Skills','customize.agents':'Agents',
|
|
112
|
+
'customize.add.dashboard':'Add dashboard','customize.add.skill':'Add skill','customize.add.agent':'Add agent',
|
|
113
|
+
'customize.empty.dashboard':'No custom dashboards yet.','customize.empty.skill':'No custom skills yet.','customize.empty.agent':'No custom agents yet.',
|
|
114
|
+
'customize.describePh':'Describe what you want…','customize.auto':'Auto','customize.generate':'Generate','customize.blocksSub':'block(s)',
|
|
110
115
|
},
|
|
111
116
|
fr: {
|
|
112
117
|
'nav.board':'Tableau','nav.requests':'Demandes','nav.attention':'Attention','nav.backlog':'Backlog',
|
|
@@ -197,6 +202,11 @@ fr: {
|
|
|
197
202
|
'task.failingPassing':'{f} en échec, {p} réussi(s)','task.comments':'Commentaires','task.noComments':'Aucun commentaire.',
|
|
198
203
|
'task.addCommentPlaceholder':'Ajouter un commentaire, une remarque, un retour…',
|
|
199
204
|
'task.toAnalyzeHint':'« À analyser » renvoie la tâche pour que l’agent la reprenne au tour suivant.',
|
|
205
|
+
'customize.title':'Personnaliser','customize.sub':'Ajoutez des dashboards, compétences et agents propres au projet — décrivez ce que vous voulez, ou laissez Auto proposer des pistes à partir de ce projet.',
|
|
206
|
+
'customize.dashboards':'Dashboards','customize.skills':'Compétences','customize.agents':'Agents',
|
|
207
|
+
'customize.add.dashboard':'Ajouter un dashboard','customize.add.skill':'Ajouter une compétence','customize.add.agent':'Ajouter un agent',
|
|
208
|
+
'customize.empty.dashboard':'Aucun dashboard personnalisé pour l’instant.','customize.empty.skill':'Aucune compétence personnalisée pour l’instant.','customize.empty.agent':'Aucun agent personnalisé pour l’instant.',
|
|
209
|
+
'customize.describePh':'Décrivez ce que vous voulez…','customize.auto':'Auto','customize.generate':'Générer','customize.blocksSub':'bloc(s)',
|
|
200
210
|
},
|
|
201
211
|
es: {
|
|
202
212
|
'nav.board':'Tablero','nav.requests':'Solicitudes','nav.attention':'Atención','nav.backlog':'Backlog',
|
|
@@ -287,6 +297,11 @@ es: {
|
|
|
287
297
|
'task.failingPassing':'{f} fallando, {p} superadas','task.comments':'Comentarios','task.noComments':'Sin comentarios.',
|
|
288
298
|
'task.addCommentPlaceholder':'Añade un comentario, una observación, feedback…',
|
|
289
299
|
'task.toAnalyzeHint':'«Por analizar» devuelve la tarea para que el agente la retome en la siguiente ronda.',
|
|
300
|
+
'customize.title':'Personalizar','customize.sub':'Añade dashboards, habilidades y agentes propios del proyecto — describe lo que quieres, o deja que Auto proponga candidatos a partir de este proyecto.',
|
|
301
|
+
'customize.dashboards':'Dashboards','customize.skills':'Habilidades','customize.agents':'Agentes',
|
|
302
|
+
'customize.add.dashboard':'Añadir dashboard','customize.add.skill':'Añadir habilidad','customize.add.agent':'Añadir agente',
|
|
303
|
+
'customize.empty.dashboard':'Aún no hay dashboards personalizados.','customize.empty.skill':'Aún no hay habilidades personalizadas.','customize.empty.agent':'Aún no hay agentes personalizados.',
|
|
304
|
+
'customize.describePh':'Describe lo que quieres…','customize.auto':'Auto','customize.generate':'Generar','customize.blocksSub':'bloque(s)',
|
|
290
305
|
},
|
|
291
306
|
de: {
|
|
292
307
|
'nav.board':'Board','nav.requests':'Anfragen','nav.attention':'Hinweise','nav.backlog':'Backlog',
|
|
@@ -377,6 +392,11 @@ de: {
|
|
|
377
392
|
'task.failingPassing':'{f} fehlgeschlagen, {p} bestanden','task.comments':'Kommentare','task.noComments':'Keine Kommentare.',
|
|
378
393
|
'task.addCommentPlaceholder':'Kommentar, Anmerkung oder Feedback hinzufügen…',
|
|
379
394
|
'task.toAnalyzeHint':'„Zu analysieren“ gibt die Aufgabe zurück, damit der Agent sie in der nächsten Runde aufgreift.',
|
|
395
|
+
'customize.title':'Anpassen','customize.sub':'Fügen Sie projektspezifische Dashboards, Skills und Agenten hinzu — beschreiben Sie, was Sie wollen, oder lassen Sie Auto Kandidaten aus diesem Projekt vorschlagen.',
|
|
396
|
+
'customize.dashboards':'Dashboards','customize.skills':'Skills','customize.agents':'Agenten',
|
|
397
|
+
'customize.add.dashboard':'Dashboard hinzufügen','customize.add.skill':'Skill hinzufügen','customize.add.agent':'Agent hinzufügen',
|
|
398
|
+
'customize.empty.dashboard':'Noch keine eigenen Dashboards.','customize.empty.skill':'Noch keine eigenen Skills.','customize.empty.agent':'Noch keine eigenen Agenten.',
|
|
399
|
+
'customize.describePh':'Beschreiben Sie, was Sie wollen…','customize.auto':'Auto','customize.generate':'Generieren','customize.blocksSub':'Block/Blöcke',
|
|
380
400
|
},
|
|
381
401
|
pt: {
|
|
382
402
|
'nav.board':'Painel','nav.requests':'Pedidos','nav.attention':'Atenção','nav.backlog':'Backlog',
|
|
@@ -467,6 +487,11 @@ pt: {
|
|
|
467
487
|
'task.failingPassing':'{f} a falhar, {p} bem-sucedido(s)','task.comments':'Comentários','task.noComments':'Sem comentários.',
|
|
468
488
|
'task.addCommentPlaceholder':'Adicione um comentário, uma observação, feedback…',
|
|
469
489
|
'task.toAnalyzeHint':'«Por analisar» devolve a tarefa para o agente a retomar na ronda seguinte.',
|
|
490
|
+
'customize.title':'Personalizar','customize.sub':'Adicione dashboards, habilidades e agentes específicos do projeto — descreva o que quer, ou deixe o Auto propor candidatos a partir deste projeto.',
|
|
491
|
+
'customize.dashboards':'Dashboards','customize.skills':'Habilidades','customize.agents':'Agentes',
|
|
492
|
+
'customize.add.dashboard':'Adicionar dashboard','customize.add.skill':'Adicionar habilidade','customize.add.agent':'Adicionar agente',
|
|
493
|
+
'customize.empty.dashboard':'Ainda sem dashboards personalizados.','customize.empty.skill':'Ainda sem habilidades personalizadas.','customize.empty.agent':'Ainda sem agentes personalizados.',
|
|
494
|
+
'customize.describePh':'Descreva o que quer…','customize.auto':'Auto','customize.generate':'Gerar','customize.blocksSub':'bloco(s)',
|
|
470
495
|
},
|
|
471
496
|
it: {
|
|
472
497
|
'nav.board':'Bacheca','nav.requests':'Richieste','nav.attention':'Attenzione','nav.backlog':'Backlog',
|
|
@@ -557,6 +582,11 @@ it: {
|
|
|
557
582
|
'task.failingPassing':'{f} falliti, {p} superati','task.comments':'Commenti','task.noComments':'Nessun commento.',
|
|
558
583
|
'task.addCommentPlaceholder':'Aggiungi un commento, un’osservazione, un feedback…',
|
|
559
584
|
'task.toAnalyzeHint':'«Da analizzare» rimanda l’attività così l’agente la riprende al giro successivo.',
|
|
585
|
+
'customize.title':'Personalizza','customize.sub':'Aggiungi dashboard, skill e agenti specifici del progetto — descrivi cosa vuoi, oppure lascia che Auto proponga candidati a partire da questo progetto.',
|
|
586
|
+
'customize.dashboards':'Dashboard','customize.skills':'Skill','customize.agents':'Agenti',
|
|
587
|
+
'customize.add.dashboard':'Aggiungi dashboard','customize.add.skill':'Aggiungi skill','customize.add.agent':'Aggiungi agente',
|
|
588
|
+
'customize.empty.dashboard':'Ancora nessuna dashboard personalizzata.','customize.empty.skill':'Ancora nessuna skill personalizzata.','customize.empty.agent':'Ancora nessun agente personalizzato.',
|
|
589
|
+
'customize.describePh':'Descrivi cosa vuoi…','customize.auto':'Auto','customize.generate':'Genera','customize.blocksSub':'blocco/i',
|
|
560
590
|
},
|
|
561
591
|
};
|
|
562
592
|
|
|
@@ -4,19 +4,19 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>spectoflow · control</title>
|
|
7
|
-
<link rel="icon" type="image/png" href="logo-dark.png" />
|
|
8
|
-
<link rel="icon" type="image/png" media="(prefers-color-scheme: dark)" href="logo-white.png" />
|
|
9
|
-
<link rel="stylesheet" href="styles.css" />
|
|
10
|
-
<link rel="stylesheet" href="designs/console.css" />
|
|
11
|
-
<link rel="stylesheet" href="designs/orbit.css" />
|
|
7
|
+
<link rel="icon" type="image/png" href="/logo-dark.png" />
|
|
8
|
+
<link rel="icon" type="image/png" media="(prefers-color-scheme: dark)" href="/logo-white.png" />
|
|
9
|
+
<link rel="stylesheet" href="/styles.css" />
|
|
10
|
+
<link rel="stylesheet" href="/designs/console.css" />
|
|
11
|
+
<link rel="stylesheet" href="/designs/orbit.css" />
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<header class="topbar">
|
|
15
15
|
<div class="brand">
|
|
16
16
|
<button class="nav-toggle" id="navToggle" data-i18n-aria="topbar.navToggle" aria-label="Toggle menu" aria-expanded="false"><span></span><span></span><span></span></button>
|
|
17
17
|
<a class="brand-logo" href="/board" data-route="board" aria-label="spectoflow home">
|
|
18
|
-
<img class="brand-logo-img is-dark" src="logo-white.png" alt="spectoflow" />
|
|
19
|
-
<img class="brand-logo-img is-light" src="logo-dark.png" alt="spectoflow" />
|
|
18
|
+
<img class="brand-logo-img is-dark" src="/logo-white.png" alt="spectoflow" />
|
|
19
|
+
<img class="brand-logo-img is-light" src="/logo-dark.png" alt="spectoflow" />
|
|
20
20
|
</a>
|
|
21
21
|
<div class="brand-text">
|
|
22
22
|
<div class="brand-line">
|
|
@@ -257,6 +257,14 @@
|
|
|
257
257
|
<div class="settings-saved" id="settingsSaved" data-i18n="settings.saved" hidden>✓ saved</div>
|
|
258
258
|
</div>
|
|
259
259
|
<div class="settings-readonly" id="settingsReadonly"></div>
|
|
260
|
+
|
|
261
|
+
<!-- Customize: project-specific dashboards, skills and agents — described or auto-proposed,
|
|
262
|
+
generated by the configured agent through the same Run/Chat pipeline as any other ask. -->
|
|
263
|
+
<div class="cz-wrap">
|
|
264
|
+
<h2 class="panel-title" data-i18n="customize.title">Customize</h2>
|
|
265
|
+
<p class="panel-sub" data-i18n="customize.sub">Add project-specific dashboards, skills and agents — describe what you want, or let Auto propose candidates from this project.</p>
|
|
266
|
+
<div id="czRoot"></div>
|
|
267
|
+
</div>
|
|
260
268
|
</div>
|
|
261
269
|
</section>
|
|
262
270
|
</main>
|
|
@@ -264,8 +272,8 @@
|
|
|
264
272
|
<footer class="app-footer">
|
|
265
273
|
<div class="footer-left">
|
|
266
274
|
<a class="footer-logo" href="/board" data-route="board" aria-label="spectoflow">
|
|
267
|
-
<img class="brand-logo-img is-dark" src="logo-white.png" alt="spectoflow" />
|
|
268
|
-
<img class="brand-logo-img is-light" src="logo-dark.png" alt="spectoflow" />
|
|
275
|
+
<img class="brand-logo-img is-dark" src="/logo-white.png" alt="spectoflow" />
|
|
276
|
+
<img class="brand-logo-img is-light" src="/logo-dark.png" alt="spectoflow" />
|
|
269
277
|
</a>
|
|
270
278
|
<span class="footer-name">spectoflow</span>
|
|
271
279
|
<span class="footer-ver" id="footerVer"></span>
|
|
@@ -323,13 +331,13 @@
|
|
|
323
331
|
</defs>
|
|
324
332
|
</svg>
|
|
325
333
|
|
|
326
|
-
<script src="stats.js"></script>
|
|
327
|
-
<script src="charts.js"></script>
|
|
328
|
-
<script src="icons.js"></script>
|
|
329
|
-
<script src="designs.js"></script>
|
|
330
|
-
<script src="i18n.js"></script>
|
|
331
|
-
<script src="app.js"></script>
|
|
332
|
-
<script src="designs/console.js"></script>
|
|
333
|
-
<script src="designs/orbit.js"></script>
|
|
334
|
+
<script src="/stats.js"></script>
|
|
335
|
+
<script src="/charts.js"></script>
|
|
336
|
+
<script src="/icons.js"></script>
|
|
337
|
+
<script src="/designs.js"></script>
|
|
338
|
+
<script src="/i18n.js"></script>
|
|
339
|
+
<script src="/app.js"></script>
|
|
340
|
+
<script src="/designs/console.js"></script>
|
|
341
|
+
<script src="/designs/orbit.js"></script>
|
|
334
342
|
</body>
|
|
335
343
|
</html>
|
|
@@ -557,6 +557,33 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
|
|
|
557
557
|
.settings-ro-k { color:var(--muted); font-size:13px; }
|
|
558
558
|
.settings-ro-v { font-family:var(--mono); font-size:12.5px; color:var(--ink); }
|
|
559
559
|
|
|
560
|
+
/* ---- Customize (Settings → dashboards/skills/agents) — reuses .card/.chat-ta/.btn styling so it
|
|
561
|
+
reads as part of the same system, not a bolted-on sub-app. ---- */
|
|
562
|
+
.cz-wrap { margin-top:28px; }
|
|
563
|
+
.cz-block { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:14px 16px; margin-top:12px; }
|
|
564
|
+
.cz-head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
|
565
|
+
.cz-head h3 { margin:0; font-size:13.5px; font-weight:700; }
|
|
566
|
+
.cz-add { font-size:12.5px; padding:6px 13px; }
|
|
567
|
+
.cz-add[aria-expanded="true"] { background:var(--signal); color:var(--on-accent); border-color:transparent; }
|
|
568
|
+
.cz-list { display:flex; flex-direction:column; gap:6px; margin-top:10px; }
|
|
569
|
+
.cz-list .empty { padding:10px 2px; font-size:12.5px; color:var(--muted); }
|
|
570
|
+
.cz-item { display:flex; align-items:baseline; gap:10px; padding:8px 10px; border-radius:8px; cursor:pointer; transition:background .15s; }
|
|
571
|
+
.cz-item:hover, .cz-item:focus-visible { background:var(--surface-2); outline:none; }
|
|
572
|
+
.cz-item-title { font-size:13px; font-weight:600; }
|
|
573
|
+
.cz-item-sub { font-size:11.5px; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
574
|
+
.cz-form { margin-top:12px; padding-top:12px; border-top:1px solid var(--line); display:flex; flex-direction:column; gap:10px; }
|
|
575
|
+
.cz-form .chat-ta { min-height:64px; }
|
|
576
|
+
.cz-form-actions { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
|
577
|
+
.cz-form-actions .chat-agent { flex-shrink:0; }
|
|
578
|
+
|
|
579
|
+
/* ---- Custom dashboard pages (Customize → generate-dashboard) — every block below is one of the
|
|
580
|
+
dashboard's own existing components (.ocard/.kpi-row/.stat-tiles/.flatlist/table), so a generated
|
|
581
|
+
page needs no page-specific CSS: it inherits the active design automatically. ---- */
|
|
582
|
+
.custom-dash-wrap { display:flex; flex-direction:column; gap:14px; }
|
|
583
|
+
.custom-dash-body { display:flex; flex-direction:column; gap:14px; }
|
|
584
|
+
.cd-markdown { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:16px 18px; font-size:13.5px; line-height:1.6; }
|
|
585
|
+
.cd-markdown :first-child { margin-top:0; }
|
|
586
|
+
|
|
560
587
|
/* App footer — professional, always at the bottom of the content */
|
|
561
588
|
.app-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; padding:14px 22px; border-top:1px solid var(--line); background:var(--surface); color:var(--muted); font-size:12.5px; }
|
|
562
589
|
.footer-left { display:flex; align-items:center; gap:9px; }
|