mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
webapp: seed the demo hub with a realistic wiki
The demo harness generated files named run-015.md, des-031.md and so on, with one-line bodies. Screenshots taken from it end up on the website, and "des-031.md" in a treemap tells a visitor nothing about what BearDrive is for. Replaced the generator with a wiki a company would actually have: runbooks, ADRs with real slugs, dated meeting notes, product and research docs, and three hand-written documents (q3-findings, incident-response, first-week) whose markdown renders with headings, tables, code and lists so the file view is worth screenshotting. Read-heat shaping is unchanged — runbooks are what the on-call agents live in, research notes are written for humans and barely read by anything — so the insights views still light up. Project renamed proj -> acme-wiki to match. Also dropped the "must never be committed" note: the file has been in the tree for a while and is genuinely useful for exploring the UI and taking product screenshots. It still only runs under BDRIVE_MANUAL_SERVE=1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RA5pQH92cxk5SfiYJeTUjK
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
afac6bef65
commit
52f595efc0
@@ -1,14 +1,19 @@
|
||||
package webapp
|
||||
|
||||
// Temporary manual demo harness: a seeded hub (~500 files, zipf-ish read
|
||||
// data, four agent devices) for exploring the web UI by hand. Not part of
|
||||
// the test suite: it only runs with BDRIVE_MANUAL_SERVE=1 and must never be
|
||||
// committed.
|
||||
// Manual demo harness: a seeded hub (a realistic company wiki, zipf-ish read
|
||||
// data, four agent devices) for exploring the web UI by hand and for taking
|
||||
// product screenshots. Not part of the test suite: it only runs with
|
||||
// BDRIVE_MANUAL_SERVE=1.
|
||||
//
|
||||
// State lives in a STABLE directory (os.TempDir()/bdrive-demo-hub, override
|
||||
// with BDRIVE_MANUAL_STATE), so restarting the harness — e.g. after a
|
||||
// frontend change — keeps accounts, browser sessions, the project id, and
|
||||
// all seeded/demo data. Delete the directory to reset the demo.
|
||||
//
|
||||
// The seed is deliberately realistic: real-looking runbooks, ADRs, dated
|
||||
// meeting notes and product docs, with bodies that render as proper markdown
|
||||
// (headings, tables, code, callouts). Screenshots taken here end up on the
|
||||
// website, and "run-015.md" in a treemap tells a visitor nothing.
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
@@ -18,6 +23,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -45,7 +51,7 @@ func TestManualServe(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p, _, err := db.GetOrCreate("proj", "") // create-or-join: id is stable across restarts
|
||||
p, _, err := db.GetOrCreate("acme-wiki", "") // create-or-join: id is stable across restarts
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -82,26 +88,26 @@ func TestManualServe(t *testing.T) {
|
||||
time.Sleep(8 * time.Hour)
|
||||
}
|
||||
|
||||
// seedDemo writes ~500 files (journal + blobs) and their read buckets. Runs
|
||||
// doc is one seeded file: a path and the markdown body behind it.
|
||||
type doc struct {
|
||||
path string
|
||||
body string
|
||||
}
|
||||
|
||||
// seedDemo writes the demo wiki (journal + blobs) and its read buckets. Runs
|
||||
// once per state dir.
|
||||
func seedDemo(t *testing.T, state, prefix, projectID string) {
|
||||
t.Helper()
|
||||
os.MkdirAll(filepath.Join(prefix, "journal"), 0o755)
|
||||
os.MkdirAll(filepath.Join(prefix, "blobs"), 0o755)
|
||||
|
||||
seed := int64(42)
|
||||
rnd := func() float64 {
|
||||
seed = (seed*16807 + 7) % 2147483647
|
||||
return float64(seed) / 2147483647
|
||||
}
|
||||
folders := []struct {
|
||||
dir string
|
||||
n int
|
||||
}{
|
||||
{"wiki/onboarding", 28}, {"wiki/architecture", 40}, {"wiki/runbooks", 55},
|
||||
{"wiki/api", 70}, {"wiki/decisions", 45}, {"docs/product", 60},
|
||||
{"docs/design", 35}, {"notes/meetings", 80}, {"notes/research", 45},
|
||||
{"shared/reports", 42},
|
||||
}
|
||||
|
||||
docs := demoDocs()
|
||||
humans := []string{"alice@x.io", "bob@x.io", "carol@x.io"}
|
||||
agents := []string{"dev-ci", "dev-snow", "codex-mia", "gemini-doc"}
|
||||
|
||||
@@ -109,51 +115,50 @@ func seedDemo(t *testing.T, state, prefix, projectID string) {
|
||||
var ops []journal.Op
|
||||
var stats []ReadStat
|
||||
var lam, seq int64
|
||||
fileNo := 0
|
||||
for _, f := range folders {
|
||||
short := filepath.Base(f.dir)[:3]
|
||||
for i := 0; i < f.n; i++ {
|
||||
fileNo++
|
||||
path := fmt.Sprintf("%s/%s-%03d.md", f.dir, short, i+1)
|
||||
content := fmt.Sprintf("# %s\n\nSeeded demo file %d in %s.\n", path, fileNo, f.dir)
|
||||
sum := sha256.Sum256([]byte(content))
|
||||
blob := hex.EncodeToString(sum[:])
|
||||
os.WriteFile(filepath.Join(prefix, "blobs", blob), []byte(content), 0o644)
|
||||
stale := int(400 * rnd() * rnd())
|
||||
lam++
|
||||
seq++
|
||||
ops = append(ops, journal.Op{
|
||||
Seq: seq, Lamport: lam, Time: now.AddDate(0, 0, -stale),
|
||||
Device: "seed", DeviceName: "seed", Author: "alice@x.io",
|
||||
User: "alice@x.io", UserName: "Alice",
|
||||
Kind: journal.KindPut, Path: path, Blob: blob,
|
||||
Size: int64(len(content)), Mode: 0o644,
|
||||
})
|
||||
hot := rnd() < 0.15
|
||||
day := now.AddDate(0, 0, -int(rnd()*20)).Format("2006-01-02")
|
||||
for _, a := range humans {
|
||||
n := int64(math.Floor((map[bool]float64{true: 30, false: 3}[hot]) * rnd() * rnd()))
|
||||
if n > 0 {
|
||||
stats = append(stats, ReadStat{Project: projectID, Path: path, Day: day,
|
||||
Kind: ReadKindHuman, Actor: a, Count: n, Last: now})
|
||||
}
|
||||
|
||||
for _, d := range docs {
|
||||
sum := sha256.Sum256([]byte(d.body))
|
||||
blob := hex.EncodeToString(sum[:])
|
||||
os.WriteFile(filepath.Join(prefix, "blobs", blob), []byte(d.body), 0o644)
|
||||
stale := int(400 * rnd() * rnd())
|
||||
lam++
|
||||
seq++
|
||||
ops = append(ops, journal.Op{
|
||||
Seq: seq, Lamport: lam, Time: now.AddDate(0, 0, -stale),
|
||||
Device: "seed", DeviceName: "seed", Author: "alice@x.io",
|
||||
User: "alice@x.io", UserName: "Alice",
|
||||
Kind: journal.KindPut, Path: d.path, Blob: blob,
|
||||
Size: int64(len(d.body)), Mode: 0o644,
|
||||
})
|
||||
|
||||
dir := filepath.Dir(d.path)
|
||||
hot := rnd() < 0.15
|
||||
day := now.AddDate(0, 0, -int(rnd()*20)).Format("2006-01-02")
|
||||
for _, a := range humans {
|
||||
n := int64(math.Floor((map[bool]float64{true: 30, false: 3}[hot]) * rnd() * rnd()))
|
||||
if n > 0 {
|
||||
stats = append(stats, ReadStat{Project: projectID, Path: d.path, Day: day,
|
||||
Kind: ReadKindHuman, Actor: a, Count: n, Last: now})
|
||||
}
|
||||
for ai, a := range agents {
|
||||
boost := 1.0
|
||||
if f.dir == "wiki/runbooks" && ai < 2 {
|
||||
boost = 4
|
||||
}
|
||||
if f.dir == "notes/research" {
|
||||
boost = 0.05
|
||||
}
|
||||
n := int64(math.Floor((map[bool]float64{true: 60, false: 5}[hot]) * rnd() * rnd() * boost))
|
||||
if n > 0 {
|
||||
stats = append(stats, ReadStat{Project: projectID, Path: path, Day: day,
|
||||
Kind: ReadKindAgent, Actor: a, Count: n, Last: now})
|
||||
}
|
||||
}
|
||||
for ai, a := range agents {
|
||||
// Runbooks are what the on-call agents live in; research notes are
|
||||
// written for humans and barely read by anything.
|
||||
boost := 1.0
|
||||
if dir == "wiki/runbooks" && ai < 2 {
|
||||
boost = 4
|
||||
}
|
||||
if dir == "notes/research" {
|
||||
boost = 0.05
|
||||
}
|
||||
n := int64(math.Floor((map[bool]float64{true: 60, false: 5}[hot]) * rnd() * rnd() * boost))
|
||||
if n > 0 {
|
||||
stats = append(stats, ReadStat{Project: projectID, Path: d.path, Day: day,
|
||||
Kind: ReadKindAgent, Actor: a, Count: n, Last: now})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := journal.Append(filepath.Join(prefix, "journal", "seed.jsonl"), ops); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -161,3 +166,216 @@ func seedDemo(t *testing.T, state, prefix, projectID string) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// demoDocs builds the seeded wiki: a handful of hand-written documents that
|
||||
// render well enough to screenshot, plus plausible filler so the folder
|
||||
// listings and the insights treemap look like a real company's knowledge base.
|
||||
func demoDocs() []doc {
|
||||
var out []doc
|
||||
add := func(path, body string) { out = append(out, doc{path, body}) }
|
||||
|
||||
add("wiki/q3-findings.md", q3Findings)
|
||||
add("wiki/runbooks/incident-response.md", incidentResponse)
|
||||
add("wiki/onboarding/first-week.md", firstWeek)
|
||||
|
||||
// Filler with real-looking names. Bodies follow a per-folder shape so a
|
||||
// reader who opens one sees something plausible rather than lorem ipsum.
|
||||
type group struct {
|
||||
dir string
|
||||
heading string
|
||||
names []string
|
||||
}
|
||||
groups := []group{
|
||||
{"wiki/onboarding", "Onboarding", []string{
|
||||
"engineering-setup", "who-does-what", "glossary", "tools-and-access",
|
||||
"first-pull-request", "how-we-write-docs", "meeting-culture", "expenses",
|
||||
"security-basics", "support-rotation", "vacation-policy",
|
||||
}},
|
||||
{"wiki/architecture", "Architecture", []string{
|
||||
"system-overview", "auth-and-sessions", "data-model", "event-pipeline",
|
||||
"storage-layout", "caching-strategy", "multi-region", "rate-limiting",
|
||||
"background-jobs", "search-indexing", "webhooks-delivery", "observability",
|
||||
"secrets-management", "migration-strategy",
|
||||
}},
|
||||
{"wiki/runbooks", "Runbook", []string{
|
||||
"deploy-and-rollback", "database-failover", "oncall-handoff",
|
||||
"restore-from-backup", "rotate-credentials", "scale-up-workers",
|
||||
"clear-stuck-queue", "expired-certificate", "region-evacuation",
|
||||
"data-export-request", "hotfix-process", "postmortem-template",
|
||||
"paging-policy", "load-shedding", "cache-flush",
|
||||
}},
|
||||
{"wiki/api", "API", []string{
|
||||
"rest-conventions", "authentication", "errors", "pagination",
|
||||
"rate-limits", "webhooks", "versioning", "idempotency",
|
||||
"batch-endpoints", "sdk-guidelines", "deprecation-policy",
|
||||
"changelog", "sandbox-environment",
|
||||
}},
|
||||
{"wiki/decisions", "Decision record", []string{
|
||||
"0001-postgres-over-dynamo", "0002-monorepo", "0003-typescript-everywhere",
|
||||
"0004-no-graphql", "0005-queue-choice", "0006-feature-flags",
|
||||
"0007-tenant-isolation", "0008-append-only-journals", "0009-object-storage",
|
||||
"0010-auth-provider", "0011-observability-stack", "0012-release-cadence",
|
||||
"0013-pricing-model", "0014-support-tiers", "0015-data-retention",
|
||||
"0016-mobile-strategy", "0017-i18n", "0018-schema-migrations",
|
||||
}},
|
||||
{"docs/product", "Product", []string{
|
||||
"pricing-v2", "roadmap-h2", "personas", "activation-metrics",
|
||||
"onboarding-flow", "trial-experiment", "churn-drivers", "packaging",
|
||||
"competitive-positioning", "feature-requests", "beta-program",
|
||||
"launch-checklist", "success-metrics", "pricing-faq",
|
||||
}},
|
||||
{"docs/design", "Design", []string{
|
||||
"design-system", "brand-voice", "iconography", "empty-states",
|
||||
"motion-principles", "accessibility", "dark-mode", "form-patterns",
|
||||
"illustration-style",
|
||||
}},
|
||||
{"notes/research", "Research", []string{
|
||||
"competitive-landscape", "user-interviews-q2", "pricing-sensitivity",
|
||||
"churn-interviews", "market-sizing", "buyer-personas", "win-loss-review",
|
||||
"support-ticket-themes", "nps-verbatims", "usability-round-3",
|
||||
"agent-usage-patterns", "enterprise-requirements",
|
||||
}},
|
||||
{"shared/reports", "Report", []string{
|
||||
"board-update-q3", "churn-analysis", "revenue-review", "hiring-plan",
|
||||
"security-review", "uptime-report", "cost-breakdown", "growth-review",
|
||||
"customer-health", "quarterly-okrs", "annual-plan", "budget-forecast",
|
||||
"partner-review",
|
||||
}},
|
||||
}
|
||||
for _, g := range groups {
|
||||
for _, n := range g.names {
|
||||
out = append(out, doc{
|
||||
path: g.dir + "/" + n + ".md",
|
||||
body: fillerDoc(g.heading, n),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Dated meeting notes: the long tail every wiki has.
|
||||
start := time.Date(2026, 3, 2, 0, 0, 0, 0, time.UTC)
|
||||
kinds := []string{"standup", "retro", "planning", "design-review", "incident-review"}
|
||||
for i := 0; i < 60; i++ {
|
||||
d := start.AddDate(0, 0, i*3)
|
||||
kind := kinds[i%len(kinds)]
|
||||
out = append(out, doc{
|
||||
path: fmt.Sprintf("notes/meetings/%s-%s.md", d.Format("2006-01-02"), kind),
|
||||
body: fillerDoc("Meeting", fmt.Sprintf("%s %s", d.Format("Jan 2"), kind)),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// fillerDoc renders a plausible short document for the long tail.
|
||||
func fillerDoc(heading, slug string) string {
|
||||
title := strings.ToUpper(slug[:1]) + strings.ReplaceAll(slug[1:], "-", " ")
|
||||
return fmt.Sprintf(`# %s
|
||||
|
||||
_%s · owned by the platform team_
|
||||
|
||||
## Summary
|
||||
|
||||
Short context on %s so anyone — human or agent — can act without asking.
|
||||
|
||||
## Details
|
||||
|
||||
- What it covers and who it affects
|
||||
- The constraint that made us choose this
|
||||
- What to do when it changes
|
||||
|
||||
## Related
|
||||
|
||||
See the rest of the %s section.
|
||||
`, title, heading, title, strings.ToLower(heading))
|
||||
}
|
||||
|
||||
// --- hand-written documents (these are the ones that get screenshotted) ---
|
||||
|
||||
const q3Findings = `# Q3 findings
|
||||
|
||||
_Written by Claude on Snow's machine · reviewed by Alice_
|
||||
|
||||
Churn is concentrated in self-serve accounts that never reach a second
|
||||
seat. Everything below follows from that one fact.
|
||||
|
||||
## Headline numbers
|
||||
|
||||
| Metric | Q2 | Q3 | Change |
|
||||
| --- | --- | --- | --- |
|
||||
| Net revenue retention | 104% | 97% | **−7 pts** |
|
||||
| Self-serve churn | 4.1% | 6.8% | +2.7 pts |
|
||||
| Team-plan churn | 1.9% | 1.7% | −0.2 pts |
|
||||
| Median seats at churn | 1.0 | 1.0 | — |
|
||||
|
||||
## What we learned
|
||||
|
||||
1. **Single-seat accounts churn 4× faster.** Accounts that never invite a
|
||||
teammate leave within 40 days on average. Accounts that add a second seat
|
||||
in week one almost never leave.
|
||||
2. **The aha requires two people.** Every retained account has at least one
|
||||
shared folder with activity from more than one machine.
|
||||
3. **Price is not the driver.** Only 6% of exit surveys mention cost;
|
||||
41% say "never really got started".
|
||||
|
||||
## What we are doing about it
|
||||
|
||||
- Make the second seat reachable without a credit card
|
||||
- Move the invite step into the first session, not the settings page
|
||||
- Instrument time-to-second-device as the activation metric
|
||||
|
||||
## Open questions
|
||||
|
||||
- Does a 3-seat free tier cannibalize Team, or feed it?
|
||||
- Can onboarding create the second seat automatically for an agent?
|
||||
`
|
||||
|
||||
const incidentResponse = `# Incident response runbook
|
||||
|
||||
How we handle a production incident, start to finish. **Agents: read this
|
||||
before touching anything during an active incident.**
|
||||
|
||||
## Severity levels
|
||||
|
||||
| Level | Meaning | Response |
|
||||
| --- | --- | --- |
|
||||
| SEV-1 | Customer-facing outage | Page on-call, all hands |
|
||||
| SEV-2 | Degraded service | On-call handles, updates hourly |
|
||||
| SEV-3 | Internal breakage | Ticket, fix within the week |
|
||||
|
||||
## First 15 minutes
|
||||
|
||||
1. Acknowledge the page and open an incident channel.
|
||||
2. Assign an incident commander — one voice, one timeline.
|
||||
3. Freeze deploys:
|
||||
|
||||
` + "```sh\ndeployctl freeze --reason \"SEV-1 in progress\"\n```" + `
|
||||
|
||||
4. Post the first status update **before** debugging.
|
||||
|
||||
## After the incident
|
||||
|
||||
- Write the retro within 48 hours (blameless, timeline-first)
|
||||
- File follow-ups as issues with the ` + "`incident`" + ` label
|
||||
- Update this runbook if reality disagreed with it
|
||||
`
|
||||
|
||||
const firstWeek = `# Your first week
|
||||
|
||||
Welcome. This page is the short version; everything else is linked from here.
|
||||
|
||||
## Day one
|
||||
|
||||
- Get access to the wiki, the repo, and the on-call rotation
|
||||
- Run the setup script and open a pull request that changes one line
|
||||
- Say hello in the team channel
|
||||
|
||||
## Day two to five
|
||||
|
||||
1. Pair with someone on a real ticket
|
||||
2. Read the [architecture overview](../architecture/system-overview.md)
|
||||
3. Shadow an on-call handoff
|
||||
|
||||
## How we work
|
||||
|
||||
We write things down. If you asked a question and the answer was not in the
|
||||
wiki, the answer belongs in the wiki — your agent can add it for you.
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user