velais-project-dashboard
activeCreates and maintains a Velais-branded project dashboard as static .html pages generated from .md files in a dashboard/ folder: three shared pages committed for the whole team (Tasks with due dates, client milestones and a timeline, Resources, Handoffs with internal handoff files and client blockers) plus private, per-person profiles that stay local (Topics, Mockup, Data, Structure, Plan, session log). Use when a user asks for a project dashboard, status page, control panel or "where does the project stand" view for a Velais project; when adding tasks, milestones, resources, handoffs, decisions, open questions, data sources, phases, mockups or todos; when switching or creating a dashboard profile; or when a Claude session must log its summary. Do NOT use for client-facing React apps (use velais-ui-design), for BC-Delivery vault control planes with write-back, or for one-off charts.
Quality Score Breakdown
Show checks (11)
- ✓ SKILL.md exists with exact casing 3/3
- ✓ Valid YAML frontmatter 3/3
- ✓ No unexpected frontmatter keys 1/1
- ✓ Name field valid (kebab-case) 2/2
- ✓ Name matches folder name 1/1
- ✓ Description field present 2/2
- ✓ No angle brackets in frontmatter 1/1
- ✓ Folder name is kebab-case 1/1
- ✓ No README.md inside skill folder 1/1
- ✓ Test directory with test-cases.yml exists 2/2
- ✓ Status 'active' is valid 1/1
Show checks (7)
- ✓ Contains action verbs: creates 2/4
- ✓ Contains trigger indicators: use when, use for, asks for 5/5
- ✓ Description is specific and actionable 4/4
- ✓ File types mentioned in description 3/3
- ✓ Description length: 895/1024 chars 2/2
- ✓ Has negative triggers (scope boundaries) 2/2
- ✓ Owner/author specified in metadata 2/2
Show checks (8)
- ✓ Skill body has content 3/3
- ✓ Has step/section structure 4/4
- ✓ Includes examples 5/5
- ✓ Includes error handling 4/4
- ✗ Consider moving detailed docs to references/ for better token efficiency 0/4
- ✓ Actionable language: 4/10 verb patterns found 3/3
- ✓ Word count: 2243/5000 2/2
- ✓ All referenced paths exist 3/3
Show checks (10)
- ✓ test-cases.yml exists and parses 3/3
- ✓ 9 should-trigger tests ✓ 4/4
- ✓ 4 should-not-trigger tests ✓ 3/3
- ✓ 11 functional tests ✓ 5/5
- ✓ 5 negative tests ✓ 3/3
- ✓ 2 edge case tests ✓ 3/3
- ✓ Performance baseline documented 2/2
- ✓ All functional tests have ≥2 assertions 2/2
- ✓ All trigger phrases are diverse 2/2
- ✓ All assertions are specific 2/2
Show checks (5)
- ✓ No secrets detected 5/5
- ✓ No injection vectors in frontmatter 3/3
- ✓ Name is not reserved 3/3
- ✓ No suspicious code patterns 2/2
- ✗ 5 external URLs found — verify they're necessary: https://fonts.googleapis.com, https://fonts.gstatic.com, https://fonts.googleapis.com/css2?family=Host+Grotesk:wght@400;500;600&display=s 1/2
Suggestions
- → Move detailed reference material to references/ and link from SKILL.md
Test Coverage
Velais project dashboard
One folder, dashboard/, holds Markdown content, a stdlib Python generator, brand
assets and the generated HTML. No server, no LLM in the build, no timestamps in the
output. Pages are read-only: people edit the Markdown (by hand or through Claude) and
the Stop hook rebuilds.
Two areas:
- Shared (committed, same for everyone, first in the nav): Tasks, Resources,
Handoffs, plus the code, the assets and the
handoffs/folder at the repo root. - Private (gitignored, one profile per person, after a divider in the nav): Topics, Mockup, Data, Structure, Plan and the session log. Fully personalizable.
dashboard/
build.py Markdown -> *.html (deterministic, stdlib only) shared
session_log.py hook: registers each Claude session shared
config.json project, client, shared page paths shared
assets/ style.css, font, logo, favicon shared
content/
tasks.md internal tasks with due dates (Tasks page) shared
milestones.md client/SoW milestones, optional (Tasks timeline) shared
resources.md free Markdown (Resources page) shared
handoffs.md client blockers only (Handoffs page, part b) shared
profiles/<name>/ private, gitignored
config.json optional {"pages": [...]}
topics/*.md mockup/*.html data.md structure.md plan.md sessions/<id>.md
.profile name of the active profile (gitignored)
*.html generated, gitignored; index.html redirects to tasks.html
handoffs/ one .md per internal handoff (Handoffs page, part a) shared
README.md format and example, skipped by the build
Install in a project
- Copy
assets/dashboard/from this skill to<project>/dashboard/. - Handoff files: search the repo for files or folders named
*handoff*. If there is a live folder of handoffs between people, pointpaths.handoffs_dirindashboard/config.jsonat it (relative todashboard/) and add the frontmatter below to its files. Otherwise copyassets/handoffs/to<project>/handoffs/. - Merge
assets/hooks.jsoninto<project>/.claude/settings.json(UserPromptSubmit and Stop hooks). - Edit
dashboard/config.json:project,client. Paths default tocontent/tasks.md,content/milestones.md,content/resources.md,content/handoffs.mdand../handoffs. - One list: if the project has a
project-management.html(or another to-do page), move its open tasks tocontent/tasks.mdand its client or SoW dates tocontent/milestones.md, then delete it (git rm). Repoint anyCLAUDE.md, bot or agent instruction that named it todashboard/content/tasks.md. - Create the user’s own profile: copy
assets/profile/todashboard/profiles/<their-name>/, then fill or delete its files. Create other people’s profiles only as empty folders (mkdir dashboard/profiles/<name>). python3 dashboard/build.py --profile <their-name>, opendashboard/index.html.- Add to the project
CLAUDE.md: the dashboard is the only task list and bots and agents read and updatedashboard/content/tasks.md; what is shared vs private; the content rules below; whose profiles must not be filled; and that sessions fill## Summarybefore closing substantial work. - Commit only the shared files, including
handoffs/. The shipped.gitignorealready excludes*.html,.profile,profiles/andsessions/.
Keep build.py identical across projects; project content lives in Markdown. If a
page needs a layout the generator cannot express, add a mockup fragment. If you do
change the generator, update the copy in this skill too and keep --demo green.
Profiles
dashboard/.profilenames the active profile;build.py --profile <name>switches and rebuilds;--profile ""shows only the shared pages. An unknown name fails with the list of existing profiles.- Without a profile
config.json, a private page appears only when its file exists, so an empty profile shows just the shared pages. Aconfig.jsonwithpagesfixes the list and order. - The header shows
project · client · Profile. Switching removes the other profile’s generated pages. - A profile belongs to its person. Never write content into someone else’s profile, not even a helpful seed; they decide what goes there.
- Sessions are logged in the profile active when the session started and stay there if the profile is switched mid-session. A new session started while someone else’s profile is active logs into that profile: switch back first.
Shared pages
Tasks (content/tasks.md, content/milestones.md). tasks.md holds internal
due dates; milestones.md holds client or SoW dates (contract phases, client inputs,
payments). Never repeat a milestone as a tasks.md checkpoint.
## Tuesday · due 2026-10-13 · @name
- [ ] Define the evaluation metrics
- [ ] Draft the golden set
## Review checkpoint · due 2026-10-20 · @name @other
Each ## group is a dated unit with optional @owners; a group without tasks is a
checkpoint. [x] and [~] still render, but done and irrelevant tasks are deleted
(see Content rules).
## [x] Kick-off · due 2026-10-05
## Evaluation dataset approval · due 2026-10-23..2026-10-30
Otherwise our dataset is used
## Delivery · due 2026-12-04
€10,275 (30%)
In milestones.md, [x] marks a reached milestone, due A..B is a window (drawn as
a bar), and one optional note line goes under the heading (shown in the marker’s
tooltip). Headings without due are ignored. A missing file, or one without ##
headings, means no milestone row and no milestone KPI: right for projects without
fixed SoW dates.
The page has no title block: under the header (project · client · profile) it opens on
the KPI strip (next due with countdown, next milestone with countdown when there are
milestones, overdue, due in 7 days, tasks done), then the timeline with month marks
and a today line. Milestones sit on their own row above the internal groups as
diamonds in a separate colour (green when reached, grey when passed without [x]).
Internal groups get one marker each (circle = tasks, diamond = checkpoint; colour = on
track, due within 3 days, overdue, done). There is no card list under the timeline:
hovering, focusing (Tab) or tapping a marker opens a tooltip with the name, the date or
window and the note (for groups: date, owners and n/m done); Escape or a second tap
closes it. Then come the due-date cards sorted by date with progress bars. Countdowns and
the today line are computed in the browser, so the HTML stays deterministic. HTML
comments in both files are ignored, so the format hint can live at the top.
Keep the page read-only. In-browser editing (local server + PUT) was tried and dropped: it adds a server to run and a second write path.
Handoffs, two parts:
-
(a) Internal handoffs between people: one Markdown file each in
handoffs/at the repo root (paths.handoffs_dir, relative todashboard/, default../handoffs), namedYYYY-MM-DD-short-topic.md, with frontmatter:--- from: alex to: sam date: 2026-10-12 title: Review notes on the first draft status: open ---The page lists open handoffs newest first (date, from → to, title); a click opens the handoff IN PLACE, its body rendered from Markdown (
## headings, tables, nested-/1.lists with indented continuation lines,> quotes, code fences, bold,code, links), with a link to the source.mdat the bottom.status: donemoves one under a collapsed “Done” disclosure.README.mdis skipped. No files shows a short empty state. Write a handoff only when the user asks. -
(b)
content/handoffs.md: only “Waiting on” blockers, rendered as cards under “External blockers”.
Resources (content/resources.md). Free Markdown: an intro paragraph, then
## Section blocks with paragraphs and bullets, each rendered as a card. The intro of
handoffs.md is the Handoffs page lead and its sections follow the same format.
Missing or title-only file shows an empty state. Fill them only with what the user
gives you. Resources is a page, not a folder: never copy, move or symlink client
material (databases, code copies) into dashboard/; reference it by its workspace path.
Content rules
- Tasks: about 3–4 real tasks per person per week, similar ones merged. No chasing or
confirmation filler, no “if it comes up” items, no sub-project prefixes like
[GL]/[DPR]. - Done tasks are deleted from
tasks.md, not ticked and kept. Tasks that no longer matter are deleted, not ticked. - Client or SoW dates go in
milestones.md, internal due dates intasks.md; never both. content/handoffs.md: only real external blockers from the client, one short section. No “what we owe them”, no internal notes, nothing that is already a task.- One list: the dashboard replaces any
project-management.html; delete it on install. Bots and agents read and updatecontent/tasks.md.
Private pages (inside the active profile)
Write all content in English, even when the chat is in another language.
Topics (topics/<topic>.md): # Title, one scope line, one bullet per claim:
- [deciso] Exclude the client's current roster. — source: chat decision 2026-10-07
- [aperto] Does an exclusive deal block? (conflicts with the Armada case) — source: call 2026-10-05
Tags are fixed identifiers: deciso (decided with the client), stakeholder (said by
the client, not decided), proposta (proposed), aperto (open). (conflicts with …)
lands in Needs attention, (stakeholder answer: …) or (resolved: …) closes a
question, (superseded …) hides a claim. Never delete a claim. README.md in the
folder is always skipped, so it can document the format with examples.
Data (data.md): ## Source, then status: (available, partial, blocked,
unverified), optional verified: YYYY-MM-DD, owner: @name, access: …, a free
paragraph, bullets. Build it from the real sources, not from memory or docs:
- Query databases read-only with an absolute URI
(
sqlite3.connect("file:/abs/path.sqlite?mode=ro", uri=True)). A relative path from the wrong directory silently creates an empty database file. - One section per data domain (documents, templates, master data, permissions, …) with real counts and distributions, plus sections for the code that reads them (source registry, API routes, schema generation, SQL scripts). Note how code joins the data when it matters (e.g. a join on a text name instead of an id).
- Recount before writing a number; state masked values as masked.
- Surface surprises as
Open:in the session summary, not as decisions.
Structure (structure.md): intro, then ## Phase sections in flow order: phase
question, ### Input, ### Rules, ### Output. A phase named with “gate” or
“eligib” renders as a hard gate; optional ## Loop becomes the feedback box. Each
phase also gets its own page.
Plan (plan.md): ## Lane, optional description, GitLab task lists. [ ] open,
[x] closed, [~] n/a; #id on top-level todos only, next id = highest + 1, never
reused; @name, · opened, · closed, · due YYYY-MM-DD. Change it only when the
user explicitly asks, and only the items they name.
Mockup (mockup/<name>.html): one HTML fragment per tab, first line
<!-- title: Tab name -->, shared classes only (hero, card, grid2, kpis,
kpi, tag, label, muted, tile, masonry), no inline colours, no scripts,
placeholder data only.
Sessions (sessions/<id>.md): created by the hook with author from
git config user.name. Before closing substantial work, replace the ## Summary
placeholder with 2–5 bullets prefixed Decided: / Open: / Ingested / Researched
where they apply.
Working in a team
- Commit shared files only. Generated HTML is gitignored, so there are no HTML merge
conflicts; after a pull, run
python3 dashboard/build.py. - Decisions enter Topics only after the user confirms them; stakeholder statements get
stakeholder. Conflicts between sources are shown, never resolved silently. - No personal data: people by name and role, never emails or phones.
Checks
python3 dashboard/build.py --demoandpython3 dashboard/session_log.py --demoprintok; run both after any script change. The demo builds in a temp copy that excludescontent/,profiles/,sessions/and.profile, so real content never skews it; it covers missing and emptymilestones.md, milestones and windows on the timeline, and handoff files (open listed newest first, done collapsed, README skipped).- A build error never blocks the hook (stderr, exit 0). If a page is missing, run
build.pyby hand and read the error; check.profilenames an existing profile. - A claim that does not appear: tag spelling, the em dash in
— source:, the line starts with- [, and the file is notREADME.md. - A milestone that does not appear: the heading needs
· due YYYY-MM-DDand must not sit inside an HTML comment. A handoff that does not appear: the file ends in.md, is notREADME.md, starts with---frontmatter, and is notstatus: done.
Common mistakes
| Mistake | Fix |
|---|---|
Client or SoW dates as tasks.md checkpoints | Put them in milestones.md; tasks.md keeps internal due dates |
Ticked done items kept in tasks.md | Delete done and irrelevant tasks |
Filler tasks, “if it comes up” items, [GL]/[DPR] prefixes | 3–4 real tasks per person per week, similar ones merged |
”We owe them”, internal notes or tasks in handoffs.md | Client blockers only; internal handoffs are files in handoffs/ |
project-management.html left next to the dashboard | Move its content, delete it; one list in content/tasks.md |
| Seeding a colleague’s empty profile | Create the folder empty; it is theirs |
Copying or symlinking client data into dashboard/resources | Resources is a page; reference workspace paths |
Hardcoding page content in build.py | Put it in Markdown or a mockup fragment |
New module-level name in build.py shadowing an existing one (a TASK regex broke the Plan parser) | Prefix new globals per feature (TASK_LINE, TASKS_CSS) and run --demo |
Demo fixture built from the real content/ | Keep the demo’s ignore list; fixtures live in the temp copy |
sqlite3.connect("x.sqlite") from the wrong cwd | Read-only absolute URI |
$Q "select …" in zsh passing the whole string as one word | Use Python, or a shell function, for repeated queries |
| Adding todos the user did not ask for | Plan changes only on explicit request |
| Build timestamps or random ids in output | Keep output deterministic; dates relative to today go in browser JS |