| .gitea/workflows | ||
| creative-deliverables | ||
| docs | ||
| frontend | ||
| scripts | ||
| src | ||
| tests | ||
| .gitignore | ||
| conftest.py | ||
| README.md | ||
| requirements.txt | ||
stackchain-dashboard
Live AI-driven adaptive UI for the Stackchain AI Lab Gitea experience.
Local quickstart
Python 3.11 or newer is recommended. Create an isolated environment and install the pinned dependencies:
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install -r requirements.txt
Point the dashboard at the Gitea server root (without /api/v1) and provide a
token that can read dashboard data, update the authenticated user's notification
threads, create and self-assign issues, discover, claim, and release issue assignments,
list repository labels and open milestones, set or clear due dates on assigned issues, create issue comments, close assigned issues,
inspect/comment on assigned pull
requests, merge assigned pull requests, and submit pull-request reviews.
Pull-request replies and mobile My Work issue and PR comments use Gitea's
issue-comment API; mobile issue capture requires issue
creation and assignment permission. The New issue sheet can optionally select an open
repository milestone and due date; the dashboard validates both and sends them with
self-assignment in the single create request, so planned work appears in its release
lane immediately. On a cold offline launch, Save for filing stores up to 20
account-bound title/description captures without selecting a repository or entering the
mutation outbox. Drafts marks them Needs filing; after a fresh reconnect confirms
the same Gitea login, Choose repository restores the capture to the normal planning
and durable delivery flow. A different or unconfirmed account can only copy or discard
the private content. Issue capture and authored mobile actions (issue
comments, pull-request comments, notification replies, and reviews) persist per-draft
idempotency keys, so retrying after a timeout, reload, process restart, or handoff to
another worker replays a confirmed result instead of posting duplicate content. The ordered,
five-item Today plan syncs across the operator's devices. After a healthy, fully paginated
My Work refresh proves that an item is complete or otherwise no longer eligible, Stackchain
queues an idempotent retirement before removing it locally; partial and degraded refreshes
leave the plan unchanged, and offline retirements replay after reconnect. When
Keep My Work available offline is enabled, every issue or pull request in the bounded
Today queue is warmed automatically after a healthy authenticated refresh and as soon as
it is added. The readiness indicator reports saved, pending, and retryable items; unchanged
updated_at revisions make no detail request, transient failures retain the prior copy,
and pull-request diffs remain online-only. Saved unread-update conversations remain
triageable offline: Queue read & next writes an account-bound, notification-ID-
deduplicated acknowledgement to the durable background delivery system, removes the
update from the local queue immediately, and opens the next saved conversation. A cold
offline reload suppresses acknowledgements still waiting to sync; reconnect uses the
authenticated notification-read endpoint and keeps transient failures queued. Installed-app
navigations are also deadline-bounded: after four seconds without a network response,
Stackchain aborts the request and opens the cached dashboard shell. If the shell has not
been installed yet, it returns explicit HTTP 504 reconnect guidance instead of hanging.
Closed-app
delivery requests are deadline-bounded: a stalled identity, CSRF, or mutation request is
aborted after 15 seconds, its durable claim returns to the queue, and the next sync retries
with the unchanged idempotency key. Device purge cancels an active drain before closing
private outbox storage. Results are coordinated through a bounded SQLite ledger. Set STACKCHAIN_STATE_DIR to a
persistent, writable service directory (or set STACKCHAIN_IDEMPOTENCY_DB to an explicit
SQLite path); the local default is .stackchain-state/idempotency.sqlite3. Ledger reads and
writes run outside the request event loop, and lock admission is bounded to 100 ms by
default. Tune it with STACKCHAIN_IDEMPOTENCY_LOCK_TIMEOUT_SECONDS; keep the value below
route deadlines. Reservation contention returns retryable HTTP 503 with Retry-After: 1.
If contention occurs after the upstream mutation, the dashboard fails closed with
Retry-After: 5 and asks the caller to verify the result before retrying. Direct API callers
should preserve the Idempotency-Key header with the unchanged route and payload until a
201 response is confirmed. Closing an assigned issue, native Comment,
Approve, and Request changes reviews, and assigned-PR merge require repository
write permission. Native Comment, Approve, and Request changes reviews support
head-scoped draft comments anchored to changed lines; the dashboard validates each
comment path and submits the summary, decision, and inline comments in one review
request. The dashboard rechecks the current pull-request head, CI success, draft
state, and mergeability immediately before every merge.
Serve the dashboard only to trusted users on its own origin; cross-origin API
access is intentionally disabled. Authentication defaults to fail-closed
operator mode. It requires two independent secrets of at least 24 characters;
missing, short, reused, or invalid configuration leaves /healthz available but
returns HTTP 503 before any Gitea access. The access token is entered at /login;
the browser receives only a short-lived signed session and CSRF proof. Generate
both values in your secret manager (for example, openssl rand -hex 32) and inject
them at runtime—never commit them:
export GITEA_URL='https://forge.example.com'
export GITEA_TOKEN='<read-notification-and-issue-write-token>'
export STACKCHAIN_DASHBOARD_AUTH_MODE='operator'
export STACKCHAIN_DASHBOARD_ACCESS_TOKEN='<operator-sign-in-secret>'
export STACKCHAIN_DASHBOARD_SESSION_SECRET='<independent-cookie-signing-secret>'
# Optional; defaults to STACKCHAIN_STATE_DIR/sessions.sqlite3.
export STACKCHAIN_SESSION_DB='/var/lib/stackchain-dashboard/sessions.sqlite3'
# Optional; defaults to eight hours.
export STACKCHAIN_DASHBOARD_SESSION_TTL_SECONDS=28800
# Optional sign-in throttle: five failures per five minutes, up to 10,000 sources.
export STACKCHAIN_LOGIN_MAX_FAILURES=5
export STACKCHAIN_LOGIN_WINDOW_SECONDS=300
export STACKCHAIN_LOGIN_MAX_ENTRIES=10000
# Optional; defaults to STACKCHAIN_STATE_DIR/login-attempts.sqlite3.
export STACKCHAIN_LOGIN_ATTEMPT_DB='/var/lib/stackchain-dashboard/login-attempts.sqlite3'
# Trust forwarding headers only from these immediate reverse-proxy networks.
export STACKCHAIN_TRUSTED_PROXY_CIDRS='127.0.0.0/8'
uvicorn src.main:app --host 127.0.0.1 --port 8000
Sign-in failures are scoped to a hashed canonical client address and persisted across
workers and restarts. Once the budget is exhausted, the server returns 429 with
Retry-After; the mobile login form disables retries for that interval. Expired
source records are pruned and the ledger is size-bounded. Keep its SQLite file on
shared writable storage. X-Forwarded-For is ignored unless the immediate peer is
inside STACKCHAIN_TRUSTED_PROXY_CIDRS; list only networks you operate. Without
that setting, a reverse proxy is safely treated as one shared source.
Inbound API mutations are admitted through a body-size boundary before FastAPI
parses JSON: sign-in is capped at 16 KiB and other POST, PUT, and PATCH
requests under /api/v1/ are capped at 64 KiB. Both declared and streamed bodies
are counted. Oversized requests receive a compact, non-cacheable HTTP 413 response;
validation errors expose field locations and messages but never echo submitted
values or validation context. Preserve these limits at the reverse proxy or enforce
equal or tighter upstream limits.
Each signed cookie includes an opaque session identifier whose hash and expiry are kept in the SQLite session registry. Keep that registry on persistent, writable storage shared by all dashboard workers. Operators name a device at sign-in and can open Active devices to review creation/expiry times, identify the current device, and revoke one remote session without interrupting other trusted devices. The API exposes only independent management IDs and bounded labels—never cookie values, session hashes, CSRF proofs, or source addresses. Existing two-column registries are migrated in place and their live sessions remain valid.
High-impact actions—merging a pull request, closing an assigned issue, revoking a remote device, or signing out every device—require the operator access token again. The server issues a random 90-second grant bound to the active session, exact action, and exact target. Only its digest is stored, and the grant is consumed atomically on first use. Expired, replayed, cross-session, and target-substituted grants fail before Gitea or session state is changed. The browser preserves the pending request and retries it once after the built-in mobile/keyboard-accessible authorization prompt.
If an active session expires, the first authenticated API rejection replaces the dashboard with sign-in and explains that private drafts remain on the device; signing in again resumes account-bound queued delivery. Expiry recovery does not clear offline state. A selectively or globally revoked device instead receives a bounded revocation reason: on its next server contact, Stackchain clears its owned local/session storage, private outbox database, and dashboard caches before enabling sign-in. Revocation blocks server access immediately, but no web application can erase a device that remains offline forever. Sign out & clear this device revokes only the current session before clearing browser state, so a copied cookie cannot be replayed afterward; other signed-in devices remain active. Sign out all devices is a separately confirmed lost-device safety action that atomically revokes every existing operator session before clearing the current browser and returning to sign-in. Registry read or write failures return a sanitized HTTP 503 before Gitea is contacted.
Terminate TLS at the trusted reverse proxy: session cookies are deliberately
Secure, HttpOnly, SameSite=Strict, and scoped to the deployment subpath.
Every response also defines the browser execution boundary with a Content Security
Policy that allows scripts only from the dashboard origin, denies framing and
plugins, and blocks unused browser capabilities. Keep these response headers when
proxying; do not add inline scripts or broaden script-src. The dashboard bootstrap
and stylesheet are same-origin static assets included in the offline PWA shell.
Use Sign out & clear this device on shared devices; it clears Stackchain's
offline snapshots, drafts, outboxes, background IndexedDB, and PWA caches without
removing unrelated forge preferences. Rotate either dashboard secret by replacing
the injected value and restarting the service; changing the signing secret expires
all sessions immediately. The signing key is never derived from the access token.
For direct, unproxied local development only, set
STACKCHAIN_DASHBOARD_AUTH_MODE=insecure-local and open
http://127.0.0.1:8000/. This mode checks the network peer and rejects non-loopback
clients; do not use it behind a reverse proxy, whose loopback connection would hide
the browser's peer address. To verify the backend and its Gitea connection in
operator mode, sign in and request http://127.0.0.1:8000/api/v1/context; a
successful response is JSON containing
user, repos, issues, and pull_requests. Press Ctrl/Cmd+K in the dashboard
to search commands plus issues and pull requests across every repository visible to
the configured Gitea token. Remote search starts after two characters, is debounced,
and keeps local commands usable if Gitea search is unavailable. Selecting a remote
result opens a mobile-safe, read-only preview without discarding the search query;
open unassigned issues can be claimed in place and handed into My Work after Gitea
confirms the assignment. Closed work and pull requests remain read-only with a safe
canonical Gitea link. The bounded APIs are available at
GET /api/v1/search?q=<query>&limit=<1-25> and
GET /api/v1/repos/<owner>/<repo>/issues/<number>/preview?kind=issue|pull. Never commit the token
or place it in a tracked configuration file.
For service monitoring, GET /healthz is a liveness check that confirms the
API process is running and does not contact Gitea. GET /readyz is the
readiness check: it validates the configured Gitea credentials and returns
HTTP 503 with an error when Gitea is unavailable or authentication fails.
The application-lifetime Gitea transport bounds aggregate upstream traffic and
coalesces identical concurrent GETs without caching completed responses. By
default, at most eight requests run at once, with one slot reserved for authored
mutations so polling and detail-read bursts cannot starve comments, reviews, or
other writes. Requests that cannot enter within 250 ms fail as retryable HTTP
503 responses. Tune these limits with GITEA_MAX_CONCURRENCY (minimum 2) and
GITEA_ADMISSION_TIMEOUT_SECONDS (minimum 0.001); keep the admission timeout
below the route deadlines. Streaming diff reads share the same read capacity,
while POST, PATCH, PUT, and DELETE requests are never coalesced.
Live snapshots expose independent context, events, and notifications
revisions. The browser sends its known revisions on later polls, so /api/v1/live
can omit unchanged section bodies while still returning current freshness and
retry metadata. The client retains omitted data and only rebuilds or persists the
sections that changed.
Offline mobile shell
At phone widths, a persistent bottom task dock keeps Work, Find, New, Search, and Drafts thumb-reachable. It reuses the existing sheets and My Work filter, preserves the selected release lane, shows the current draft count, respects the device safe area, and moves out of the way while a full-screen task is open. Desktop layout is unchanged.
My Work also has a local Later queue. Later today defers an item for four hours, while Tomorrow returns it at 09:00 in the device's local timezone. Deferred items leave normal and Attention queues without marking notifications read or changing any Gitea issue or pull request. They automatically return to their existing priority position at the wake time, and Bring back now restores them early. Later state is stored only in this browser, scoped to the confirmed Gitea login, and removed when fully loaded work confirms that an item no longer exists.
After one successful online load, the installed dashboard precaches a versioned,
subpath-scoped application shell. During a network outage or a dashboard HTTP
500, 502, 503, or 504 response, navigation falls back to that shell when
it is available so local issue and review drafts remain reachable. Authentication
and other client-error responses are never hidden. A visible, accessible offline
or server-outage notice distinguishes cached mode from live Gitea data, and the
dashboard keeps polling until it can restore its live snapshot automatically.
Users can explicitly enable Keep My Work available offline. Each healthy live refresh then stores a seven-day, versioned snapshot containing only the signed-in user identity and queue-card metadata for issues, pull requests, unread updates, and pagination totals. For each opened Today issue or pull request, Stackchain additionally retains an allowlisted detail record with its body and newest 20 comments. Requested-review records also retain the head SHA, CI state, newest 20 prior reviews, and at most 50 sanitized file previews with 400 diff lines per file. A previously opened unread update is retained by notification identity with allowlisted subject context and its newest 20 conversation messages. This account-bound cache is limited to ten records across all detail kinds; credentials, repository catalogs, events, raw patches, and complete API responses are excluded. A cold offline launch labels the saved time. Cached Today details open in the existing phone sheet, where comments can enter the account-bound durable outbox. For issue and non-review pull details, planning, assignment, review, merge, and close controls remain disabled until reconnection. Cached requested reviews open in the existing review sheet so file progress, notes, summary, decision, and inline-comment drafts remain usable under the saved head SHA. Review submission is never queued: reconnect reloads the current head before enabling submit; a changed head starts clean SHA-scoped progress while retaining the prior-head draft. Cached unread updates use the same phone conversation sheet and replies enter the account-bound durable outbox, while mark read, ownership, deferral, and older-message loading remain disabled until reconnection. Cards without a saved detail explain that reconnection is required. Clear offline work data deletes both stores, and opting out or seven-day expiry deletes them automatically.
API responses and mutations are never cached by the service worker. New issue captures,
issue comments, pull-request comments, and unread-update replies use bounded local
outboxes when connectivity or a retryable server failure prevents delivery. Issue
captures and authored messages are also mirrored into account-bound IndexedDB lanes
and registered with Background Sync, so a supporting installed browser can deliver
new issues, issue comments, pull-request comments, and unread-update replies after
every dashboard client has closed. The worker verifies the current Gitea login, shares
an atomic delivery claim with the foreground path, and preserves the original
idempotency key. Installed browsers can explicitly enable Notify me when queued
work finishes; permission is requested only from that user gesture and the choice
is stored for the confirmed account in the private background outbox database.
Successful deliveries produce privacy-safe receipts that open the created issue or
source conversation, while permanent validation failures open Drafts for recovery.
Authentication expiry during a worker mutation is recoverable: the worker releases the
claim without creating an Attention receipt, stops that drain, and keeps the original
idempotency key. Loading the authenticated dashboard after signing in asks the worker
to resume queued delivery automatically.
Notification text never includes issue titles, comment bodies, or validation details.
The preference is off by default, unsupported or denied browsers retain foreground
reconciliation, and Sign out & clear this device removes the account-bound choice.
Browsers without
IndexedDB or Background Sync keep the foreground reconnect behavior. Drafts shows
queued and needs-attention messages with explicit send/discard controls; reopening the
dashboard reconciles worker completions and permanent failures into the visible outbox.
Foreground delivery flushes messages sequentially with their original idempotency keys.
Tabs coordinate each queue through the browser lock manager (with an expiring local
lease fallback), so an installed PWA and a browser tab cannot submit the same operation
concurrently. Queue changes are broadcast to other tabs and refresh Drafts without
polling; an abandoned lease can be reclaimed after expiry. State-sensitive actions such
as reviews, merges, closures, labels, milestones, and assignments are never queued.
Service-worker upgrades are atomic and remove only older
stackchain-dashboard-* caches, preserving unrelated caches on the same origin.
Run the test suite with:
python3 -m pytest tests/ -q
Autonomous release worker
The deterministic issue-to-release engine lives at src/release_engine.py.
It discovers or targets one Gitea issue, verifies its claim, creates an issue
branch, invokes a configured coding agent, runs tests, pushes, and opens a
linked pull request. Merge remains gated by CI/review; the existing release
workflow drafts the release candidate after merge.
Plan against live Gitea without mutating anything:
python3 -m src.release_engine \
--repo stackchain/stackchain-dashboard \
--agent timmy \
--issue 14 \
--dry-run
Execute one ticket after implementing and committing its change directly on
the deterministic branch printed by --dry-run:
export GITEA_TOKEN='<scoped-token>'
export RELEASE_AGENT_COMMAND=true
python3 -m src.release_engine \
--repo stackchain/stackchain-dashboard \
--agent timmy \
--issue 19 \
--agent-timeout 1800 \
--test-timeout 900 \
--termination-grace 5 \
--agent-env AGENT_CONFIG_HOME \
--test-command 'python3 -m pytest tests/ -q'
GITEA_TOKEN needs issue and repository write scopes. The engine verifies the
claim, reruns the test command, pushes the branch, and opens a PR containing
Closes #19 plus test evidence. Replace 19 with the selected issue number;
do not run without --issue when processing a preselected ticket.
Agent and test commands use POSIX argument quoting and run as argument vectors,
not through a shell. Issue context is available to the agent only through
RELEASE_ISSUE_NUMBER, RELEASE_ISSUE_TITLE, RELEASE_ISSUE_BODY,
RELEASE_REPO, and RELEASE_BRANCH; do not put issue placeholders in command
text. Shell substitutions, redirects, and pipelines are intentionally not
interpreted. Put any trusted shell workflow in a reviewed wrapper script and
configure that script as RELEASE_AGENT_COMMAND instead.
Every child process receives an explicit least-privilege environment rather
than inheriting the worker's credentials. Coding agents receive a minimal
runtime environment plus the five RELEASE_* context values above. Tests and
Git receive only the minimal runtime environment, so GITEA_TOKEN and
unrelated parent secrets remain in the release-engine process. If an agent
runtime needs another non-secret setting, allow it explicitly with a repeated
--agent-env NAME option or the comma-separated RELEASE_AGENT_ENV variable.
GITEA_TOKEN, reserved RELEASE_* names, and malformed names are rejected
before the issue is claimed. Secret-bearing values must not be allowlisted.
Agent and test deadlines are independent. On expiry, the engine terminates the
command's entire process group, escalates from SIGTERM to SIGKILL after the
configured grace period, and records agent_timed_out or tests_timed_out
without advancing the release. The equivalent environment variables are
RELEASE_AGENT_TIMEOUT, RELEASE_TEST_TIMEOUT, and
RELEASE_TERMINATION_GRACE; every value must be positive.
Durable state defaults to .release-engine/state.json. Successful checkpoints
record the repository, issue branch, and exact commit SHA. A restart resumes at
tests, push, or PR creation only after validating that identity and a clean
worktree; a pushed checkpoint also verifies the remote branch SHA. Invocations
are serialized by an OS-backed, non-blocking lease at
.release-engine/run.lock under the canonical repository path. If an hourly
run overlaps a manual or slow prior run, the contender exits with release run already active and owner diagnostics before Gitea discovery or any Git/agent
work. The kernel releases ownership when the process exits, so never delete a
lock file to recover from a crash; stale file metadata cannot retain the lease.
Checkpoint replacement uses writer-unique, flushed temporary files and an
atomic rename, preventing concurrent writers from colliding or exposing partial
JSON. Full behavior and safety gates are documented in
docs/release-engine-spec.md.