Web UI refresh behavior
The dashboard (web/app.js) re-reads data on a timer, after navigation and
after a mutation. Every one of those paths updates the page through one shared
mechanism, so a refresh touches only the parts of the DOM whose data changed.
This chapter lists every view and refresh path, what each can change, and the
rules the mechanism follows. scripts/test_ui_refresh.mjs verifies them in
headless Chrome.
The mechanism
- Idempotent setters.
setText,show,setClass,setAttr,setPropandsetDatawrite only when the value differs.setStateleaves an identical status panel alone. Setting a value that is already there still counts as a mutation to aMutationObserver, so the guard matters. reconcile(parent, descriptors)renders every keyed list. A descriptor is{key, sig, build}:keyidentifies the item (an id),sigis a string that changes exactly when what is displayed changes, andbuild()creates the node. A node whose key and signature match is kept. A node whose signature changed is rebuilt and replaced in place. New items are inserted, missing items removed, and nodes are moved only when their order changed.patchSection(container, signature, build)does the same for a region that is not a list (the lease card, the repository binding snippet).- State carried across a rebuild. When a node is replaced,
captureUiandrestoreUigive the new node the focus, text selection, open<details>and scroll offsets of the old one, matched by position inside the node. - Signatures describe what is shown. A list row’s signature is its data. A section with buttons also includes the actor and any other value its buttons read. The lease card’s signature is its rendered text, so a countdown that stays within the same displayed minute changes nothing.
- Refresh gate.
refreshBlocked(region)stops a background refresh (poll, auto-refresh or an attachment reload) while the tab is hidden, a mutation or upload is pending, a dialog is open, or a text field, menu or file chooser inside the region being refreshed has focus. A response that arrives after the gate closed is discarded. Fields outside the region, such as the search box above the task list, never block it because the refresh does not touch them. - Visibility. The 5-second timer is cleared when the tab becomes hidden. When it becomes visible the dashboard refreshes the current view once and restarts the timer.
- Loading panels. A “Loading…” panel appears only for a view that has nothing to show yet (first load, or a different project, task or filter). A reload of what is already displayed keeps it on screen and patches it.
The mechanism is vanilla JavaScript. No framework, no Alpine.js build and no
other dependency was added, and the Content-Security-Policy is unchanged.
Rendering still uses textContent and DOM construction only; no data reaches
innerHTML.
The freshness labels (Updated h:mm in the top bar, Read h:mm on Knowledge &
evidence) have minute resolution and change at most once a minute. The top bar
is outside every view.
Refresh paths
| Path | Trigger | Views |
|---|---|---|
| Poll | 5-second timer, gated by refreshBlocked | Overview, Task queue and Completed (only with Auto-refresh on), Task detail, Access (credentials), Resources |
| Load | Navigation (sidebar, brand button, tabs, “Open tasks”, settings gear and its sub-pages), project or filter selects, Refresh buttons, Load more | Every view |
| Post-mutation | The success callback of startMutation | Overview (project created, policy saved), Task queue (task or objective created, task deleted or canceled), Completed (archive), Task detail (every task action, review, recovery, upload), Reports (resolve), Knowledge & evidence (record saved), Resources (created), Access (credential, operator, clock, restore), dialog lists (browser sessions) |
| Visibility | visibilitychange | The current view (one refresh on becoming visible) |
| Navigation | showView | Selects and tab state; the destination view’s load path |
Views
“Before” is what the view did on every refresh before this change.
| View | Loader and trigger | Before | What can change | Now |
|---|---|---|---|---|
| Overview: summary cards | renderSummary after any project or task load | Cleared and rebuilt four cards | The four counts | Cards keyed by label; a card is rebuilt only when its number changes |
| Overview: project cards | loadProjects: load, Refresh, poll, post-mutation | Cleared and rebuilt the grid when the list changed; the loading panel flashed on every manual load | A project’s name, repository, branch; projects added or removed | Keyed by project id, signature is the project record |
| Project selects (tasks, archive, knowledge) | fillProjectSelect, also on every entry to Tasks | Cleared and rebuilt all options | Project names and membership | Options keyed by value; the selected value is set only if it differs |
| Project settings | openProjectSettings, loadIntegrationOwner | Rewrote heading, binding snippet and owner line, blanking the owner line while it re-read | Project name, repository, branch, integration owner and policy revision | Text set only on change; binding snippet is a signed section; the owner line is not blanked for the project already shown |
| Task queue | loadTasks: load, Refresh, poll (Auto-refresh), post-mutation, search, status filter, sort, page size | Cleared and rebuilt the page of rows when anything in the list changed | Any task field; tasks added, removed or moved between pages | Rows keyed by task id with the record as signature; page buttons keyed by index |
| Completed tasks | loadCompletedTasks: tab, Refresh, poll, archive, sort | As the queue | Same, plus tasks leaving by archiving | Same rows, keyed by task id |
| Archived tasks | loadArchivedTasks: tab, project select, Refresh, back from detail | Hid the list and showed “Loading” on every load, then rebuilt it | Archived task fields; tasks restored | Cached rows stay while reloading; keyed by task id; skipped when the response is unchanged |
| Task detail: heading, status, brief | loadTaskDetail: open, poll, post-mutation | Rewrote every field, and hid the page with “Loading” after a mutation | Title, description, revision, status, kind, human-delivered flag | Each field set only on change; the page is blanked only when a different task opens |
| Task detail: acceptance criteria | same | Cleared and rebuilt | Edited criteria | Items keyed by position |
| Task detail: action buttons | renderTaskActions | Cleared and rebuilt every button, so a button could vanish under the pointer | Which actions apply: lifecycle, ownership, blocker, objective links, actor | Buttons keyed by action; signature is only the fields the buttons read, so a new checkpoint leaves them alone |
| Task detail: ownership and lease | renderLease | Cleared and rebuilt | Owner, state, lease minutes, expiry, generation | Signed section; signature is the rendered text |
| Task detail: lifecycle changes | renderLifecycle | Cleared and rebuilt | New lifecycle events or replacements | Keyed by event id and replaced task |
| Task detail: checkpoint trail | renderCheckpoints | Cleared and rebuilt every checkpoint | New checkpoints (existing ones are immutable) | Keyed by checkpoint id |
| Task detail: jobs and holds | renderJobEvidence | Cleared and rebuilt | Job state and observation, reservation state | Keyed by job and reservation id |
| Task detail: submission and workflow | renderWorkflow | Cleared and rebuilt, closing open details | Submission, activity state, blockers, integration-blocked report, next actions | Candidate, each activity, blocker, integration-blocked notice and next action keyed separately |
| Task detail: attachments | loadTaskAttachments once per task; quietly on each poll | Rebuilt on each load | Files added or finalized | Keyed by artifact id; a poll reloads quietly and keeps what is shown on error |
| Integrator reports | loadReports: open, kind, check, repository, target branch and task filters (each change reloads page one), Refresh, Load more, resolve | Cleared the list and showed “Loading” on every load | A report’s state or resolution; new reports | Cards keyed by report id; open “Report details” survives, even for a rebuilt card |
| Attention digest | loadDigest: open, Refresh | Cleared and rebuilt | The three section lists | Three section cards keyed by section |
| Autonomy | loadAutonomy: open, window select, Refresh | Cleared and rebuilt | Any figure of a card; a source that fails shows its error in its own card | Cards keyed by section; signature is the card’s source data |
| Project card autonomy summary | loadAutonomySummary after each project load, at most once a minute per project | Not shown | Counts, last canary, today’s cost | Text patched in place; the card is not rebuilt |
| Connected sessions | loadAgentSessions: open, window select, Refresh | Cleared and rebuilt | A session’s activity, held attempts and their ages; host, harness and activity filters re-render the loaded list without a request | Cards keyed by session id |
| Knowledge & evidence | loadShared: open, selects, filters, tag chips, Refresh, Load more, search, post-mutation | Cleared and rebuilt, discarding history a person had expanded | Record fields, tags and scope; the rules text of a context search | Cards keyed by kind and record id; expanded history survives while the record is unchanged. A changed project, record kind or filter (tag, kind, status, include-shared; or search text, task, component, environment, version) starts a fresh list; Load more keeps the filters of the list on screen |
| Resources | loadResources: open, Refresh, poll, Load more, create; renderResources: search, sort, free-capacity filter | Cleared and rebuilt every card | Held units, description; resources added | Rows keyed by resource id; the Available column is derived (capacity − held), so it changes with its row |
| Access: credentials | loadCredentials: open, Refresh, poll, post-mutation | Cleared and rebuilt every row | Revocation, rotation, new credentials | Rows keyed by credential id (signature also records whether the row offers a replacement) |
| Access: operators | loadOperators: open, Refresh, Load more, post-mutation | Cleared and rebuilt | Role, enabled, revision | Rows keyed by operator id |
| Access: server clock, restore status | loadClock, loadRestore: open, Refresh, post-mutation | Cleared and rebuilt | Clock incident; restore progress and holds | Status lines, evidence and holds keyed separately |
| Dialogs | Built once per open | n/a | Lists inside a dialog (browser sessions, objectives) reload after Load more or a revoke | Rows keyed by id, so revoking one session rebuilds only that row. Dialog forms are never re-rendered. Task history replaces its list when the record type changes, because that is a different dataset |
Verification
node --check web/app.js checks syntax. scripts/test_ui_refresh.mjs runs
Chrome against a fixture server and, per view, requires:
- an unchanged refresh by button, poll, navigation away and back, or finished
mutation records no
MutationObservermutation inside the view; - changing one item rebuilds only that item’s node, and inserting or removing one item leaves the others in place;
- focus, selection, open
<details>, scroll position and an open dialog survive; - polling is off while the tab is hidden, resumes on becoming visible, and does not refresh around an open dialog, a pending mutation or a focused field in the region.
Run it with CHROME_BIN=/path/to/chrome node scripts/test_ui_refresh.mjs. Set
CHROME_ARGS=--no-sandbox where Chrome’s sandbox is unavailable.
scripts/test_artifact_preview.mjs covers the evidence preview dialog (JSON,
Markdown, oversized, non-text and malicious-content fixtures) and runs the same
way.
Resource search, sort and free-capacity filter
The Resources toolbar searches keys and descriptions (case-insensitive), sorts
by key, capacity, held or available units (most first; ties by key), and can
hide fully held resources. The table’s Available column is capacity minus
held units. These controls work on the resources the page has loaded. The first
time a search, a sort other than key, or the filter is used, the page fetches
the remaining pages 200 at a time, and it stops at 2,000 resources. When more
exist, a note under the toolbar says that only the first 2,000 are searched and
Load more is hidden. Rendering goes through reconcile(), so an unchanged
refresh records no DOM mutation. scripts/test_resource_search.mjs covers the
controls, the column, the mobile layout and the cap; run it like
scripts/test_ui_refresh.mjs.
Address history
Showing a view records its route in the address hash with history.pushState,
unless the hash already names it. A hashchange (Back, Forward, or an edited
link) applies the route without recording another entry. After sign-in the
current hash is applied once projects load; a route naming an unknown project
or page falls back to the overview and replaces the hash. Dialog state is not
part of the route. scripts/test_deep_links.mjs clicks through ten views,
walks back and forward through them, and opens a task link directly; it runs
the same way as scripts/test_ui_refresh.mjs.
Knowledge filters
scripts/test_knowledge_filters.mjs verifies the Knowledge & evidence filters in
a fixture-backed browser: the lessons tag, kind, status and include-shared
controls and the context task, component, environment and version inputs send
the matching GET .../knowledge and GET .../context parameters (empty values
are omitted), tag and scope chips render as text and a tag chip applies its tag
filter, and any filter change restarts pagination while Load more stays
within the displayed filter. It runs the same way as
scripts/test_ui_refresh.mjs.
Resource history
scripts/test_resource_history.mjs covers the Resources view’s History dialog
against a fixture trail: current holds, released and resolved entries, Load more
(one request per open and per click), task links into their own projects, empty
and failed trails, and the phone-width layout. It runs the same way, and also
honors CHROME_ARGS.
Job observations
Each job on the task page has a View observations button. It opens a dialog
that reads the task’s job_observations history 200 records at a time, keeps
only the records whose job_id is that job, and lists them newest first. Search
covers the summary, state and exit code; the state filter lists the states
loaded so far. A summary cleared by retention (payload_compacted_at) is
labeled as such. History is served oldest first and includes the observations of
every job on the task, so Load more is user-driven.
Loading stops, with a visible message and no Load more, at 2,000 task
observations or 2 MiB of records, whichever comes first. The records read are
the oldest ones, so a task with more history than the cap omits its newest
observations; use the CLI history for those. Download JSON saves the loaded
observations that match the current search and state as project_id, task_id,
job_id, retrieved_at (when the last page was read), the active filters,
load_limit_reached, and observations. Only the documented observation fields
are exported, and the object URL is revoked after the download. Closing the
dialog changes nothing on the task page. scripts/test_job_observations.mjs
verifies job filtering, search, the cap and the downloaded content; it runs the
same way as scripts/test_ui_refresh.mjs.
Submission comparison
The Submission & completion card on the task detail page has a Compare with
previous submission button. It opens a dialog that requests the first page
(at most 50 records) of the task’s submissions and reviews history; Load
more records fetches the next page only when chosen. The latest submission is
compared with the one before it, and two selects change the pair. Summary,
source revision, tree, checkpoint ref, handoff, acceptance evidence (matched by
criterion text), artifact IDs and the findings of changes-requested reviews are
shown side by side. Each row carries a text marker (Added, Removed, Changed or
Unchanged) in addition to its tint, long fields are clipped, and everything is
inserted as text. With a single submission the dialog says there is nothing to
compare. scripts/test_submission_compare.mjs covers these cases and runs the
same way as scripts/test_ui_refresh.mjs.
Status indicators
scripts/test_status_indicators.mjs verifies the task status badges in a
fixture-backed browser: queued, active and lapsed reviews and live In Progress
work in both the task list and the task detail, live updates without a reload,
tooltips on focus, and reduced motion. Run it with
CHROME_BIN=/path/to/chrome node scripts/test_status_indicators.mjs.
scripts/test_integration_status.mjs covers the integrator’s activity on a
waiting_integration task, in the same fixture-backed way. The badge reads
Verification Queued, Verifying, Checks Passed, Checks Failed, Verification
Unknown, Publishing or Publication Unconfirmed; only Verifying and Publishing
animate, and the tooltip names the required-check counts and says no agent owns
the work. The status filter and counts still follow work_status. A row links
the integrator’s result revision to its checks page through the forge-link
helper only for a recognised repository and a full revision, and the link does
not open the task. Evidence whose stage does not belong to the task’s status
(done, blocked, ready again) shows nothing. The task details add an
Integrator entry with the candidate and result revisions, the integrator’s
last report time and each required check’s state. Unchanged refreshes make no
DOM mutation and a transition repaints only its own row. Run it with
CHROME_BIN=/path/to/chrome node scripts/test_integration_status.mjs.
Blockers summary
The task queue opens with a Blockers card. It groups the loaded queue by the
precondition each task carries (a human decision, a saved blocker, unfinished
prerequisites or required children), by recovery_required, and by the review
and integration statuses, and adds the open integrator reports that need a
person and the decisions the attention digest says will proceed on their own.
Each group shows its count and up to ten items. An item opens the task, the
integrator reports or the decisions list; Show all applies the matching
status filter. Every queue load adds one digest request and one open-reports
request, however many tasks the project has, and never fetches task detail. A
source that fails is named in the card, so a failure never reads as “nothing is
waiting”. The card renders through the reconcile() and setState()
helpers, so an unchanged poll makes no mutation. scripts/test_blockers_summary.mjs
covers each group, the links, the empty state, the request budget and the
one-column mobile layout; run it like the other browser tests with
CHROME_BIN=/path/to/chrome node scripts/test_blockers_summary.mjs.
Forge links
Task details link a source revision to its commit page and a checks link
only when the project’s repository is a GitHub HTTPS or SSH address
(https://github.com/owner/repo(.git), [email protected]:owner/repo.git or
ssh://[email protected]/owner/repo.git) and the revision is 40 or 64 hex digits.
This covers the submission’s source revision, publication intent and result
revisions and a job’s source revision. Any other host, a malformed revision, or
a repository URL carrying a user name other than the SSH git user, a
password, a port, a query or a fragment is shown as plain selectable text.
External links open with target="_blank" and rel="noopener noreferrer".
Each check job ID in a publication result is a button: it focuses that job’s
entry in the Jobs & resource holds card, or opens the activity task that owns
the job and focuses it there. scripts/test_forge_links.mjs covers these cases
and runs the same way as the other browser scripts, with
CHROME_BIN=/path/to/chrome node scripts/test_forge_links.mjs.
Artifact integrity
Beside each artifact download, the task detail and Knowledge & evidence show
what the service records about it, from the record alone: an availability badge
(Available, Expired, Deleted, Unavailable or Not finalized), media type, size,
the SHA-256 (truncated, with a Copy SHA-256 button that copies the full
digest), and either Retained until <date> or Pinned. An unavailable
artifact has no download link and states why, including the deletion_reason
when one was recorded. An external link is labelled unverified because the
service checks neither its availability nor its checksum. No artifact content is
fetched to render any of this. scripts/test_artifact_integrity.mjs covers
available, pinned, expired, deleted, unfinalized and external-link artifacts in
both places, the clipboard fallback and a 360 px viewport; run it with
CHROME_BIN=/path/to/chrome node scripts/test_artifact_integrity.mjs.
Failed-check callout
The task page’s Submission & completion card shows a callout above the
current candidate when a job on the candidate’s exact source revision failed
(producer state failed, a non-zero exit code, or source changed while it
ran) and no later job of the same check passed. It names the check, producer
state, exit code and observed time, carries the integrator’s blocked report
when there is one, and has a Show job evidence button that focuses the job’s
entry under Jobs & resource holds; a job that the page does not list opens
the task’s job history instead. A failure that a later pass of the same check
superseded is shown as a neutral note rather than an error, and failures of
other revisions are ignored. The callout reads the task’s job evidence and, only
when the submission has workflow activities, one history?kind=jobs&limit=50
page per change of that evidence; it renders through reconcile(), so an
unchanged poll mutates nothing. scripts/test_failure_callout.mjs covers the
current, superseded, older-revision, clean and activity cases and runs the same
way as scripts/test_ui_refresh.mjs.
Time display
My account has a Time display choice, Local time or UTC, saved in the
browser’s local storage under agent-coordinator.time-zone. It defaults to
Local time, also when storage is unavailable. Every date on the page is
formatted by one helper, formatInstant(), which appends the zone
abbreviation. Every date, including one inside a sentence or a tooltip, is a
focusable <time> whose datetime and title hold the exact ISO-8601 UTC instant; keyboard focus shows it in a
bubble. Lease “minutes remaining” figures stay relative. The zone is part of
every render signature, so changing it rebuilds only the dated nodes and an
unchanged refresh still records no DOM mutation.
scripts/test_time_display.mjs checks both modes across the task list, task
detail and sessions, persistence, and blocked storage; run it with
CHROME_BIN=/path/to/chrome node scripts/test_time_display.mjs.
Task filters
The task queue and Completed tasks views filter the loaded list by Kind,
Origin and Delivery (human-delivered or supervisor-claimable) as well as by
Status (queue only) and search, all combined with AND. Rows show their kind,
origin and a human-delivered pill. Filters read fields already in the list
items, so changing one makes no request; the active filter count and Clear
filters render through the same patching helpers, so a refresh with filters
active records no DOM mutation. Switching project resets every filter, and
Clear filters resets the filters but not the search text.
scripts/test_task_filters.mjs verifies each filter, their combination, the
empty-state text, Clear filters, the project reset and mobile width in a
fixture-backed browser. Run it with
CHROME_BIN=/path/to/chrome node scripts/test_task_filters.mjs.
Saved task views
The Views controls under the task toolbar save the status, kind, origin,
delivery, search and sort as a named view for the signed-in actor and project.
A name has at most 60 characters and must be unique in the project (ignoring
case); a project keeps at most 20 views. Applying a view sets the toolbar and
re-renders the loaded tasks, so it makes no request. The active view is marked
in the select, and when the toolbar no longer matches it the note says so and
Reset to view appears. Delete asks for confirmation in a dialog.
The controls render through reconcile() with idempotent setters, so a refresh
with an active or diverged view records no DOM mutation.
Views are stored in localStorage under agent-coordinator.task-views as
{actor: {project: [view]}}, with no task data or secrets. Each loaded value is
checked against the toolbar’s own option lists; an entry that fails, a
duplicate, or an entry past the 20th is ignored, and the next save drops it
while keeping other actors’ and projects’ entries. If storage throws on read or
write, views still work until the page is reloaded and a note says they cannot
be kept. scripts/test_task_views.mjs verifies save, reload and apply, rename,
delete, the limits, scoping, corrupt storage, throwing storage, the refresh and
mobile width. Run it with
CHROME_BIN=/path/to/chrome node scripts/test_task_views.mjs.