305 lines
18 KiB
Markdown
305 lines
18 KiB
Markdown
# 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:
|
|
|
|
```bash
|
|
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. 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:
|
|
|
|
```bash
|
|
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.
|
|
|
|
If an active session expires or is revoked, 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. This recovery does
|
|
not clear offline state. **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. Bodies, comments, diffs, credentials, repository catalogs,
|
|
events, and complete API responses are excluded. A cold offline launch labels the
|
|
saved time and renders this snapshot read-only; opening live details, pagination,
|
|
and server mutations remain disabled until reconnection. **Clear offline work
|
|
data** deletes the snapshot, and opting out deletes it 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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`:
|
|
|
|
```bash
|
|
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 \
|
|
--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.
|
|
|
|
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. Full
|
|
behavior and safety gates are documented in
|
|
[`docs/release-engine-spec.md`](docs/release-engine-spec.md).
|