Workspace tools · Local app and recovery
Lightweight local app and recovery
The six primary pages open from reads the capture service prepared, and each says how old its read is. Closing the window leaves capture running. When the service stops, the network drops or the server crashes, the app says so in fixed words, and delivery resumes from the last batch the server acknowledged.
For: the developer who runs the RepoOps desktop app all day, and the admin who has to explain why a machine stopped reporting
What it does, and why it helps
On the desktop app, the capture service (the daemon) runs after each five-minute capture cycle and prepares the first reads of Today, Attribution, AI Security, Remediation, LLM Cost Metrics and Memory for the repositories you use: the one selected as primary (or the first tracked one) and any repository you opened a page for in the last day, at most three a cycle. It prepares a page again when its data changed, not on a timer: a page older than its window is refreshed by the capture service only while you have that repository open, and a cycle with nothing to prepare does no preparation work at all. A page then opens from that prepared read without scanning the case store, and a line under its heading gives the read's age: Prepared 4 min ago by the capture service. A read older than the page's window (10 minutes for Today, 30 for the others) reads as stale and refreshes in the background. When nothing is prepared, the line says the page reads its data when you open it, and why: the capture service is not running, the first preparation has not finished, or the data is too large to keep prepared. Opening a page makes no model call and no GitHub request.
Closing the window does not stop RepoOps. The capture daemon is a separate process, so capture continues. The app keeps its local server running while a window is open and for 10 minutes after the last one closes; then it stops the server to free its memory, and the tray reads Server stopped while no window is open; capture continues. While it is stopped the daemon also runs what the server did for your team: the telemetry and agent trace stream, transcript requests (the tray badge still counts them), operator configuration, the team policy refresh, license renewal, the store retention sweeps, the hosted rollups, fleet control, prevention sync, the session signals scan and its alerts, production error imports, the nightly brain passes, the opt-in captures and syncs, and your team's decisions on local cases. Show dashboard, opening the app again, or any request to the old local address starts the server again, and the first page waits while it starts. Quit in the tray stops the server; the daemon keeps running after Quit too. While Run the background capture daemon is ticked in Settings (the default), the app starts the daemon when it opens, bound to a team or not, and checks every 30 seconds that it is still running. A daemon that stopped is started again after 10 seconds, then 20, 40 and so on up to 5 minutes; after five restarts in 30 minutes the app stops trying and the tray reads Daemon: off (crash loop).
Delivery to a team is opt-in. When it is on, each batch moves the sync cursor only after the server acknowledges it, so a dropped network costs nothing but time: the next cycle after reconnecting resumes from the cursor. Runtime health, on each of the six pages, lists what the service reports: queued bytes and the oldest queued day, disk free and disk pressure, drops by reason, incomplete intervals, restarts and their reasons, memory, and transcript delivery with acknowledged records. It shows no queued record count, because nothing counts the records.
Updates install themselves. When an update check or download fails, the tray adds Update failed: download it manually…, and Settings, App updates, gives the reason in plain words with a link to repoops.ai/download. The data directory sits outside the install folder, and the uninstaller removes only its browser registration, so an update does not touch the stored cases or cursors.
Going back is your choice, never the app's. When the update server lists an older version, Settings, App updates, and the tray offer Roll back to v<version>. The app first checks that the older version can read this device's database, then asks in its own dialog, downloads it, restarts and installs it. Updates then pause until you click Resume updates, so the version you left does not return by itself; a later fix still installs. An older version that finds a database a newer one changed in ways it cannot read stops at launch and names the version to install, rather than opening it. Tests run two builds' migrations against one database and show the rows, prepared reads and sync cursors survive an update and a rollback; no rollback has been run on a packaged build yet.
The pain. A desktop app that scans on every page switch is slow exactly when you need it, and a blank page does not say whether it is loading, broken or empty. When capture stops overnight, nobody finds out until the evidence is missing.
The point of view. A page should open from work already done and say how old it is. A stopped service, an offline network and a failed read are different states, and each should read differently.
What gets easier. Knowing what you are looking at. The readiness line names the age or the reason, the daemon card names the state, and Sync says when the server last acknowledged anything.
When it helps. Every time you open the desktop app, after a restart or sleep, when a network drops, and when an update installs.
Its limits. Prepared pages, the smaller installer, the budget harness, the daemon supervisor and the server that stops while no window is open are on main but in no released installer yet. What still pauses while the server is stopped only serves the pages or catches up on its own, for example the repository mirrors and the approval queue; the list is in ON-DEMAND-SERVER.md in the product focus plan. The app restarts the daemon only while the app itself runs; after Quit nothing restarts it unless it was installed as a service. Nothing has been measured on a 4 GB machine, and the only resource receipt, from a 32 GB developer laptop, fails its memory budgets. Rollback reaches only versions published after it shipped, so the first release with it has nothing to roll back to, and no rollback has been run on a packaged build.
Understand it in 30 seconds
Read the narration
- 0:00 You open the app, and every page starts reading from scratch.
- 0:06 The capture service prepares each page's first read,
- 0:09 and the page says how old it is.
- 0:13 Close the window and capture keeps running; ten minutes later the server stops.
- 0:18 A stopped daemon is restarted, and a crash loop is named.
- 0:23 Rolling back checks the database first,
- 0:25 then pauses updates until you resume them.
Synthetic example. Read the guide
Where to find it
- Desktop: The readiness line under each primary page's heading; Runtime health on the six primary pages; the capture daemon card and Sync on Today; Settings, Capture daemon and Cloud sync (hosted); the tray menu.
- Hosted: None of this is a hosted page. A bound installation's health reaches the team on /team/runtime-health and Connections.
- API:
/api/page-readiness, /api/daemon/warm-status
When to use it
The morning after a restart
Situation. The laptop rebooted overnight. You open the app on Today with one tracked repository.
What you do. Read the line under the heading before the figures, then switch to Attribution and Remediation.
What you see. Today reads Prepared 12 min ago, older than this page's window, so it is refreshing now. The other pages read Prepared 12 min ago by the capture service and open without a scan; once the refresh lands the line adds refreshed just now on this machine.
What it establishes. You read the numbers knowing their age, and nothing waited on a full store parse.
A laptop that stopped reporting
Situation. An admin asks why your installation reads Silent for 2 days on Connections.
What you do. Open the tray and read the daemon line. Open Today and read the capture daemon card and Sync.
What you see. The tray reads Daemon: off (crash loop): the app restarted the daemon five times in 30 minutes and it stopped each time. Today's card reads Off (crash loop), Sync reads daemon off with last acknowledged 2 days ago, and Runtime health names the last exit reason.
What it establishes. Once the cause is fixed, Daemon: Start in the tray restarts capture and clears the limit. The next cycles deliver from the cursor, and Sync reads last acknowledged just now.
Before you start
- Supported versions
- Read against origin/main at ddbee985c. The released installer is v0.3.2; prepared pages (LDG-0971, LDG-0976) and the installer cut (LDG-0972) merged after it and reach users in the next release.
- Where it runs
- Desktop only. The released v0.3.2 Windows installer is 122,132,920 bytes. An unsigned branch build after LDG-0972 measured 105,456,880 bytes, of which the Electron runtime is about 92 MiB; it is not released, and installed size and signed installers are not measured.
- Permissions
- None beyond the local app. The prepared reads sit in the data directory, readable only by your user.
- Connections
- At least one tracked repository for anything to be prepared. Team delivery needs a bound device and Cloud sync (hosted) turned on in Settings.
- Plan
- No plan gate on the local app. Team delivery follows the hosted plans.
Configure it
- Keep the capture daemon running.
Settings, Capture daemon, Run the background capture daemon is on by default. While it is on, the desktop app starts the daemon at launch and restarts it within about a minute if it stops. To keep it restarted when the app is not running, run npm run daemon:install from a source install, which registers a scheduled task, launchd job or systemd unit.
- Read the readiness line before the numbers.
It re-reads up to three times ten seconds apart after a non-ready answer, then every minute, and again when the tab becomes visible. After a page loads, the app also fetches up to 16 other prepared reads while the browser is idle, at most once every five minutes.
- Open Runtime health when something looks wrong.
It names disk pressure (yes, under the free-space floor, or no), drops by reason, restarts and the last exit reason, memory, and transcript delivery. A value nothing reported reads not reported.
- Close the window instead of quitting.
The window releases its resources and capture continues. Ten minutes later the server stops too and the tray says so; Show dashboard in the tray starts it and brings the window back.
- Turn on team delivery if you want acknowledgment.
Settings, Cloud sync (hosted). Today then reads Team delivery: last acknowledged N ago, or Team delivery: bound, first push pending before the first batch lands.
| Setting | Where | A sensible choice | Why it matters |
|---|---|---|---|
REPOOPS_PAGE_PROJECTIONS | the data directory's .env | leave unset | Set to 0 to stop preparing pages; every page then reads its data when opened. |
REPOOPS_PAGE_PROJECTION_REPOS | the data directory's .env | 3 (the default) | How many repositories are prepared per pass. Under memory pressure the daemon prepares one. |
REPOOPS_PAGE_PROJECTION_RECENT_HOURS | the data directory's .env | 24 (the default) | How long a repository you opened stays on the preparation list. The primary repository is always on it. |
REPOOPS_DESKTOP_SERVER_IDLE_MIN | the environment of the desktop app | 10 (the default) | Minutes with no window open before the app stops its server. 0 keeps the server running for as long as the app runs. |
REPOOPS_AUTO_UPDATE | the environment of the desktop app | leave unset | Set to 0 to be asked first: Update ready, with Restart & install and Later. |
REPOOPS_DAEMON_DISABLED | the environment | leave unset | Set to 1 to pin the daemon off; the tray then reads Daemon: off (env off-switch). |
REPOOPS_DAEMON_MEMORY_CEILING_MB | the environment | leave unset (half of RAM, at least 2048 MB) | Above the ceiling the daemon exits so it can be restarted clean; the desktop app restarts it while it runs, and so does an installed service. |
What you should see
A fresh prepared page
Configuration. One tracked repository, daemon running, a capture cycle finished a few minutes ago.
Expect. The line reads Prepared N min ago by the capture service. The page's figures appear without a loading pass over the store.
Verify. In the browser tools, the page's first request answers with the header x-repoops-cache: prepared.
The server stops answering
Configuration. The app's server process crashes while the window is open.
Expect. The window shows Reconnecting to RepoOps… with Retry now, then Still reconnecting… with the attempt number, while the watchdog restarts the server.
Verify. If the server crashes five times within a minute the app stops restarting it and shows RepoOps stopped responding with the path of server.log in the data directory.
A day offline with team delivery on
Configuration. A bound device loses its network for a day and reconnects.
Expect. While offline, Sync reads the last acknowledged time from before the outage and nothing moves the cursor. After reconnecting, the next cycle delivers from the cursor.
Verify. Sync reads last acknowledged just now. Records the receiver refused are counted on Today as quarantined by the receiver: set aside, not retried.
Data and cost
- What is captured
- Prepared reads are the exact answers of the page routes, stored in the data directory under page-projections, owner-only. A body over 512 KB is not kept.
- Who can see it
- Nothing prepared leaves the machine. Team delivery sends what the other features capture, not the prepared pages.
- How long it is kept
- Prepared reads are replaced on each preparation. The data directory survives updates and uninstall; delete it yourself to remove everything.
- What leaves the machine
- Preparing and opening pages makes no model call and no GitHub request. The update check reads repoops.ai/updates at start and every four hours.
- What it costs
- No model spend. Preparation runs inside the daemon's existing cycle. The measured cost is time: on one developer laptop with 96,000 synthetic cases, Today loaded fully in 1.1 s from a prepared read and 3.0 s without one.
When the result differs
| Symptom | Likely cause | Next action |
|---|---|---|
| No readiness line on Today. | Today was opened without a repository, so there is no page to ask about. | Open Today from the dashboard with a repository selected. |
| The line reads The capture service is not running. | The daemon stopped or crashed over its memory ceiling and the app has not restarted it yet, the app is not running, or Run the background capture daemon is off. | Wait a minute for the app to restart it, or use Daemon: Start in the tray. If the tray reads Daemon: off (crash loop), read Last exit reason in Runtime health first. |
| The line reads This page's data is too large to keep prepared. | The page's read is over 512 KB for this repository. | Nothing to fix; the page reads its data when opened. |
| An update did not install. | The update check or download failed. The tray shows Update failed: download it manually…, and Settings, App updates, gives the reason. | Download the installer from repoops.ai/download and run it; the data directory is kept. |
| Roll back is disabled in Settings, App updates. | The update server lists no older version that supports rollback, it could not be reached, or the older version cannot read the database this version wrote. The reason is under the button and in its tooltip. | For a database the older version cannot read, stay on this version, or copy the data directory somewhere safe before installing the older version by hand. |
| A newer version does not install after a rollback. | Updates pause after a rollback so the version you left does not return; Settings, App updates, reads Updates are paused. | Click Resume updates when you want it back. A version published after the one you left installs without this. |
| Runtime health has no Queued records row. | Nothing counts the queued records, so the row is not drawn rather than always reading not reported. | Read Queued bytes and Oldest queued, or Sync on Today for the last acknowledgment. |
| Sync reads failed send attempts since the last acknowledgment. | Sends to the receiver failed and it has not acknowledged a batch since; the number counts attempts, not records. | Check the network and the team binding; Team delivery reads stopped, the credential was revoked or expired when the binding is gone. |
- Disable
- REPOOPS_PAGE_PROJECTIONS=0 turns preparation off; Daemon: Stop stops capture.
- Roll back
- Settings, App updates, or the tray: Roll back to v<version>. It asks first, keeps the data directory, and pauses updates until Resume updates. The updater never downgrades on its own.
- Revoke access
- Unbinding the device stops team delivery; Today reads Team delivery: off, not connected.
- Delete
- Not provided in the app. Uninstalling keeps the data directory (%APPDATA%\repo-dashboard on Windows, ~/.repo-dashboard elsewhere); delete it by hand to remove prepared reads, cases and cursors.
Related tasks
Maintenance evidence
- Feature id
local-app-recovery(spine leaffirst-run)- Owner
- Lean local app (L1 and L2 of the product focus plan: LDG-0971, LDG-0972, LDG-0973, LDG-0976), recovery gaps LDG-0995, rollback LDG-0996. Guide: LDG-0981.
- Supported product version
- RepoOps main after v0.3.2, with LDG-0995 and LDG-0996
- Last verified
- 2026-09-26, read against origin/main at ddbee985c, then the daemon, update and runtime health sections again against the LDG-0995 change, then the update and rollback sections against the LDG-0996 change; lines read from public/lib/page-readiness.js, public/lib/runtime-health.js, public/lib/today-queue.js, installer/lib/daemon-controller.mjs, installer/lib/daemon-supervisor.mjs, installer/lib/update-status.mjs, lib/update-rollback.mjs, lib/schema-compat.mjs, installer/recovery.html and public/settings.html; sizes from RUNTIME-PAYLOAD.md and timings from PAGE-READINESS.md.
- Example fixtures
- lib/drills/daemon-harness.mjs and lib/drills/synthetic-transcripts.mjs; inline cases in lib/page-readiness.test.mjs, lib/page-projection-serve.test.mjs, lib/daemon/jobs/page-projections.test.mjs, public/lib/page-readiness.test.mjs, public/lib/runtime-health.test.mjs, lib/sync/cloud-pusher.test.mjs, installer/recovery-decision.test.js, installer/lib/startup-window-policy.test.mjs, installer/lib/daemon-supervisor.test.mjs, installer/lib/update-status.test.mjs, lib/update-rollback.test.mjs and lib/schema-compat.test.mjs.
- Source references
lib/page-readiness.mjs,lib/daemon/jobs/page-projections.mjs,public/lib/page-readiness.js,public/lib/runtime-health.js,public/lib/today-queue.js,installer/lib/daemon-controller.mjs,installer/lib/daemon-supervisor.mjs,installer/lib/update-status.mjs,lib/update-rollback.mjs,lib/schema-compat.mjs,installer/lib/startup-window-policy.mjs,installer/recovery-decision.js,installer/main.js,lib/server-idle.mjs,lib/daemon/service-ticks.mjs,lib/sync/cloud-pusher.mjs,scripts/installer-bom.mjs- Documentation review
- Written by the authoring agent against the code; independent review not yet recorded.
- Video review
- Narrated story rendered and published again 2026-09-26 (render f8a5da097e0c, LDG-0988) after LDG-1000 made the earlier render wrong about the server: it now shows the server stopping ten minutes after the last window closes, the daemon supervisor and the crash-loop state, and the rollback control, beside the desktop navigation with Connections selected. Six frames, the captions and the transcript were reviewed by the authoring agent, not an independent reviewer; the audio was not listened to by a person. It is a labeled synthetic explanation with no measured timings on screen, not a recording of the packaged app.
Last updated