Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
77e4b47
docs(spec): UX overhaul design — onboarding, auth, join, channel brow…
dmfarcas Jul 12, 2026
3832e5d
docs(plans): UX overhaul roadmap + 9 workstream implementation plans
dmfarcas Jul 12, 2026
844f6d6
build(docker): multi-stage Dockerfile (bookworm builder → slim runtime)
dmfarcas Jul 12, 2026
01b6030
build(docker): entrypoint auto-generates+persists auth secret, migrat…
dmfarcas Jul 12, 2026
fe130f8
build(docker): single-service compose with one persistent data volume
dmfarcas Jul 12, 2026
b27373a
docs: Docker quickstart (docker compose up → :3030)
dmfarcas Jul 12, 2026
571b51b
merge: WS-1 Docker onboarding (single-container, SQLite, one volume)
dmfarcas Jul 12, 2026
3ab6e20
feat(client): accessible Field primitive with on-blur inline validation
dmfarcas Jul 12, 2026
f662070
feat(client): EmptyState primitive (status + teach + single CTA)
dmfarcas Jul 12, 2026
cdeafd7
feat(client): Kbd shortcut-hint primitive
dmfarcas Jul 12, 2026
4c716eb
feat(client): LiveLog role=log live-region primitive
dmfarcas Jul 12, 2026
5d3ed5f
feat(client): accessible Menu dropdown primitive (role=menu, arrow na…
dmfarcas Jul 12, 2026
0304310
feat(client): deterministic seedDemo helper on window.__bool for the …
dmfarcas Jul 12, 2026
b02f5f4
merge: WS-0 foundation primitives + Ralph seed helper
dmfarcas Jul 12, 2026
b90ea59
fix(client): WS-0 review — arrow-key Menu test, real dev-seed asserti…
dmfarcas Jul 12, 2026
e121f00
design(ws2): Direction-A onboarding + auth mock (setup wizard, auth c…
dmfarcas Jul 12, 2026
d5df6c0
feat(server): invites table + issue/validate/consume store (single-us…
dmfarcas Jul 12, 2026
8b12eb5
feat(server): setup-status + admin-gated invite issuance + public inv…
dmfarcas Jul 12, 2026
77458e5
feat(client): setup-status + invite validate/create fetch wrappers
dmfarcas Jul 12, 2026
79134db
feat(client): pick-your-vibe onboarding theme step (live preview + pe…
dmfarcas Jul 12, 2026
13cede7
feat(client): first-run SetupWizard (admin card → pick-your-vibe), pa…
dmfarcas Jul 12, 2026
e37dba4
feat(client): branded sign-in/register card (Field-based, invite-gate…
dmfarcas Jul 12, 2026
837b0c5
feat(client): AuthGate branches to SetupWizard on first run, LoginFor…
dmfarcas Jul 12, 2026
d69d379
fix(client): move required asterisk outside label element to fix getB…
dmfarcas Jul 12, 2026
801cd96
fix(server): enforce invite-gated registration + first-user admin boo…
dmfarcas Jul 12, 2026
a637aad
fix(client): associate invite error with its input via aria-described…
dmfarcas Jul 12, 2026
e95f719
merge: WS-2 first-run setup + prod-grade auth (invite-gated, admin bo…
dmfarcas Jul 12, 2026
c315f86
feat(client): message list is a role=log live region via WS-0 LiveLog
dmfarcas Jul 12, 2026
f692d9d
feat(client): 'pick a channel' + 'no messages yet' empty states via W…
dmfarcas Jul 12, 2026
469fd68
feat(client): history-loading skeleton (reduced-motion aware) in Mess…
dmfarcas Jul 12, 2026
fabc2f0
feat(client): connection-error + reconnecting banner (icon+text, dism…
dmfarcas Jul 12, 2026
166b6ea
feat(client): responsive shell — mobile sidebar drawer, rail/member c…
dmfarcas Jul 12, 2026
1bc9b6d
merge: WS-7 chat shell polish (role=log, empty/loading/error states, …
dmfarcas Jul 12, 2026
955b066
fix(client): WS-7 code-review findings — tokenize scrim, grouping tes…
dmfarcas Jul 12, 2026
0d0240b
design(ws4): Direction-A network directory mock (dark+light, search/n…
dmfarcas Jul 12, 2026
09f197f
feat(ws4): curated deterministic IRC network catalog (8 well-known ne…
dmfarcas Jul 12, 2026
3b150b3
feat(ws4): NetworkSettings gains optional onBack for advanced-leaf mode
dmfarcas Jul 12, 2026
e6786d6
feat(ws4): NetworkDirectory — searchable catalog, nick capture, auto-…
dmfarcas Jul 12, 2026
709206e
feat(ws4): rail + and AppShell open the network directory as the fron…
dmfarcas Jul 12, 2026
cccd9c2
fix(ws4): survive-unmount auto-connect + e2e spec update + a11y cleanup
dmfarcas Jul 12, 2026
c3c9df8
feat(client): parse the full IRC command set (/notice /away /back /in…
dmfarcas Jul 12, 2026
8c9bcf2
feat(client): store actions + protocol for notice/away/invite/names (…
dmfarcas Jul 12, 2026
645a6ff
feat(client): IRC command catalog (single source for palette + cheats…
dmfarcas Jul 12, 2026
dcf00a7
feat(client): scope chips filter results + APG combobox ARIA (aria-ac…
dmfarcas Jul 12, 2026
09a1ca8
feat(client): full IRC command set in Cmd-K with inline arg hints + K…
dmfarcas Jul 12, 2026
7063924
fix(docker): exclude @bool/desktop from container build
dmfarcas Jul 17, 2026
2950b3a
fix(client): dispatch the WS-6 command kinds from the composer
dmfarcas Jul 17, 2026
7824b23
docs: cold-start deep audit — real stack, real Libera, ranked findings
dmfarcas Jul 17, 2026
085371e
docs(audit): correct F6 — link-preview IS wired in manager; failure i…
dmfarcas Jul 17, 2026
61335d8
docs: 'survives being used' implementation plan — audit spine fixes +…
dmfarcas Jul 17, 2026
93289a0
docs(plan): Phase 2 — the full Slack-level surface (nick popover, con…
dmfarcas Jul 17, 2026
b5134ba
docs(plan): Tasks 15-16 — full Direction-A mock parity (verified gap-…
dmfarcas Jul 17, 2026
0ab0266
fix(server): key incoming DMs by counterparty, not own nick (audit B1)
dmfarcas Jul 18, 2026
007fb72
feat(server): honor notice flag; wire /away /invite /names through IR…
dmfarcas Jul 18, 2026
46c6c41
test(server): cover user:invite happy-path + net:error (Task 2 review…
dmfarcas Jul 18, 2026
9f3c7b1
feat(client): WS auto-reconnect with capped backoff; hello per connec…
dmfarcas Jul 18, 2026
5f508a7
test(client): scope reconnect timer in chat-store test; null retryTim…
dmfarcas Jul 18, 2026
a2ecd03
feat(server): persist joined channels, auto_connect flag, resume IRC …
dmfarcas Jul 18, 2026
ab9dfa5
feat: session:targets snapshot — channels/DMs/unreads survive restart…
dmfarcas Jul 18, 2026
697a08b
fix(client): mobile drawer — display:contents wrapper broke .app > .r…
dmfarcas Jul 18, 2026
916b424
feat(client): zero-state routes to directory; join opens channel; men…
dmfarcas Jul 18, 2026
a7b6318
ci: reality gate — docker build + real IRC smoke incl. restart surviv…
dmfarcas Jul 18, 2026
c069e4d
feat(client): UserPopover — click any nick for Message/Whois/op actio…
dmfarcas Jul 18, 2026
46176ba
ci: unit job builds @bool/shared before typecheck; drop dead smoke he…
dmfarcas Jul 18, 2026
f541a54
fix(client): freeze UserPopover channel context; gate channel actions…
dmfarcas Jul 18, 2026
8b3b3e1
feat: visible per-network connection state + reconnect button; fix de…
dmfarcas Jul 18, 2026
feec552
feat: channel browser on live /list results — no more blank join prom…
dmfarcas Jul 18, 2026
eb4ef00
fix(server): link previews actually fire — observable failures + real…
dmfarcas Jul 18, 2026
a2d37e8
fix(server): surface preview failure reason; type-safe pinned lookup …
dmfarcas Jul 18, 2026
4698de2
feat(client): history-start orientation note + topic truncation fix (…
dmfarcas Jul 18, 2026
0713ae5
polish: ## sigils, hover reactions, human connection copy, nickname h…
dmfarcas Jul 18, 2026
8d1b077
feat(client): mock parity — day bars, join/part lines, inline mention…
dmfarcas Jul 18, 2026
a4c1a50
fix(client): only synthesize 'you joined' system line for genuinely n…
dmfarcas Jul 18, 2026
64c1c2f
feat: presence parity — away-notify, DM dots, sidebar filter, compose…
dmfarcas Jul 18, 2026
a7ea9cf
fix(client): preserve caps/live-state across net:list on reconnect; g…
dmfarcas Jul 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# .dockerignore
**/node_modules
**/dist
**/.turbo
**/coverage
**/*.log
.git
.github
docs
design
*.db
*.db-*
media
.env
.env.*
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# .github/workflows/ci.yml
name: ci
on:
push:
branches: [main, 'feat/**']
pull_request:

jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with: { node-version: 22, cache: pnpm }
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm -r test

smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- name: Build and start the real stack
run: docker compose -f docker-compose.ci.yml up -d --build
- name: Install smoke deps
run: npm install ws@8
- name: Run the cold-start smoke (signup → IRC → restart → intact)
run: BOOL_IRC_HOST=irc node tools/ci-smoke.mjs
- name: Dump logs on failure
if: failure()
run: docker compose -f docker-compose.ci.yml logs
45 changes: 45 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# syntax=docker/dockerfile:1

# ---- Builder: compile all workspace packages (needs toolchain for better-sqlite3) ----
FROM node:20-bookworm AS builder
WORKDIR /app
RUN npm install -g pnpm@9

# Install deps with the full workspace manifest set for better layer caching
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml* ./
COPY packages/client/package.json packages/client/
COPY packages/server/package.json packages/server/
COPY packages/shared/package.json packages/shared/
RUN pnpm install --frozen-lockfile || pnpm install

# Copy sources and build everything (shared → server dist, client → static assets).
# The desktop (Electron) package is not part of the container — its deps are not
# installed above, so exclude it from the recursive build.
COPY . .
RUN pnpm --filter '!@bool/desktop' -r build

# ---- Runtime: slim image, run the Fastify server ----
FROM node:20-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production \
HOST=0.0.0.0 \
PORT=3030 \
BOOL_DB=/data/bool.db \
BOOL_MEDIA_DIR=/data/media

RUN npm install -g pnpm@9 \
&& mkdir -p /data/media

# Bring the built workspace (includes node_modules with the compiled better-sqlite3
# binding, server dist, client dist, and the better-auth CLI used by migrate).
COPY --from=builder /app /app
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

EXPOSE 3030
VOLUME ["/data"]

HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=5 \
CMD node -e "fetch('http://127.0.0.1:'+ (process.env.PORT||3030) +'/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,24 @@ pnpm --filter @bool/server db:migrate # run migrations manually
The server runs migrations automatically before starting, so a fresh deploy
needs no manual DB step.

## Self-hosting with Docker

bool runs as a single container with an embedded SQLite database — no external
services required.

```bash
git clone <this-repo> bool && cd bool
docker compose up -d
```

Open **http://localhost:3030**. The first visit walks you through creating the
admin account — there is no config file to edit. All data (database + uploads)
persists in the `bool-data` volume.

To run behind a domain, set `BETTER_AUTH_URL` (and, if serving the API from a
different origin, `BOOL_TRUSTED_ORIGINS`) in `docker-compose.yml`, then
`docker compose up -d --build`.

## 🗺️ Roadmap

Built milestone by milestone, each with its own design spec + plan (see `docs/`).
Expand Down
114 changes: 114 additions & 0 deletions design/network-directory.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>bool — Network Directory</title>
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{
--bg-0:#0a0c0d;--bg-1:#0e1113;--bg-2:#101416;--bg-3:#151a1d;--bg-4:#1b2226;
--line:#20282c;--line-2:#2a343a;--ink-0:#e6efe9;--ink-1:#9fb1ab;--ink-2:#63756f;--ink-3:#455049;
--green:#39e08b;--green-dim:#1f7a4c;--amber:#ffb454;--cyan:#4fd6e0;--blue:#7aa2f7;--violet:#b48ead;--red:#ff6b6b;
--on-accent:#06110b;--scrim:rgba(4,6,7,.68);
--radius:12px;--radius-sm:8px;
--mono:'JetBrains Mono',ui-monospace,monospace;--sans:'IBM Plex Sans',system-ui,sans-serif;
}
[data-theme="light"]{
--bg-0:#f4f6f5;--bg-1:#ffffff;--bg-2:#ffffff;--bg-3:#eef1f0;--bg-4:#e4e9e7;
--line:#dde3e1;--line-2:#c9d2cf;--ink-0:#0f1a16;--ink-1:#33433d;--ink-2:#5c6b64;--ink-3:#8a978f;
--green:#12915a;--green-dim:#9fe0c0;--on-accent:#ffffff;--scrim:rgba(20,30,26,.35);
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--sans);background:var(--bg-0);color:var(--ink-0);min-height:100vh;-webkit-font-smoothing:antialiased;padding:40px 16px}
.toggle{position:fixed;top:14px;right:14px;font-family:var(--mono);font-size:11px;color:var(--ink-1);background:var(--bg-3);border:1px solid var(--line-2);border-radius:6px;padding:6px 10px;cursor:pointer;z-index:10}
.stack{display:flex;flex-direction:column;gap:28px;align-items:center}
.panel-label{font-family:var(--mono);font-size:10px;letter-spacing:1.4px;text-transform:uppercase;color:var(--ink-3);width:min(560px,92vw)}

.card{width:min(560px,92vw);background:var(--bg-2);border:1px solid var(--line-2);border-radius:var(--radius);
box-shadow:0 40px 120px -20px rgba(0,0,0,.6);overflow:hidden}
.card-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px 12px;border-bottom:1px solid var(--line)}
.card-title{font-family:var(--mono);font-size:13px;font-weight:600;letter-spacing:.02em}
.card-x{background:none;border:none;color:var(--ink-2);font-family:var(--mono);font-size:16px;cursor:pointer}
.card-body{padding:16px 18px}

.search{display:flex;align-items:center;gap:10px;background:var(--bg-3);border:1px solid var(--line-2);border-radius:var(--radius-sm);padding:9px 12px;margin-bottom:14px}
.search .pmt{font-family:var(--mono);color:var(--green);font-weight:700}
.search input{flex:1;background:none;border:none;outline:none;color:var(--ink-0);font-family:var(--sans);font-size:14px}
.search input::placeholder{color:var(--ink-3)}

.net{display:flex;align-items:center;gap:12px;padding:11px 12px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--bg-3);margin-bottom:8px;cursor:pointer;text-align:left;width:100%;font:inherit;color:inherit}
.net:hover,.net:focus-visible{border-color:var(--green-dim);outline:none;box-shadow:0 0 0 1px var(--green-dim)}
.net .mark{width:34px;height:34px;border-radius:8px;display:grid;place-items:center;flex:none;font-family:var(--mono);font-weight:700;font-size:12px;background:var(--bg-4);color:var(--green);border:1px solid var(--line)}
.net .main{min-width:0;flex:1}
.net .name{font-size:14px;font-weight:600}
.net .desc{font-family:var(--sans);font-size:12px;color:var(--ink-2);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.net .tag{font-family:var(--mono);font-size:10px;color:var(--ink-2);background:var(--bg-4);border:1px solid var(--line);border-radius:4px;padding:2px 7px;flex:none}

.advanced{margin-top:6px;text-align:center}
.advanced button{background:none;border:none;color:var(--ink-1);font-family:var(--mono);font-size:12px;cursor:pointer;text-decoration:underline;text-underline-offset:3px}

/* Empty state */
.empty{display:flex;flex-direction:column;align-items:center;gap:8px;padding:40px 24px;text-align:center}
.empty h2{font-family:var(--sans);font-size:15px}
.empty p{font-family:var(--sans);font-size:13px;color:var(--ink-2);max-width:320px}
.empty .cta{margin-top:8px;font-family:var(--mono);font-size:13px;font-weight:600;padding:8px 16px;border-radius:var(--radius-sm);border:1px solid var(--green);background:var(--green);color:var(--on-accent);cursor:pointer}

/* Nick capture sub-state */
.nickcap label{display:block;font-family:var(--mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-1);margin-bottom:4px}
.nickcap input{width:100%;font-family:var(--mono);font-size:13px;color:var(--ink-0);background:var(--bg-3);border:1px solid var(--line-2);border-radius:var(--radius-sm);padding:8px 9px;outline:none}
.nickcap .chosen{font-family:var(--mono);font-size:12px;color:var(--ink-2);margin-bottom:12px}
.nickcap .chosen b{color:var(--green)}
.nickcap .row{display:flex;gap:8px;margin-top:14px}
.btn{font-family:var(--mono);font-size:13px;font-weight:600;padding:8px 16px;border-radius:var(--radius-sm);cursor:pointer}
.btn-go{border:1px solid var(--green);background:var(--green);color:var(--on-accent)}
.btn-back{border:1px solid var(--line-2);background:none;color:var(--ink-1)}
</style>
</head>
<body>
<button class="toggle" onclick="document.documentElement.dataset.theme = document.documentElement.dataset.theme==='light'?'dark':'light'">theme</button>
<div class="stack">

<div class="panel-label">Directory — searchable list, advanced link</div>
<div class="card">
<div class="card-head"><span class="card-title">Add a network</span><button class="card-x" aria-label="Close">✕</button></div>
<div class="card-body">
<div class="search"><span class="pmt">/</span><input placeholder="Search networks — name or description" value=""></div>
<button class="net"><span class="mark">LC</span><div class="main"><div class="name">Libera.Chat</div><div class="desc">Home of free/open-source software projects</div></div><span class="tag">FOSS</span></button>
<button class="net"><span class="mark">OF</span><div class="main"><div class="name">OFTC</div><div class="desc">Open and Free Technology Community</div></div><span class="tag">FOSS</span></button>
<button class="net"><span class="mark">RZ</span><div class="main"><div class="name">Rizon</div><div class="desc">General chat, anime and gaming communities</div></div><span class="tag">Community</span></button>
<button class="net"><span class="mark">SN</span><div class="main"><div class="name">Snoonet</div><div class="desc">Reddit-affiliated general network</div></div><span class="tag">Community</span></button>
<div class="advanced"><button>Advanced / custom server →</button></div>
</div>
</div>

<div class="panel-label">Nick capture — after selecting a network</div>
<div class="card">
<div class="card-head"><span class="card-title">Join Libera.Chat</span><button class="card-x" aria-label="Close">✕</button></div>
<div class="card-body nickcap">
<div class="chosen">Connecting to <b>irc.libera.chat:6697</b> over TLS</div>
<label for="m-nick">Choose a nick</label>
<input id="m-nick" placeholder="yournick" value="">
<div class="row">
<button class="btn btn-go">Connect</button>
<button class="btn btn-back">← Back</button>
</div>
</div>
</div>

<div class="panel-label">Empty state — no networks configured yet</div>
<div class="card">
<div class="card-head"><span class="card-title">Add a network</span><button class="card-x" aria-label="Close">✕</button></div>
<div class="card-body">
<div class="empty">
<h2>No networks yet</h2>
<p>Pick a network from the directory and choose a nick — bool connects you automatically.</p>
<button class="cta">Browse the directory</button>
</div>
</div>
</div>

</div>
</body>
</html>
106 changes: 106 additions & 0 deletions design/onboarding-auth.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<!-- design/onboarding-auth.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>bool — Onboarding &amp; Auth (Direction A)</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
:root{
--bg-0:#0a0c0d;--bg-1:#0e1113;--bg-2:#101416;--bg-3:#151a1d;--bg-4:#1b2226;
--line:#20282c;--line-2:#2a343a;--ink-0:#e6efe9;--ink-1:#9fb1ab;--ink-2:#63756f;--ink-3:#455049;
--green:#39e08b;--green-dim:#1f7a4c;--amber:#ffb454;--red:#ff6b6b;--on-accent:#0a0c0d;
--mono:'JetBrains Mono',ui-monospace,monospace;--sans:'IBM Plex Sans',system-ui,sans-serif;
--radius:6px;--radius-sm:4px;
}
[data-theme="light"]{
--bg-0:#ffffff;--bg-1:#f4f6f5;--bg-2:#eef1f0;--bg-3:#eaeeec;--bg-4:#dfe5e2;
--line:#d5dcd8;--line-2:#c3ccc7;--ink-0:#141a18;--ink-1:#3a453f;--ink-2:#6a756f;--ink-3:#94a099;
--green:#0f9d58;--green-dim:#0b7a44;--amber:#c47f00;--red:#c53030;--on-accent:#ffffff;
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--sans);background:var(--bg-0);color:var(--ink-0);min-height:100vh;padding:40px 20px;-webkit-font-smoothing:antialiased}
.toggle{position:fixed;top:16px;right:16px;font-family:var(--mono);font-size:11px;color:var(--ink-1);background:var(--bg-3);border:1px solid var(--line-2);border-radius:var(--radius-sm);padding:6px 10px;cursor:pointer}
.wrap{display:flex;flex-wrap:wrap;gap:32px;justify-content:center;align-items:flex-start;max-width:1200px;margin:0 auto}
.card{width:380px;background:var(--bg-1);border:1px solid var(--line-2);border-radius:14px;padding:28px 26px;box-shadow:0 30px 80px -30px rgba(0,0,0,.7)}
.brand{display:flex;align-items:center;gap:10px;margin-bottom:6px}
.brand .logo{font-family:var(--mono);font-weight:700;font-size:22px;color:var(--green)}
.brand .logo b{color:var(--ink-0)}
.sub{font-size:13px;color:var(--ink-2);margin-bottom:22px}
.steps{display:flex;gap:6px;margin-bottom:20px}
.dot{height:4px;flex:1;border-radius:2px;background:var(--line-2)}
.dot.on{background:var(--green)}
label{display:block;font-family:var(--mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-1);margin-bottom:4px}
.fld{margin-bottom:14px}
.inp{position:relative;display:flex}
input{flex:1;width:100%;font-family:var(--mono);font-size:13px;color:var(--ink-0);background:var(--bg-3);border:1px solid var(--line-2);border-radius:var(--radius-sm);padding:8px 9px;outline:none}
input:focus{border-color:var(--green)}
input.bad{border-color:var(--red)}
.reveal{position:absolute;right:6px;top:0;bottom:0;background:none;border:none;color:var(--ink-2);cursor:pointer;font-family:var(--mono);font-size:12px}
.err{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11px;color:var(--red);margin-top:5px}
.rules{font-family:var(--mono);font-size:11px;color:var(--ink-2);margin:2px 0 14px;display:flex;flex-direction:column;gap:2px}
.rules .ok{color:var(--green)}
.btn{width:100%;font-family:var(--mono);font-size:13px;font-weight:600;padding:10px 16px;border-radius:var(--radius-sm);border:1px solid var(--green);background:var(--green);color:var(--on-accent);cursor:pointer;margin-top:6px}
.btn.ghost{background:none;color:var(--ink-1);border-color:var(--line-2);margin-top:12px}
.switch{text-align:center;font-size:12px;color:var(--ink-2);margin-top:14px}
.switch a{color:var(--green);text-decoration:none}
.vibes{display:flex;flex-direction:column;gap:10px;margin-bottom:8px}
.vibe{display:flex;gap:12px;align-items:center;padding:10px;border:1px solid var(--line-2);border-radius:var(--radius);cursor:pointer;background:var(--bg-2)}
.vibe.sel{border-color:var(--green);box-shadow:0 0 0 1px var(--green)}
.vibe .swatch{display:flex;gap:3px}
.vibe .swatch span{width:14px;height:26px;border-radius:3px;border:1px solid rgba(0,0,0,.2)}
.vibe .vt{font-weight:600;font-size:14px}
.vibe .vs{font-family:var(--mono);font-size:11px;color:var(--ink-2)}
h1{font-size:17px;font-weight:600;margin-bottom:4px}
</style>
</head>
<body data-theme="dark">
<button class="toggle" onclick="document.body.setAttribute('data-theme', document.body.getAttribute('data-theme')==='light'?'dark':'light')">toggle light</button>
<div class="wrap">

<!-- (1) First-run setup wizard -->
<section class="card" aria-label="First-run setup">
<div class="brand"><span class="logo">bool<b>_</b></span></div>
<div class="sub">Welcome. Create the admin account — no config files.</div>
<div class="steps"><div class="dot on"></div><div class="dot"></div></div>
<h1>Create your admin account</h1>
<div class="fld"><label>Username</label><div class="inp"><input value="ada" /></div></div>
<div class="fld"><label>Email</label><div class="inp"><input value="ada@example.com" /></div></div>
<div class="fld"><label>Password</label><div class="inp"><input type="password" value="correcthorse" /><button class="reveal">Show</button></div>
<div class="rules"><span class="ok">✓ At least 8 characters</span><span>• Not your username</span></div>
</div>
<button class="btn">Create admin &amp; continue</button>
</section>

<!-- (2) Sign-in / register card -->
<section class="card" aria-label="Sign in">
<div class="brand"><span class="logo">bool<b>_</b></span></div>
<div class="sub">Sign in to your server.</div>
<div class="fld"><label>Username</label><div class="inp"><input value="kai" /></div></div>
<div class="fld"><label>Invite code</label><div class="inp"><input class="bad" value="XY" /></div>
<div class="err"><span aria-hidden="true">⚠</span>Invite code looks incomplete</div>
</div>
<div class="fld"><label>Password</label><div class="inp"><input type="password" value="hunter2hunter" /><button class="reveal">Show</button></div></div>
<button class="btn">Sign in</button>
<div class="switch">Have an invite? <a href="#">Create an account</a></div>
</section>

<!-- (3) Pick-your-vibe theme step -->
<section class="card" aria-label="Pick your vibe">
<div class="brand"><span class="logo">bool<b>_</b></span></div>
<div class="sub">Pick your vibe — you can change this anytime.</div>
<div class="steps"><div class="dot on"></div><div class="dot on"></div></div>
<div class="vibes">
<div class="vibe sel"><div class="swatch"><span style="background:#0b0b16"></span><span style="background:#181733"></span><span style="background:#a6f065"></span><span style="background:#8b7dff"></span></div><div><div class="vt">Friendly</div><div class="vs">Aurora · warm &amp; welcoming</div></div></div>
<div class="vibe"><div class="swatch"><span style="background:#0a0c0d"></span><span style="background:#151a1d"></span><span style="background:#39e08b"></span><span style="background:#ffb454"></span></div><div><div class="vt">Terminal</div><div class="vs">for the IRC natives</div></div></div>
<div class="vibe"><div class="swatch"><span style="background:#17130f"></span><span style="background:#231d17"></span><span style="background:#a3c48f"></span><span style="background:#e8a76b"></span></div><div><div class="vt">Editorial</div><div class="vs">calm &amp; readable</div></div></div>
</div>
<button class="btn">Enter bool</button>
</section>

</div>
</body>
</html>
Binary file added design/shots/audit/01-setup-wizard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added design/shots/audit/02-first-empty-state.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added design/shots/audit/03-cmdk-zero-state.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added design/shots/audit/04-network-directory.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added design/shots/audit/05-first-message.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added design/shots/audit/06-python-live.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added design/shots/audit/07-unreads-dm.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added design/shots/audit/08-mobile-390.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added design/shots/audit/09-after-restart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions docker-compose.ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# docker-compose.ci.yml — bool + a throwaway IRC server, for the smoke gate.
services:
bool:
build: .
ports:
- "3030:3030"
environment:
BETTER_AUTH_URL: http://localhost:3030
volumes:
- bool-ci-data:/data
depends_on:
- irc
irc:
image: ghcr.io/ergochat/ergo:stable
ports:
- "6667:6667"

volumes:
bool-ci-data:
Loading
Loading