pour-pwa-roadmap
Pour PWA Roadmap
The mobile/PWA companion landed in Phase 1 (Steps A–G2 of the
webbranch). This doc tracks the gaps surfaced during real-device test-drive, the Phase 2 deferral list locked in ADR-005-PWA-Companion, and the Phase 3+ horizon. Companion to pour-api-contract (binding wire shape) and pour-design-spec §7 (architecture intent).
1. Status as of 2026-04-27
web branch is feature-complete for Phase 1 + Phase 1.5 + Phase 1.5++. Test suite ~774 passing (run cargo test for the live count).
Phase 1 (Steps A–G2) shipped: pour serve, full /api/v1/* surface, embedded PWA, integration tests, OpenAPI spec, ADR + WASM-tradeoffs note.
Phase 1.5 closed real-device test-drive gaps: preset selector (read-only chips), show_when cascade fix, scenario8 captured_at timezone test fix.
Phase 1.5++ closed a UX-inspector audit: visual identity overhaul (cyan accent, ▽ icon, header redesign, mobile-fit polish), client-side idempotency-key persistence, contract §9 amendment (round 5: only 2xx cacheable), server-side idempotency cacheability fix, structured logging via tracing, test isolation fix (the ~/.pour/ pollution race in tests/server_submit.rs).
Mutation paths (preset save/delete, autocreate sub-form, offline queue) remain Phase 2.
2. Phase 1.5 — DONE (2026-04-26)
2.1 Preset Selector (read-only) — DONE
Saved presets render as chip row above the form fields. Tap-to-apply mirrors TUI semantics (preset_exclude fields untouched, missing keys reset to field defaults). Read-only — save/delete still happens via the TUI; that lands in Phase 2.3 with mutation UI.
Implementation: web/app.js fetchPresets/buildPresetChipRow/applyPreset; web/styles.css chip-row scroll-snap. aria-pressed for toggle semantics.
2.2 show_when Cascade — DONE
Root cause was two compounding bugs: (1) fields with show_when that started hidden were never inserted into the DOM, so recomputeVisibility queried null; (2) collectValues() gated on visibility before reading values, creating a stale snapshot. Fix: render all fields unconditionally and toggle the hidden attribute on each; readCurrentFieldValues() reads all DOM fields without a visibility gate.
Equals match now works for static_select, dynamic_select, text, number controllers. one_of and empty-controller edge cases preserved from the Step E iteration.
2½. Phase 1.5++ — DONE (2026-04-27)
UX inspector audit closed across visual identity, mobile fit, and one CRITICAL idempotency-on-retry bug. All landed in web/, src/server/, and tests/.
2½.1 Visual Identity Overhaul
- Cyan accent palette replaces the forest-teal stub (
--accent: #0891b2light /#67e8f9dark). Theme color in HTML meta and manifest match. - ▽ icon — the canonical Pour glyph used in
src/tui/summary.rs(▽ saved) andsrc/tui/form.rs(▽ pour {module_key}). White-filled triangle on cyan circle, vertices within 80% safe zone for Android maskable cropping. - Header reads
pour › <vault>— lowercase, monospace, dynamically populated from/api/v1/health.vault_base_path(basename only). - Status pill — color-only (no emojis). Border + text colored by transport: success (API), warning (FileSystem), danger (offline).
- Module tiles — show
[<key>]dim subtext under display name, mirroring TUI dashboard rows.
2½.2 Mobile UX
- Sticky submit bar with
padding-bottom: max(16px, env(safe-area-inset-bottom))— keyboard never covers it; notch/home-indicator respected. safe-area-insetpadding on body, header, sticky bar, toast.- SVG chevron on
<select>so dropdowns are recognizable (was hidden byappearance: none). - Composite array stacks vertically on
max-width: 480px— three-column layout was unreadable on portrait phones. - Skeleton loading on form view enter — labels + disabled
Loading…selects render synchronously before async fetches. - Empty state copy for empty modules list and empty
dynamic_selectwithallow_create: false.
2½.3 Token Rotation + a11y
- 401 anywhere via
apiFetch→ clearlocalStorage.pour_token, jump to token-gate view, throw "Unauthorized" (callers' catch suppresses toast). aria-required,aria-describedby,role="alert"on field errors,role="status" aria-live="polite"on toast,:focus-visibleoutlines.- Single
<main>with<section>children (HTML spec compliance).
2½.4 Idempotency-on-retry — CRITICAL Bug Fix (contract round 5)
Bug: server cached every response (including 4xx/5xx). Client-side persistent Idempotency-Key (added Phase 1.5++) made it strictly worse — user could not retry-after-fix on validation_failed because the server replayed the cached 400 for 5 minutes.
Contract amendment §9 (round 5): only 2xx terminal successes are cached. 4xx and 5xx are NOT cached — these are recoverable. The client SHOULD reuse the same key for retries after a recoverable error so duplicate-write races never open.
Server fix: src/server/handlers/submit.rs gates idempotency.complete() on parts.status.is_success(); non-2xx calls a new idempotency.release() that removes the entry entirely so retries execute fresh.
Client fix: _pendingIdempotencyKey persists across retries within a form session, rotates on 2xx success or explicit navigation (Pour Another, dashboard).
Tests: submit_idempotency_400_not_cached_allows_retry_with_fix, submit_idempotency_201_is_cached_replay_returns_201 (regression guard).
2½.5 Structured Logging (pour serve)
tracing+tracing-subscriber+tower-http(trace feature only)init_logging()usestry_initfor test fixture compatibilityPOUR_LOGenv var for level filter (defaultinfo,pour=info,tower_http=info)TraceLayerwraps the api subrouter (outermost layer — sees post-middleware status)- Custom logs at: startup, auth outcomes (debug/info/warn per §14 slugs), submit success/failure (module + code, NO field values), captures (debug/warn), idempotency cache (replay/in-flight/eviction with
key_shortfirst 8 chars) - §14 compliance: no tokens, no request bodies, no user-supplied content in any log line
2½.6 Test Isolation (POUR_HOME race)
tests/server_submit.rs previously used bare unsafe { set_var(POUR_HOME, …) } without an ENV_LOCK. Concurrent test functions raced — when one test's EnvGuard dropped mid-other-test, POUR_HOME unset and History::load() fell through to ~/.pour/, polluting users' real history files with test fixture entries (vault_path: "Coffee/note.md", first_field: "Collision Bean", etc.).
Fix: added file-level ENV_LOCK + EnvGuard matching the pattern in server_captures.rs / server_history.rs / server_integration.rs. New regression test submit_history_written_to_pour_home_not_real_home asserts the entry lands in the tempdir and POUR_HOME restores after guard drops.
User remediation: a backup-and-filter cleanup script for polluted ~/.pour/cache/history.jsonl is documented in conversation history; affected users should run it.
2½.7 Other Small Phase 1 Polish (not yet scheduled)
- README "Capturing from your phone" diagram
mobile_visiblestatus pill in PWA header (so the user can confirm the PWA understands which modules are exposed)/api/v1/configre-fetch chattiness — PWA fetches config on every dashboard return; could cache with ETag/If-None-Match- Surface
Idempotency-Keyretry count to the user on transient failures (only matters when offline queue lands in Phase 2.1) - Idempotency cache is FIFO not true LRU per its docstring (drift; not user-visible; clarify or upgrade)
to_bytes(usize::MAX)on response body before caching — fine for current JSON handlers, would need bounding if a future handler streams large bodies
3. Phase 2 — Offline-first PWA
The original deferral list per the plan and ADR-005-PWA-Companion §Decision:
3.1 Offline Submit Queue (the Festival case) — DONE (2026-04-27)
- TASK-2.1.1 — IDB schema:
pour-queueDB,pending_submitsstore, auto-incrementid, indexes onmodule_key+queued_at. Schema inweb/queue.js(page context) and inlined inweb/sw.js(SW context). Migration rule: NEVERdeleteObjectStore('pending_submits')— data loss = capture loss. Each record carries:id, module_key, body, idempotency_key, auth_header, queued_at, attempt_count, last_error. - TASK-2.1.2 — SW intercepts
POST /api/v1/submit/*. Network unreachable or 5xx → queue + synthetic 202. 4xx → pass through (client-fixable, never queued). Synthetic 202 body:{ queued, queue_id, captured_at }.Idempotency-Keycaptured at queue time, reused on every drain retry.captured_atfrom original body, never drain time.QuotaExceededError→ 507 "queue full" response to page. - TASK-2.1.3 — Background Sync drain (
pour-queue-draintag). FIFO byqueued_atASC, tiebreak byidASC. On 2xx: delete record, postMessage pageDRAINED. On 4xx: keep, incrementattempt_count, setlast_error(code only — §14). On 5xx/network: keep, reschedule. Safari fallback: page firesDRAIN_NOWpostMessage onwindow.online. Both paths call identicaldrainQueue(). - TASK-2.1.4 — "Queued (n)" badge in header (hidden when n=0). Tap → expandable panel listing pending records: module key + relative
queued_atonly (no field values, §14). Panel shows Retry/Edit/Discard actions per record. SW postMessages update badge without polling. - TASK-2.1.5 — Conflict UX: Discard (single tap confirm), Retry now (triggers drain), Edit (pre-fills form with queued body, retains
Idempotency-Key). If resubmit returnsIdempotency-Replay: true, shows "This was already saved — showing the original."captured_atstays original on edit. IDB record cleaned up after successful 201.
3.2 Service Worker — App-shell Cache — DONE (2026-04-27)
- TASK-2.2.1 —
/sw.jsserved at root scope withCache-Control: no-cache, max-age=0, must-revalidateandContent-Type: application/javascript. Route insrc/server/mod.rs. Tests intests/server_static.rs(6 new tests)./static/sw.jsreturns 404 (scope-poisoning guard). - TASK-2.2.2 — App-shell cache: pre-caches
/, /app.js, /queue.js, /styles.css, /manifest.json, /static/icon.svg. Cache-first for shell; network-only for/api/v1/*(no fallback — always live). SW registered inDOMContentLoadedafter token bootstrap; failure degrades silently. - TASK-2.2.3 — Update flow: new SW → postMessage
SW_UPDATEDto page → soft refresh banner ("New version available — tap to refresh"). User-initiatedskipWaiting()only (never auto-claim mid-form). Dismiss hides banner for this session.
3.3 create_template Sub-form Overlay — DONE (2026-04-27)
- TASK-2.3.1:
<section id="subform-overlay" role="dialog" aria-modal>inindex.html. Focus trap (Tab/Shift-Tab cycles inside panel), ESC → cancel, scrim tap → cancel. Mobile-fit (360px minimum,100dvhwith safe-area awareness). No<select>in overlay. - TASK-2.3.2: Template fields rendered from
/api/v1/configtemplates.<name>.fields[].text/number→ standard inputs;static_select→ inline cycling control with ◂ ▸ chevrons (role="combobox"+ hidden<ul role="listbox">for a11y). Up/Down navigate fields; Left/Right cycle static_select options. Default values prefill. Required validation with per-field error pills. Overlay header shows parent field prompt + typed novel value. - TASK-2.3.3: Confirm wires
auto_create_inputs[field]into_pendingAutoCreateInputs. Parent submit includes map when non-empty. Novel-value check mirrorssrc/autocreate.rs is_existing_option(case-insensitive, trimmed). If user edits parent field back to an existing option, the map entry is dropped before submit. Map cleared on form reset and 2xx success. - TASK-2.3.4: Cancel reverts parent input to its open-time value. Map entry dropped. Focus returns to parent input.
- TASK-2.3.5: Server 400
auto_create_input_required→ overlay re-opens with top-level banner. Servervalidation_failedwith template-field names → overlay re-opens with per-field errors pinned. 201 withwarnings[].code === "autocreate_failed"→ summary view shows non-fatal warning chip ("Saved, but note creation failed: …"). No user content logged (contract §14).
3.4 Preset Mutation UI — DONE (2026-04-27)
- TASK-2.4.1 — "Save as preset" inline affordance below chip row. Tap "+ Save as preset" to reveal inline name input. Name validated 1–64 chars, no
/. Values snapshot excludescomposite_array,preset_exclude=true, andshow_when-hidden fields (mirrors TUI).PUT /api/v1/presets/{module}/{name}withdescription: "". On 200/201: re-fetch canonical list, re-render chip row from response. Inline error for 400; toast for other failures. - TASK-2.4.2 — Long-press (~500ms) or right-click on any named chip opens an edit panel inline (not modal). Panel shows name (editable) + description (editable). Save: PUT new name first; if name changed, DELETE old after PUT success (best-effort — page-close-mid-rename leak documented as known minor issue). Panel shows helper text explaining values are not re-snapshotted (delete + re-save for that).
-webkit-touch-callout: noneon chips suppresses iOS native context menu. Pointer events detect long-press vs short-tap; movement > 8px cancels the long-press (drag threshold). - TASK-2.4.3 — Delete button in edit panel. Single-tap-confirm: first tap → "Tap again to confirm" (2s window). Second tap →
DELETE /api/v1/presets/{module}/{name}. 204 or 404 → remove chip, close panel, clear_activePresetif it matched. All viarefreshPresetChipRowFromListwhich checks_activePresetagainst fresh list. - TASK-2.4.4 — Drag-and-hold reorder. Pointer events.
dragCommittedflag: move > 8px from pointerdown = committed drag (otherwise falls through to short-tap → apply). Ghost clone follows pointer; placeholder shows insertion point in real time. On drop: read DOM order (exclude<none>), firePUT /api/v1/presets/{module}/orderwith full canonical list. Re-render from server response. Concurrency: "latest-wins" serialized — only one PUT in flight; further drops queue the latest order and fire after the in-flight settles (_reorderInFlight+_reorderPendingOrder). Coexists with horizontal scroll-snap viatouch-action: pan-xon chips,touch-action: noneonly during committed drag. - TASK-2.4.5 — Error paths: all mutations show toast on failure with server's
error.message. Offline (!navigator.onLine) → toast "Offline — preset changes need a connection", NO mutation queued. 401 → existing global handler inapiFetch(clear token, jump to token-gate). Always re-render chips from response, never from optimistic local state.
3.5 History view + Heatmap — DONE (2026-04-27)
Data source decision (Open Q2 closed): Path (a) — client-side rollup from /api/v1/history. Rationale: zero contract cost; no new endpoint or contract amendment required. The summary block (streak, today/week counts) comes from the first no-cursor call. The heatmap rolls up entries[].timestamp into a local-date → count map client-side.
Heatmap pagination loop termination: fetches pages at limit=1000 (HEATMAP_FETCH_LIMIT) using the server's next_cursor field until: (a) has_more === false, or (b) the oldest entry in the latest page falls before the 90-day window (remaining entries guaranteed older), or (c) a network error (renders with partial data). Covers ~90 days of 10/day captures in a single request for typical usage.
Cursor discipline: Always next_cursor from server; never derived from entries[last].timestamp. Same-millisecond entries from offline-queue replay would be silently dropped by a timestamp-only cursor — the id-based cursor is immune.
- TASK-2.5.1 — History view shell + bottom-tab nav.
<section id="history">in<main>. Bottom-tab nav (role="tablist",role="tab",aria-selected). Lazy-loaded on first tab tap. Module key + relative time + first_field per entry. Empty state copy. Tab nav hidden on form/summary views (sticky submit owns the bottom). - TASK-2.5.2 — Cursor pagination. First load:
GET /api/v1/history?limit=50(no cursor — returnssummaryfor heatmap). Scroll within ~200px of bottom +has_more === true→ append with?limit=50&cursor=<next_cursor>. One request in flight at a time (_historyFetchInFlightguard). Inline spinner + "Couldn't load more — tap to retry" on failure. - TASK-2.5.3 — Tap entry →
GET /api/v1/captures/{history_id}→ panel with vault_path + transport_mode header. Content rendered via<pre>+textContentonly (neverinnerHTML— XSS guard). 404: "This capture's file no longer exists in the vault." 502: "Couldn't read — vault unreachable." No content logged (contract §14). - TASK-2.5.4 — Heatmap renderer. Past 90 days, columns-of-weeks, cyan accent gradient. Count tiers: 0=bg, 1=light, 2-3=mid, 4+=darkest. Each cell has
aria-label="<long-date>: <count> captures". Tap → popover with date + count.prefers-reduced-motionrespected (transitions only when no-preference). Day-of-week labels (M/W/F). Month labels not included (complex for the gain — omitted in v1). Sits above history list. - TASK-2.5.5 — Decision documented above and in
web/app.jscomment block. - TASK-2.5.6 — Tab nav final: lowercase mono labels, cyan active underline indicator. Hidden on form/summary, shown on dashboard/history.
history.pushState/popstatewired: entering history pushes{ view: "history" }state;popstateto any other state returns to dashboard. Queue badge (Stream A) coexists in header at 360px — tab nav is fixed at bottom, badge at top.
3.6 Capture Trim from PWA — INTENTIONALLY DEFERRED
pour trim stays desktop-only per pour-api-contract §15. The "type 'trim' to confirm" gate is manifesto-aligned and the destructive-action UX doesn't fit a phone screen.
4. Phase 3 — Polish and Reach
4.1 TLS
- Self-signed cert generation in
pour serve --tls - Setup guide for trusting the cert on iOS/Android (the part most users will hate)
- Once TLS lands,
crypto.randomUUID()works natively (currently using a non-secure-context fallback; see Step E iteration)
4.2 mDNS / pour.local
- Advertise the server via mDNS so the phone can resolve
pour.localinstead of a raw LAN IP - Cross-platform mDNS in Rust is annoying; defer until TLS is sorted (since the certs and the hostname both need to align)
4.3 MCP Companion (pour mcp)
- Per pour-api-contract §15: a thin Model Context Protocol server that wraps the HTTP API and exposes
pour_submit_<module>tools dynamically derived from/api/v1/config - Same binary or sibling crate; reuses the engine
- Documented intent only — no implementation slot until the HTTP API has stabilized in production usage
5. Phase 2 Utoipa Migration — SCHEDULED
Per pour-api-contract §15.1: replace the hand-written pour-openapi.yaml with utoipa-derived output once we're confident the contract is stable. Annotate handlers with #[utoipa::path(…)] and types with #[derive(ToSchema)]. The hand-written YAML deletes in the same PR.
When to trigger: after Phase 2 ships and we've gone ~1 month without a contract amendment. Premature automation re-creates the drift problem from a different angle.
6. Out of Scope (probably forever)
- Multi-tenant
pour serve(one binary, one config, one vault, one token) - Cloud sync (violates manifesto's "Plaintext is Forever" — files are the source of truth)
- A native iOS/Android app (the PWA is the answer; app stores violate "the tool gets out of the way")
- Cross-device preset sync via cloud (presets live in
~/.pour/presets.json; if you want sync, use a vault sync tool)
7. Change Log
- 2026-04-26 — initial roadmap. Phase 1.5 surfaced from real-device test-drive: preset selector +
show_whencascade. Both in flight. - 2026-04-26 — Phase 1.5 landed: preset selector (read-only chips, tap-to-apply mirroring TUI),
show_whencascade fix (DOM-creation root cause + collectValues stale-snapshot fix), scenario8 captured_at timezone test fix. - 2026-04-27 — Phase 1.5++ landed across visual identity (cyan palette, ▽ icon, header redesign, status-pill colors,
[key]subtext on tiles), mobile UX (sticky submit + safe-area, select chevron, composite stack on narrow, skeleton loading, empty states), token rotation (401-anywhere → token-gate), a11y essentials, structured logging (tracing+ TraceLayer + §14-compliant custom logs), idempotency cacheability fix (contract §9 round-5 amendment + client-side key persistence), and test isolation fix (POUR_HOME race intests/server_submit.rs). - 2026-04-27 — Phase 2 Stream B landed:
create_templatesub-form overlay (TASK-2.0.1, TASK-2.3.1–2.3.5). Overlay shell + focus trap + ESC/scrim cancel; template field rendering with inline cycling (◂ ▸,role="combobox", hidden listbox); novel-value check mirroringis_existing_option(case-insensitive);auto_create_inputsmap wired into parent submit body; cancel-revert; server-error relay (overlay re-open onauto_create_input_requiredorvalidation_failedtemplate fields; non-fatalautocreate_failedwarning chip on summary). TASK-2.0.1 (PWA test infra gap note) also written. No Rust changes; no new dependencies. - 2026-04-27 — Phase 2 Stream A landed: service worker + offline submit queue (TASK-2.2.1–2.2.3, TASK-2.1.1–2.1.5). Contract §6.4 round-6 amendment (synthetic 202 clarification). New files:
web/sw.js,web/queue.js. New route/sw.js(+/queue.js) insrc/server/mod.rs. 6 new tests intests/server_static.rs. No new Rust dependencies; vanilla JS + native IDB/Service Worker APIs only. - 2026-04-27 — Phase 2 Stream C landed: preset mutation UI (TASK-2.4.1–2.4.5). Inline save-as affordance; long-press/right-click edit panel (name + description); PUT-then-DELETE rename with known minor leak documented; single-tap-confirm delete; pointer-event drag-reorder with latest-wins serialization. No Rust changes; no new dependencies. §3.4 closed.
- 2026-04-27 — Phase 2 Stream D landed: history view + heatmap (TASK-2.5.1–2.5.6). Bottom-tab nav (capture/history,
role="tablist", cyan active indicator, hidden on form/summary); lazy history list with cursor pagination; tap-to-read capture panel (<pre>+textContent, neverinnerHTML); 90-day heatmap (client-side rollup from/api/v1/history, columns-of-weeks, count tiers,aria-labelper cell,prefers-reduced-motion);pushState/popstatebrowser-back wiring. Open Q2 (heatmap data source) closed: path (a) client-side rollup. No Rust changes; no new dependencies. §3.5 closed. Phase 2 complete. - 2026-04-27 — Stream C post-inspector fixes (4 CRITICAL + 3 MAJOR findings): (1)
collectPresetValuesnow skips empty-string fields, mirroring TUI!val.is_empty()predicate exactly; (2) "order" reserved-name guard added in save-as + rename validation (client) andput_handlerbelt-and-suspenders (server,400 validation_failed reserved_name); contract §6.8 round-8 amendment + OpenAPI note; 3 regression tests intests/server_presets.rs; (3) rename clobber blocked client-side before PUT fires; (4) long-press with cache desync re-fetches before opening edit panel, aborts with toast if still missing; (5) delete/rename of active preset now resets form fields to defaults viaapplyPreset(mod, null); rename also updates_activePresetto new name before chip re-render; (6) reorder 400 error now re-fetches canonical order and re-renders chip row; (7) save-as single-tap-confirm pattern guards silent overwrite of existing preset name. No new dependencies. 21server_presetstests all passing (was 18). - 2026-04-27 — Stream B post-inspector fixes (5 MAJOR findings): (1) error routing — all
validation_failedfields[] now go to parent form only; (2) focus trap — Tab when focus is outside panel forces recapture to first/last focusable; (3) static_select required-validation bypass —_userInteractedflag on cycling wrapper gates required check; (4) iOS 400-race guard —openSubformOverlaycall inauto_create_input_requiredhandler now no-ops if_overlayContextalready set; (5) server warning messages changed to code-only strings (src/server/handlers/submit.rs); internal error now logged viatracing::warn!. 780 tests passing. - 2026-04-27 — Stream A post-inspector fixes (5 CRITICAL findings): (1) dead 507 handler moved before the 202 branch in
handleSubmit; (2)_editingQueueIdleak patched — cleared inopenForm,loadDashboard, andbtn-pour-another; (3) fresh-install SW_UPDATED suppressed —activateonly posts when old cache keys existed; (4) stale auth token eliminated —auth_headerremoved from IDB,drainQueueusesgetAuthFromClient()(MessageChannel to page) for a fresh token at drain time, defers if no client open; (5)#queue-sync-statuswired to drain state machine (DRAIN_STARTED/DRAIN_FINISHED postMessages, window online/offline, panel open/close); CSS modifier classes added. Body limit intests/server_static.rsbumped to 524288. 780 tests passing. - 2026-04-27 — Stream D post-inspector fixes (4 CRITICAL findings): (1) heatmap fetch loop now has a hard
HEATMAP_MAX_PAGES=50iteration cap + empty-page guard — prevents infinite loop on malformed server response (empty entries withhas_more=true); (2)renderHeatmapreuses a single#heatmap-popoverelement (create-once viagetElementById, reuse on re-render) and attaches the global dismiss click listener exactly once via_heatmapClickListenerAttachedflag — eliminates popover/listener accumulation across Dashboard ⇄ History round-trips; (3+4) two-level pushState/popstate state machine —_historyViewPushedmodule-level flag replaces the stalehistory.state.viewguard (fixes reload bug where history.state survives page reload and guard skips the push);openCapturePanelpushes Level 2 state{view:"history",panel:id};closeCapturePanel(fromPopstate)distinguishes button-close (callshistory.back()) from popstate-close (no double-pop); popstate handler routes on new state shape: panel→close panel only, no-panel history view→close panel, no-history-view→go to dashboard + reset_historyViewPushed. 783 tests passing.