Files
beardrive/web/docs
eb01953729 feat(webapp): undo a whole agent run from the run card (#156)
* feat(webapp): undo a whole agent run from the run card

The run card was grouped for this and stopped one button short: every row
inside it carried an action, the header carried none, so reverting a bad run
meant clicking file by file and hoping you got them all.

POST /api/p/<id>/undo-run works out, for every path the run touched, the op
that puts it back — a put at the pre-run blob, or a delete for a file the run
created — and writes them all in ONE journal append. That is the atomicity
argument, not an optimization: one Put of one object either lands or it does
not, so there is no half-undone run to report. appendOps is the batch write
every path in the package now goes through; appendOp is its single-op call.

Selection is by the journal an op was READ FROM, never op.Device — that field
is arbitrary JSON any member with write access can put in their own journal,
and the card attributes rows the same way. The note form additionally requires
an empty Session, because runs.ts can never file a session-carrying op under a
note-keyed card.

Append-only throughout: the run's own ops are never edited or removed, so
one-writer-per-journal and deterministic replay both survive. The undo's ops
carry a note naming the run, so the undo is itself a run card you can undo.

The confirm asks the server for the file list rather than deriving it from the
loaded feed (paged and filterable, so a client-computed list is wrong exactly
when the run is old), lists every path with its action, and names the one thing
that can burn someone: a file a teammate changed after the run is reverted too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(webapp): the undo confirm names the paths it will not write

planUndo already refuses a path the hub's own upload door would refuse — a
peer can push one under .bdrive/ or with a control character in it — but the
dialog listed only what the undo WOULD do, which reads as "all of it".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:04:35 -07:00
..

docs.beardrive.ai

The public product documentation: CLI, sync model, self-hosting. Astro + Starlight, static output, Pagefind search.

npm install
npm run dev       # http://localhost:4321
npm run build     # -> dist/
npm run preview

npm run check:sitemap http://localhost:4321        # after npm run preview
npm run check:sitemap https://docs.beardrive.ai    # or against production

The sitemap

/sitemap-index.xml -> /sitemap-0.xml, generated by @astrojs/sitemap. Starlight would add that integration itself; astro.config.mjs declares it explicitly instead, which replaces Starlight's default rather than duplicating it and is what makes the lastmod option reachable.

Each URL carries a <lastmod> taken from the commit date of the markdown behind it — see "Checkout depth" below for the one way that goes wrong. The redirect stubs are correctly absent: Astro emits them as noindex meta-refresh pages, and @astrojs/sitemap leaves them out.

public/robots.txt exists to carry the Sitemap: line: robots.txt is the one file a crawler fetches without being told, and the landing page's robots.txt is on a different host so it cannot point here.

Verify that file survives the deploy. This host is behind Cloudflare, which was serving a managed robots.txt of its own (the content-signals policy block) back when the origin had none — a body with no Sitemap: and, in fact, no directives at all. Whether an origin robots.txt now replaces that or gets merged with it is Cloudflare's call, not this repo's, so after deploying check the served file rather than the built one:

curl -s https://docs.beardrive.ai/robots.txt | grep -i sitemap

check:sitemap is the same set of checks Search Console runs — follow robots.txt to the index, parse it, confirm every advertised URL returns 200 — and takes any origin, so it works against a local preview and against production. It is worth running against production after a deploy: a sitemap that has gone stale or started advertising 404s looks completely fine until something crawls it, which is weeks later.

Why this is a standalone site

Unlike the hub frontend (internal/webapp/static) and the cloud landing page (cloud/internal/landing/dist), this is not embedded into a Go binary:

  • Docs change far more often than the binary. Embedding them would mean a Go rebuild and redeploy to fix a typo.
  • A Pagefind search index has no business shipping inside every self-hoster's install.

It lives in the OSS repo because that is what it documents. "Edit this page" resolves to something an outside contributor can open a PR against.

Design tokens

scripts/tokens.mjs reads the @theme block in internal/webapp/frontend/src/tw.css — the source of truth for the BearDrive palette — and emits src/styles/tokens.gen.css. That file is generated, gitignored, and regenerated by npm run dev and npm run build, so the palette cannot drift. src/styles/custom.css maps Starlight's --sl-color-* variables onto it and invents no colors of its own.

(The cloud landing page can't do this — it sits in a different Go module and keeps a copy of the tokens, policed by its own check-tokens.mjs.)

Adding a page

Drop a .md file under src/content/docs/<section>/ with title and description frontmatter, then add its slug to the sidebar in astro.config.mjs. Sidebar order is explicit, not alphabetical.

Write description for every page: it is the meta description, the search result snippet, and what llms.txt shows.

llms.txt

starlight-llms-txt generates /llms.txt, /llms-small.txt, and /llms-full.txt at build time.

Convention puts llms.txt at the root domain, not a docs subdomain — so beardrive.ai/llms.txt should redirect or proxy to docs.beardrive.ai/llms.txt. That redirect lives in the cloud landing page and is the one cross-repo coordination point this split introduces.

Structure

The sidebar order in astro.config.mjs is the recommended path, and the recommended path is agent-first:

  • Start here — what it is, set up with your agent, first hour. No brew install appears in this group.
  • Working with agents — the workflows the product exists for.
  • Manual setup (optional) — the CLI route: install, set up by hand, skills and hooks in detail. Same destination, more steps; one click away, never on the critical path.
  • Self-hosting, Reference, Concepts — unchanged in intent.
  • More — off-site links (use cases, blog, GitHub), last because the order is the recommended path and nothing that leaves the docs belongs above them.

Keep new onboarding content out of Manual. If a page teaches someone how to get started, it belongs in Start here and should say what to ask an agent, not what to type.

Job-shaped "use case" pages — written for someone deciding, not someone building — live on the marketing site (beardrive.ai/use-cases), which owns that audience and already had its own copy of all of them. The docs used to carry a parallel set; they were deleted and each URL redirects to its counterpart there.

The header logo links to beardrive.ai, not the docs index. Starlight always points it at the docs root and exposes no option for it, so that link lives in src/components/SiteTitle.astro — Starlight's own component with one changed href, registered through components: in astro.config.mjs. If a Starlight upgrade restyles the header, that file is the first thing to re-diff against node_modules/@astrojs/starlight/components/SiteTitle.astro.

Deploying

Static output in dist/. Any static host works; build command npm run build, output directory dist, project root web/docs.

Today that host is Cloudflare Pages, project beardrive-docs (docs.beardrive.ai), deployed by .github/workflows/docs.yml on every push to main that touches web/docs/** or the token source. It needs two repository secrets: CLOUDFLARE_API_TOKEN (an account token with Cloudflare Pages: Edit) and CLOUDFLARE_ACCOUNT_ID.

Not the Pages Git integration, deliberately: it clones shallow, which costs every <lastmod> (see "Checkout depth"), and it would build the docs on commits that cannot change them. The Pages project is a direct-upload project — nothing deploys it but this workflow. That is also the failure mode to watch for: when deploys were manual, they simply stopped, and the site sat three weeks stale while every check that only runs during a deploy stayed green.

Redirects

The docs were reorganized around the agent-first path, so three old URLs moved:

Old New
/start/install /manual/install/
/start/quickstart /manual/setup-by-hand/
/guides/connect-an-agent /start/setup/

astro.config.mjs declares these, which in a static build emits meta-refresh pages — fine for humans, weak for search engines. Real 301s belong in the host.

Firebase Hosting (simplest static option on GCP — CDN, TLS, and custom domains included):

{
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*"],
    "redirects": [
      { "source": "/start/install", "destination": "/manual/install/", "type": 301 },
      { "source": "/start/quickstart", "destination": "/manual/setup-by-hand/", "type": 301 },
      { "source": "/guides/connect-an-agent", "destination": "/start/setup/", "type": 301 }
    ]
  }
}

Cloud Storage behind an external Application Load Balancer: put the rules in the URL map, which redirects before the bucket is ever reached.

gcloud compute url-maps edit docs-url-map   # pathMatchers[].pathRules[]:
#   - paths: ["/start/install"]
#     urlRedirect:
#       pathRedirect: "/manual/install/"
#       redirectResponseCode: MOVED_PERMANENTLY_DEFAULT
#       stripQuery: false

Whichever host wins, keep the Astro redirects block as well: it is the portable fallback, and it keeps local npm run preview honest.

Note that the build reads a file outside web/docs (the token source), so the host must check out the whole repository rather than just this subdirectory.

Checkout depth

Check out with full history, not a shallow clone. The sitemap's <lastmod> for each page is the commit date of the markdown behind it, so a depth-1 checkout — the default for actions/checkout and for most build hosts — has nothing to read the dates from.

- uses: actions/checkout@v4
  with:
    fetch-depth: 0 # sitemap <lastmod> comes from commit dates

Getting this wrong degrades rather than breaks: astro.config.mjs detects the shallow clone and emits no lastmod at all, because the alternative is stamping all 25 pages with the one commit a shallow clone has, and a sitemap that claims the whole site changed on every deploy is one Google learns to ignore. So the symptom is a silently less useful sitemap — check for <lastmod> in the deployed sitemap-0.xml after changing hosts.