Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

PTASK Triage Cockpit

A visually compelling, on-brand, live triage cockpit for PureTensor Task Intelligence (pt). Surfaces pending tasks and critical issues ranked by the server's composite priority_score, with four runtime-switchable themes.

Live: https://ptask.tail07f9ef.ts.net (tailnet-gated; no login)

What it is

A read-only Python stdlib sidecar that runs alongside the canonical pt serve. It reads the same ~/puretensor-tasks/tasks.db read-only (WAL ⇒ safe concurrent reads — zero risk to the canonical server) and delegates all writes to the pt binary, so the canonical mutation path is never bypassed.

No pip dependencies (stdlib only). No build step on the frontend (single self-contained index.html, vanilla JS + CSS custom properties).

Themes (switch top-right, persisted in localStorage)

Theme Look
Mission Control Dark telemetry ops board, teal→blue glow, dense (default)
Crystal / Glass Glassmorphism, frosted panels, gradient, crystal-cube motif
LCARS Star-Trek ops console, blocky amber/orange/lavender (rail collapses <700px)
Executive Light, minimal, whitespace

Domain split (v0.19 / pt 3.26)

The header domain switch is configured with PTASK_DASH_DOMAINS as comma-separated key[:Label[:ABBR]] entries. The first configured domain is the default unless PTASK_DASH_DEFAULT_DOMAIN selects another key. Every card wears a clickable domain chip that advances through the configured list by persisting an explicit domain: label through pt edit --label/--unlabel; the composer offers AUTO plus the same list.

With PTASK_DASH_DOMAINS unset or blank, the cockpit retains its legacy ALL / ENG / MGMT switch and deterministic classifier: explicit domain:eng/domain:mgmt label > project map > label map > title keywords > ENG. Switching remains a pure client-side re-render across every panel.

Design tokens (v2.6.8 design pass — see DESIGN_BASELINE.md)

Rules the 2026-07-06 audit added after measuring 276 contrast failures across the four themes (worst: Exec board — P3 HIGH lane label at 1.27:1, heatmap counts at 1.3:1; Mission statusbar/ages at 2.6-2.9:1):

  • --ink — the only text color allowed on saturated fills (priority badges/chips, severity pills, gradient buttons). Dark ink passes ≥4.5:1 on every fill and both --grad endpoints; white never did on P1/P2/P5/teal.
  • --p5t…--p1t, --accent-t, --hdr-muted, --live — text variants: same hue as the fill tokens but readable as text on the theme's surfaces. Dark themes alias them to the fills; Exec overrides with darkened versions. Any new "priority-colored text" must use --pNt, never --pN.
  • --heat-base — per-theme opaque base the heatmap cells blend against so cell ink can be luminance-picked (renderHeat), re-rendered on theme switch.
  • --dim must stay ≥4.5:1 on --bg2/--panel2; it is real content (ages, status, hints), not decoration.
  • Radii scale is 3-step: 14 (panels) / 10 (cards, inputs) / 6 (chips, badges, small buttons). Type floor is 10px. Control icons are the inline SVG sprite (#i-check etc.), never emoji; toast text may keep emoji.
  • Themes are costumes over the same bones: a skin may never cost correctness (contrast, overflow, touch targets) at any width — LCARS drops its rail and elbow padding below 700px for exactly this reason (320px reflow).

Interaction model (v2.7.x)

  • Event delegation, no inline handlers. Rendered controls carry data-act (+ data-id/data-uuid/data-n/data-p); two document-level listeners (click + Enter/Space) dispatch every action. There are no inline on* attributes in rendered HTML — the page is CSP-clean and the 15s re-render doesn't reattach hundreds of handler strings. Add a new control by giving it a data-act and a branch in the click delegate, never an inline onclick.
  • Focus return survives the re-render: overlays remember their invoker by a stable [data-act][data-id] selector and refocus the re-rendered twin.
  • Modal drawer. The detail drawer is a real aria-modal dialog — scrim dims + blocks the board, click-scrim / Escape / close-button all dismiss, Tab is trapped inside. Timeline points open it too (data-act=drawer).
  • Phone header (<640px): the five count chips collapse to one compact summary line (#chips-c); only one of #chips/#chips-c is displayed so screen readers read the counts once. The capture bar keeps its own full-width row — the dictation control is never the thing that gets shrunk.
  • The capture result card is a <div>, not a <section>. The phone shell hides every main > section but the active pane; the receipt for a task you just dictated has to survive that whichever tab you are on.

Layout

  • Header — crystal-cube mark, live UTC clock, count chips (crit / urgent / overdue / due≤7d / open), then the full-width capture bar: press to dictate, and the task is transcribed, classified ENG/MGMT and created in one round trip, with a result card carrying the PT-id and an Undo. Beside it the small ⌨ + button (or the n key) opens the task composer (title + description + severity + domain + optional deadline, with its own 🎤 speak-to-fill for the review path). Domain switch + theme switcher sit on the row below.
  • Critical Now — top tasks by composite priority_score (raw P-level as a badge), pulse/glow animation on newly-arrived criticals
  • Priority Lanes — P5→P1 columns, score-ranked within each, inline done button
  • Deadline Timeline — dated pending tasks on a horizontal NOW-anchored axis
  • Neglect Heatmap — pending tasks by priority × age bucket (the real "what's been sitting" signal)

Phone / home-screen app (v0.13, ≤640px)

Designed for Safari "Add to Home Screen" on an iPhone 17 Pro Max (440×956 CSS px, safe areas 59 top / 34 bottom): viewport-fit=cover + black-translucent status bar, safe-area padding on the header, tab bar, drawer and composer, a 180px apple-touch-icon.png and PNG manifest icons. The tailnet is the only access gate; the sidecar does not challenge the browser.

  • Two-row header: brand · crit/urg/overdue counts · + ; full-width ALL/ENG/MGMT switch. The theme switch moves into the More pane.
  • One pane at a time behind a fixed bottom tab bar — Critical (with a P5 badge) · Lanes · Recent · Review · More (timeline + heatmap + appearance). The pane and lane choice persist in localStorage.
  • Lanes show one priority at a time via a sticky P5…P1 picker; the page scrolls, nothing scrolls inside anything else. Recent loses its 340px box.
  • Detail drawer → bottom sheet; composer → top-anchored sheet with 16px inputs (no iOS focus-zoom); hover lifts are disabled where hover doesn't exist.
  • Resuming from the background refreshes and re-opens the SSE stream.

API (sidecar)

An edit failure stops processing before a requested priority change. The sidecar still delegates fields and priority to separate CLI invocations for compatibility, so a later pt priority failure cannot roll back an earlier successful pt edit. The Rust dashboard API applies the entire edit in one transaction.

Method Path Notes
GET /healthz systemd/tunnel probe
GET /api/config public dashboard title, domains, default domain, and version
GET /api/stats counts, throughput, overdue, due≤7d
GET /api/tasks?status=&limit= tasks + scoring fields + project + labels (v0.12)
GET /api/critical?limit= top pending by priority_score
GET /api/timeline pending tasks with a deadline
GET /api/heatmap priority × age-bucket matrix
POST /api/tasks/<id>/done shells pt done <id>
POST /api/tasks/<id>/snooze {days?} shells pt snooze <id> "<days> days" (v0.12)
POST /api/tasks/<id>/dismiss shells pt dismiss <id> (v0.12)
POST /api/tasks/<id>/reopen shells pt reopen <id> (v0.12)
POST /api/tasks/<id>/edit {title?, description?, priority?, deadline?, labels_add?, labels_remove?} shells pt edit (+ pt priority for level); null deadline clears (v0.12)
POST /api/tasks {title, description?, priority?, deadline?} shells pt add [--priority=] [--description=] [--deadline=] -- "<title>"
POST /api/voice (raw audio body) Whisper STT → Bedrock Claude draft → {transcript, fields:{title,description,priority,deadline,labels,domain,reason}} to pre-fill the composer
POST /api/voice/task (raw audio body) The same pipeline, then pt add — the header capture bar's one-press path. Returns {ok, pt_id, id, transcript, fields, stt, llm}. Rejects silence and Whisper artefacts with ok:false and creates nothing

Run locally

# against a copy of the DB (never the live one for dev)
PTASK_DB=/tmp/tasks.dev.db PTASK_DASH_BIND=127.0.0.1:9519 python3 server.py
# open http://127.0.0.1:9519/  (no login; production is reached only on the tailnet)

Config (env)

Var Default Purpose
PTASK_DB ~/puretensor-tasks/tasks.db SQLite path (opened read-only)
PTASK_BIN ~/.cargo/bin/pt pt binary for write delegation
PTASK_DASH_BIND 127.0.0.1:9510 bind address (loopback; production sets this to the tailnet)
PTASK_DASH_TITLE PTASK header, browser, and home-screen title
PTASK_DASH_DOMAINS (unset) comma-separated key[:Label[:ABBR]] list; blank keeps legacy ENG/MGMT mode
PTASK_DASH_DEFAULT_DOMAIN first configured key domain assigned to tasks without an explicit configured domain: label
PTASK_DASH_WWW ./www static dir
PTASK_ACTOR dashboard actor stamped on dashboard-originated pt writes
PTASK_STT_URL http://127.0.0.1:9000/transcribe voice STT endpoint (local Whisper); accepts -F audio=@
PTASK_VOICE_MODEL us.anthropic.claude-haiku-4-5-20251001-v1:0 Bedrock model for voice→task extraction
PTASK_VOICE_REGION $AWS_DEFAULT_REGION or us-east-1 Bedrock region (keyless, IAM via ~/.aws)
PTASK_AWS_BIN /usr/local/bin/aws aws CLI path for Bedrock invoke
PTASK_VOICE_FALLBACK_URL http://127.0.0.1:8600/v1/chat/completions local vLLM fallback if Bedrock errors
PTASK_VOICE_FALLBACK_MODEL nemotron-lightning fallback model id

Browser verification

Two by-hand end-to-end checks (not in CI — each needs a browser download). Both boot their own sidecar on a spare loopback port, so neither touches the live dashboard.

dashboard/tests/e2e/severity-order.sh # the board renders severity-ordered
dashboard/tests/e2e/domains.sh        # configured-domain tenant switch

severity-order.sh serves a copy of the task DB and asserts, in a real Chromium, that the Critical panel never shows a lower severity above a higher one, that /api/tasks returns severity-first by default, and that each priority lane holds only its own band. Run it after touching TASK_ORDERS, q_tasks, or the board's render path.

Deploy

# One-time: create a clean deployment worktree. Do not serve from the active
# development checkout at ~/ptask; branch switches there must not change live code.
git -C ~/ptask fetch origin main
git -C ~/ptask worktree add -b deploy/ptask-dashboard \
  ~/worktrees/ptask-production origin/main

# Later releases: fast-forward only after the canonical main commit is merged.
git -C ~/worktrees/ptask-production pull --ff-only origin main

# optional per-instance knobs (title, domains, bind, PTASK_ACTOR) — not a login secret
# ~/puretensor-tasks/.dashboard.env is still read if present; PTASK_DASH_PASS is ignored.

# user service
cp ~/worktrees/ptask-production/dashboard/ptask-dashboard.service \
  ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now ptask-dashboard
loginctl enable-linger "$USER"          # survive logout
curl -s localhost:9510/healthz          # -> OK

Published as a Tailscale Service at https://ptask.tail07f9ef.ts.net. Serve terminates TLS and reverse-proxies plain HTTP to this sidecar. Network reach on the tailnet is the access decision.

Rollback

systemctl --user disable --now ptask-dashboard
rm ~/.config/systemd/user/ptask-dashboard.service
# the Tailscale Service is the remaining publish path

The canonical pt serve and tasks.db are never modified — nothing to revert there.

Notes on data honesty

  • Ranking uses composite priority_score, not raw P-level (P5 is inflated: 48 tasks).
  • Neglect is derived from task age (created_at); the DB's score_neglect column is currently unpopulated.
  • The heatmap groups by priority × age bucket because cluster_keywords are currently stopword noise, not useful topic labels.
  • Dependency DAG is deferred to v2 (depends_on is empty across all tasks).

Version

  • v0.21.0 — Tailnet is the only gate. Removed the login shell, session store, /api/auth/*, HTTP Basic, failed-login throttle, Face ID unlock (face-unlock.js and its tests), and the non-loopback password requirement. /login and /logout redirect to /. CSRF Origin checks remain for state-changing POSTs and accept https://ptask.tail07f9ef.ts.net. PTASK_ACTOR is unchanged. Leftover PTASK_DASH_PASS / PTASK_DASH_USER / PTASK_DASH_SESSION_STORE / PTASK_DASH_SECURE_COOKIE env vars are ignored.

  • v0.20.1 — The login throttle keyed on the TCP peer, which behind the cloudflared tunnel is the connector for every internet client: five bad guesses from anyone locked the operator out. Cf-Connecting-Ip is now believed from PTASK_DASH_TRUSTED_PROXIES only (the v0.17.1 note below described this as already true; it was not). A JSON body that is not an object (or not UTF-8) gets a 400 instead of a dropped connection. A date-only deadline is due all day (no "0d over" on the due date). /api/stream emits change when the journal grows, not on a blind 15 s tick.

  • v0.17.1 / pTask v3.22.1 — The failed-login throttle is bounded. Its key is the caller's own address (Cf-Connecting-Ip behind Cloudflare), so a host with a routed IPv6 /64 could mint unlimited keys, and the table was swept with an O(table) rebuild on every attempt — enough for one client to pin the dashboard's event loop. The table now caps at LOGIN_MAX_RECORDS, sweeps on a 30 s timer instead of per request, and evicts unlocked clients before locked ones so a flood cannot forgive a lockout. Separately, hmac.compare_digest raises on a non-ASCII str, which turned a wrong password containing an accent into a 500; both comparisons now encode first.

  • v0.17.0 / pTask v3.22.0 — Face ID fixes from the 2026-09-03 adversarial review. The enrol checkbox ships checked and only its wrapping label is hidden, so gating enrolment on box.checked alone re-enrolled on every password sign-in — minting a fresh platform passkey each time and orphaning the previous one in the keychain with no way for the app to remove it. Enrolment is now gated on an enrolOffered flag set from isSupported(), so visibility and intent cannot disagree. A Forget Face ID on this device control was added: a passkey deleted on the device is reported by WebAuthn as an ordinary cancel, by design, so nothing automatic can recover it and the record would otherwise stay valid forever with enrolment never re-offered. The recovery code list moved into the shared core (RECOVERABLE_CODES / isRecoverable) because the three wirings' hand-written copies had drifted. The core now refuses a degenerate PRF secret (must be exactly 32 bytes and not constant). Tests: 22 contract + 6 markup + a 24-check e2e, with new cases for the re-enrol regression, the forget control, and two properties that were previously unfalsifiable — deleting userVerification: "required" or the rpId binding check used to leave every test green.

  • v0.16.0 / pTask v3.21.0 — Face ID unlock, replicated from pureKEY (forzieri). On a device with a platform authenticator the auth gate offers Remember this device with Face ID; a WebAuthn PRF secret (rpId = the serving hostname, user verification required, resident key) derives a non-extractable AES-GCM key that wraps the dashboard password in IndexedDB, and the next visit shows Unlock with Face ID instead of the keyboard. The server surface is unchanged — Face ID replays the same password to the same POST /api/auth/login, with the same 250 ms delay, 5-failure lockout and opaque ptask_session cookie. The enrolment is refused, never repaired: a different hostname or origin, an unknown record version, a tampered wrap, or a 401 from the login route drops it and the password becomes the only way in; a 429 or an unreachable server keeps it. www/face-unlock.js is the crypto core, vendored byte-identical into pNOC and pSCOPE, with 16 contract tests in tests/face_unlock.test.mjs. Storage is IndexedDB, never localStorage. Verified end to end by tests/e2e/run.sh, which boots a throwaway sidecar and drives a real Chromium with a CDP virtual platform authenticator through enrolment, unlock and the 401 refusal (kept out of CI only because it needs a browser download — run it after touching the auth gate). Not available on the Rust pt serve dashboard route, which still authenticates with HTTP Basic and so has no login endpoint to replay a password to.

  • v0.15.0 / pTask v3.20.0 — dictation-first capture. A full-width record bar is the first control in the header on every width (the typed new task… box is gone; the composer is now the + button beside it, or the n key). One press records, and POST /api/voice/task transcribes, classifies and creates the task in a single round trip — the result card below the header shows the PT-id, the ENG/MGMT hemisphere with the model's reasoning, priority and deadline, and offers Undo (a reversible dismiss, never a hard delete). The extractor now also returns domain and reason: an explicit hemisphere is written as a domain:eng/domain:mgmt label so the classification is a stored fact, and when the model declines to choose, the cockpit's existing domainOf() heuristic decides as it always has. A transcript guard rejects silence, sub-three-word fragments, non-Latin drift and known Whisper idle-hallucinations before anything is written — the composer path could rely on a human reviewing the draft, the create path cannot. The local extraction fallback pointed at 127.0.0.1:8772 / mistral-medium-3.5, a seat that had stopped listening, so a Bedrock outage degraded silently to a raw-transcript title; it now targets the sovereign nemotron-lightning seat on 127.0.0.1:8600 with the reasoning_effort: "none" that model requires, and raises instead of returning empty content.

  • v0.14.1 / pTask v3.17.1 — production now runs from a dedicated, fast-forward-only worktree instead of the active development checkout or the archived standalone dashboard repository. This prevents branch switches and merge trains from changing the live cockpit source.

  • v0.14.0 / pTask v3.17.0 — PT-VE login parity: the public shell presents a branded password form instead of a browser Basic-auth prompt, exchanges the dashboard password for an opaque 24-hour HttpOnly session, persists only session-token hashes across restarts, supports explicit logout, and throttles repeated failures (five attempts, five-minute lockout). Existing Basic auth remains compatibility-only for non-browser consumers and is never challenged.

  • v0.9.1 — state-changing sidecar routes reject browser requests whose Origin does not match Host, preventing cached Basic credentials from authorizing cross-origin task or voice mutations. Header-free clients such as curl remain compatible.

  • v0.9.0 — provenance re-classified by intent, not by which process ran the INSERT. Two classes: robot (auto-generated — distilled, incident, subtask_promotion, specola) and human (everything else: the operator typed/dictated it, OR Claude Code/HAL created it on the operator's request — manual/voice_memo/telegram/claude_code/mcp/ remote-cli; unknown → human). This flips claude_code+mcp+remote-cli from robot to human, so the board is no longer all-bot. Flux added_machine renamed added_robot; ROBOT_SOURCES (Rust/Python) + AUTO_SRC (JS).

  • v0.8.0 — the flux chip is now a range picker: click it for a dropdown of 30m / 1h / 6h / 24h / 7d. /api/stats returns flux.by_window (added, added_human, added_machine, done for every window) plus flux.windows, so the cockpit switches range as an instant local re-render off the last payload — no refetch. Selection persists in localStorage. Both servers changed (Rust pt serve 3.3.0 + Python sidecar 0.8.0); the flat added_24h* fields are replaced by the windowed object.

  • v0.7.0 — provenance + flux: every card wears a subtle PT-xxxx tag with an origin glyph (person = operator-entered manual/voice_memo/telegram, bot = machine-generated: claude_code, mcp, distilled, incident, …); the drawer spells the origin out. Header gains a +N/−N 24h flux chip (tasks added vs completed in the last day, added split operator/machine in the tooltip) backed by new /api/stats fields added_24h, added_24h_human, added_24h_machine, done_24h in both servers.

  • v0.6.1 — accessibility polish: interactive task titles in Critical Now and Priority Lanes now expose at least a 24px hit target with visible focus rings.

  • v0.6.0 — 🎤 speak-to-fill voice capture in the composer. A mic button records (MediaRecorder) and POSTs the clip to POST /api/voice, which runs the fleet's local Whisper (large-v3-turbo, STT) then AWS Bedrock Claude Haiku 4.5 to draft a clean title, description, severity (P1–P5) and deadline from the spoken note — the operator just reviews and hits Create. Bedrock is keyless (IAM via ~/.aws); local vLLM is the fallback. Mic needs a secure context, so the UI guides http-IP users to the HTTPS host. STT/LLM endpoints are env-overridable.

  • v0.5.0 — full task composer replaces the one-line quick-add: the header control now opens a modal with title, description, a five-pill severity selector (P5…P1), and an optional deadline. POST /api/tasks accepts {title, description?, priority?, deadline?} and shells them to pt add as explicit --priority=/--description=/--deadline= flags with a -- title separator (hyphen-safe; inline @label/#project/~2h on the title still parse). Fast capture preserved — type a title, Enter, ⌘/Ctrl+Enter to create.

  • v0.4.0 — direct severity picker + done-confirmation dialog on the dashboard.

  • v0.3.0 — scrollable priority lanes surface every pending task (no +N dead-end).

  • v0.2.1 — clamp API limit query parameters so negative SQLite limits cannot become unbounded reads; dashboard unit tests now run in CI.

  • v0.2.0 — promote/demote priority controls (▲/▼ steppers) on Critical-strip and lane cards. New POST /api/tasks/<id>/priority {level:1..5} endpoint shells pt priority <id> <level> (requires pt ≥ 1.2.0), which re-runs scoring so the composite ordering updates immediately.

  • v0.1.4 — Critical Now strip shows the top 12 by composite score (was 9).

  • v0.1.3 — fail closed on non-loopback binds without auth, add security headers, cap POST bodies, harden the systemd user unit, and add accessibility landmarks / reduced-motion handling.

  • v0.1.2 — fix done buttons to use pt_id (PT-N) rather than task UUID.

  • v0.1.1 — proper LCARS elbow-frame styling (shoulder headers, pill rail, blocky asymmetric panels), scoped so the other three themes are unchanged.

  • v0.1.0 — initial triage cockpit (4 themes, live poll, critical strip, lanes, timeline, neglect heatmap).