diff --git a/.github/actions/setup-env/action.yml b/.github/actions/setup-env/action.yml index 7141e18c..189a595a 100644 --- a/.github/actions/setup-env/action.yml +++ b/.github/actions/setup-env/action.yml @@ -17,6 +17,15 @@ # runners it falls back to a workspace-relative path. Hard-coding # the default silently fails the save with a Path Validation # Error. +# +# 4. Playwright browser cache (~/.cache/ms-playwright) keyed on the +# docs lockfile. ~130 MB Chromium download otherwise re-fetched +# every run. See #132. +# +# 5. Astro content-collection cache (docs/.astro/) keyed on docs +# lockfile + astro.config.mjs. Speeds up warm `astro build` — +# unchanged content skips the parse + transform pipeline. See +# #132. name: Setup CI environment description: Restores caches and sets up Go + pnpm + Node for WaveHouse CI @@ -34,6 +43,12 @@ outputs: pnpm-store-path: description: "Absolute path to the pnpm content-addressable store on this runner — pass to actions/cache/save in the consuming workflow." value: ${{ steps.pnpm-store.outputs.path }} + playwright-cache-hit: + description: "true if the exact-key Playwright browser cache was restored" + value: ${{ steps.playwright-cache.outputs.cache-hit }} + astro-cache-hit: + description: "true if the exact-key Astro content cache was restored" + value: ${{ steps.astro-cache.outputs.cache-hit }} runs: using: composite @@ -84,6 +99,34 @@ runs: restore-keys: | pnpm-${{ runner.os }}- + # Playwright Chromium binary (~130 MB) lives outside the pnpm store at + # ~/.cache/ms-playwright. Without this cache, every CI run cold-pulls + # Chromium during install-docs-playwright — adds 30-60s per run. Keyed + # on docs/pnpm-lock.yaml since that's where the Playwright version is + # locked. Closes the Playwright half of #132. + - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('docs/pnpm-lock.yaml') }} + restore-keys: | + playwright-${{ runner.os }}- + + # Astro's content-collection cache (docs/.astro/data-store.json plus + # generated content types). Restored across runs so unchanged Markdown + # / MDX content skips the parse + transform pipeline. Keyed on + # lockfile + astro.config.mjs since plugin-chain changes invalidate + # the cache shape; intra-key content changes are caught by Astro's + # own file-hash check inside data-store.json. Closes the build-cache + # half of #132. + - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + id: astro-cache + with: + path: docs/.astro + key: astro-${{ runner.os }}-${{ hashFiles('docs/pnpm-lock.yaml', 'docs/astro.config.mjs') }} + restore-keys: | + astro-${{ runner.os }}- + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: "22" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3933d07..382e162e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,8 +43,8 @@ jobs: # Saves run on every branch, gated on `*-cache-hit != 'true'` so a # same-key re-push is a no-op. PRs inherit main's cache scope, so a - # main-push save warms every PR. Three saves mirror three restores - # in setup-env — keep paths and keys in sync. + # main-push save warms every PR. Saves mirror restores in setup-env + # — keep paths and keys in sync. - name: Save Go module + build cache if: steps.setup.outputs.gobuild-cache-hit != 'true' uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 @@ -78,6 +78,20 @@ jobs: path: ${{ steps.setup.outputs.pnpm-store-path }} key: pnpm-${{ runner.os }}-${{ hashFiles('clients/ts/pnpm-lock.yaml', 'tests/e2e/sdk/pnpm-lock.yaml', 'docs/pnpm-lock.yaml') }} + - name: Save Playwright browser cache + if: steps.setup.outputs.playwright-cache-hit != 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('docs/pnpm-lock.yaml') }} + + - name: Save Astro content cache + if: steps.setup.outputs.astro-cache-hit != 'true' + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: docs/.astro + key: astro-${{ runner.os }}-${{ hashFiles('docs/pnpm-lock.yaml', 'docs/astro.config.mjs') }} + # Coverage in the job-summary panel. See #133 for post-launch # reporting decisions. - name: Coverage summary diff --git a/CHANGELOG.md b/CHANGELOG.md index e0aced6e..861bd940 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Astro / Starlight documentation site at `wavehouse.dev`** (`docs/`, `Makefile`, `.github/dependabot.yml`, `.github/workflows/ci.yml`, `.gitignore`, `.vscode/launch.json`): full content + tooling for the public docs (Getting Started, Why WaveHouse, Architecture, API Reference, TypeScript SDK, Configuration, Deployment, Development). Build-time mermaid via `rehype-mermaid` (inline-svg strategy, no client-side JS), LaTeX via `remark-math` + `rehype-katex`, image zoom via `starlight-image-zoom`, expressiveCode with github dark/light + `env`/`dns` Shiki language aliases. PostHog frontend telemetry (key is public by design — embedded in every visitor's browser). `editLink` pointed at `main`; `lastUpdated` on. Sidebar lives in `docs/src/config/sidebar.ts` as the single source of truth, consumed by both Starlight rendering and the LLM-friendly outputs below. Cloudflare Workers + Static Assets deployment via `docs/wrangler.jsonc` (no auto-deploy yet — wired up when the GH Actions workflow lands separately). Make targets stay minimal: `dev-docs` (Astro dev server on :4321), `build-docs`, `preview-docs` — the last serves the production build through `wrangler dev`, so the Cloudflare Worker's content negotiation (`Accept: text/markdown` / LLM-bot User-Agent → `.md` twin) is exercised end-to-end the same way it will be in production. - **Per-page `.md` twin + `llms.txt` family via `starlight-llm-tools`** (`docs/astro.config.mjs`, `docs/worker/index.ts`, `docs/wrangler.jsonc`): every doc is also served as raw markdown at `.md` with a navigation header (Section / Subpages / Related / HTML version pointers), plus three concatenated views — `/llms.txt` manifest, `/llms-full.txt` (every page, sidebar-ordered), `/llms-small.txt` (overview pages only). Page-title slot gains a **Copy Markdown** button and **Open with AI** dropdown (Claude / ChatGPT / Cursor). Content negotiation is handled by a Cloudflare Worker that re-exports the `cloudflare-md-router` package's default handler in one line — `Accept: text/markdown` or known LLM-bot User-Agent (GPTBot, ClaudeBot, PerplexityBot, Applebot-Extended, etc.) gets the `.md` twin transparently, with a fall-through to the HTML response when the twin doesn't exist. - **Two extracted plugin packages, MIT-licensed**: [`github.com/Wave-RF/cloudflare-md-router`](https://github.com/Wave-RF/cloudflare-md-router) (Workers handler + `createMdRouter()` factory + extensible bot-UA regex; consumed in `docs/worker/index.ts` as a one-line re-export) and [`github.com/Wave-RF/starlight-llm-tools`](https://github.com/Wave-RF/starlight-llm-tools) (Starlight plugin that auto-injects the four routes, the two components, and the `PageTitle` override; optionally calls into `starlight-glossary/transform` when present). Both pinned via `github:Wave-RF/...` in `docs/package.json`; lockfile pins the resolved commit SHAs. Pre-npm-publish state — switch to normal version specifiers when the packages land on npm. -- **Custom WaveHouse mark + favicon / OG card / Starlight nav logo / README header, fed by a single-source `make branding` pipeline** (`scripts/branding/{mark.svg,og.template.svg,generate.sh}`, `Makefile`, `docs/astro.config.mjs`, `docs/public/{favicon.svg,favicon.ico,apple-touch-icon.png,og.png}`, `docs/src/assets/branding/wavehouse-mark-{light,dark}.svg`, `README.md`): replaces the default Astro sparkle favicon and the 🏖️ emoji in the README header with a teal outlined-house silhouette containing two horizontal wave bands — a literal "WaveHouse" pun that also nods to the ClickHouse "house" naming and reads as multi-layer storage. `scripts/branding/mark.svg` is the single source of truth (one multi-element SVG using `stroke="currentColor"`); `make branding` extracts its inner content + viewBox and emits six derived artifacts via `rsvg-convert` + ImageMagick: a CSS-swap browser favicon (` + diff --git a/docs/public/og.png b/docs/public/og.png index 0912add0..35dc57db 100644 Binary files a/docs/public/og.png and b/docs/public/og.png differ diff --git a/docs/scripts/screenshot.mjs b/docs/scripts/screenshot.mjs new file mode 100644 index 00000000..133e3e19 --- /dev/null +++ b/docs/scripts/screenshot.mjs @@ -0,0 +1,56 @@ +/* Visual regression / preview screenshots for the WaveHouse docs site. + * Run with: pnpm exec node scripts/screenshot.mjs + * Requires the dev server to be running on http://127.0.0.1:4321 + * (the Astro default, which is also what `make dev-docs` starts). */ + +import { chromium } from "playwright"; +import { mkdir } from "node:fs/promises"; +import { resolve } from "node:path"; + +const BASE = process.env.BASE_URL ?? "http://127.0.0.1:4321"; +const OUT = resolve(process.cwd(), "screenshots"); + +const pages = [ + { name: "landing", url: "/", viewport: { width: 1440, height: 900 }, full: true }, + { name: "landing-mobile", url: "/", viewport: { width: 390, height: 844 }, full: true }, + { name: "getting-started", url: "/getting-started", viewport: { width: 1440, height: 900 }, full: false }, + { name: "api", url: "/api", viewport: { width: 1440, height: 900 }, full: false }, + { name: "architecture", url: "/architecture", viewport: { width: 1440, height: 900 }, full: false }, +]; + +await mkdir(OUT, { recursive: true }); + +const browser = await chromium.launch(); +try { + for (const theme of ["dark", "light"]) { + for (const p of pages) { + const ctx = await browser.newContext({ + viewport: p.viewport, + colorScheme: theme === "dark" ? "dark" : "light", + }); + const page = await ctx.newPage(); + await page.goto(`${BASE}${p.url}`, { waitUntil: "networkidle" }); + await page.evaluate((t) => { + document.documentElement.setAttribute("data-theme", t); + try { localStorage.setItem("starlight-theme", t); } catch {} + }, theme); + // Wait on web fonts + the hero's own ready signal — Hero.astro stamps + // `[data-screenshot-ready]` on via animationend on the last + // terminal line (first mount) or synchronously when the replay guard + // suppresses the entrance choreography (subsequent mounts). Pages + // without a hero never get the attribute, so we cap the wait at 5s + // and fall through to the screenshot regardless. + await page.evaluate(() => document.fonts.ready); + await page.waitForSelector("html[data-screenshot-ready]", { timeout: 5000 }).catch(() => {}); + const filename = `${p.name}-${theme}.png`; + await page.screenshot({ + path: resolve(OUT, filename), + fullPage: p.full, + }); + console.log(`✓ ${filename}`); + await ctx.close(); + } + } +} finally { + await browser.close(); +} diff --git a/docs/src/assets/branding/wavehouse-mark-dark.svg b/docs/src/assets/branding/wavehouse-mark-dark.svg index a3d0aae8..4e3db8fb 100644 --- a/docs/src/assets/branding/wavehouse-mark-dark.svg +++ b/docs/src/assets/branding/wavehouse-mark-dark.svg @@ -1 +1 @@ - + diff --git a/docs/src/assets/branding/wavehouse-mark-light.svg b/docs/src/assets/branding/wavehouse-mark-light.svg index 374047bf..11bb46ec 100644 --- a/docs/src/assets/branding/wavehouse-mark-light.svg +++ b/docs/src/assets/branding/wavehouse-mark-light.svg @@ -1 +1 @@ - + diff --git a/docs/src/components/Footer.astro b/docs/src/components/Footer.astro new file mode 100644 index 00000000..85368eb1 --- /dev/null +++ b/docs/src/components/Footer.astro @@ -0,0 +1,166 @@ +--- +const year = new Date().getFullYear(); +--- + + + + diff --git a/docs/src/components/Head.astro b/docs/src/components/Head.astro new file mode 100644 index 00000000..f9549027 --- /dev/null +++ b/docs/src/components/Head.astro @@ -0,0 +1,60 @@ +--- +import Default from "@astrojs/starlight/components/Head.astro"; +import { ClientRouter } from "astro:transitions"; +--- + + +{/* + Two media-scoped theme-color tags so the browser chrome (mobile address bar, + PWA splash) matches the OS preference at first paint — no JS needed. The + script below adds an unconditional override tag only when Starlight's manual + light/dark toggle disagrees with the OS, so the no-JS first-paint case stays + correct for everyone who hasn't explicitly toggled. +*/} + + + + + diff --git a/docs/src/components/Hero.astro b/docs/src/components/Hero.astro new file mode 100644 index 00000000..f8f92b09 --- /dev/null +++ b/docs/src/components/Hero.astro @@ -0,0 +1,723 @@ +--- +type HeroAction = { + text: string; + link: string; + icon?: string; + variant?: "primary" | "secondary" | "minimal"; + attrs?: Record; +}; +type HeroData = { + title?: string; + tagline?: string; + actions?: HeroAction[]; +}; + +// Starlight's stable Hero contract passes props directly to component +// overrides; older Starlight (pre-0.30) exposed the same data via +// Astro.locals.starlightRoute.entry. Prefer props; fall back to locals so a +// minor-version bump that drops one or the other surfaces as a real error +// (entry undefined) rather than a silently-blank hero. +const heroProps = (Astro.props ?? {}) as { + data?: { title?: string; hero?: HeroData }; + entry?: { data?: { title?: string; hero?: HeroData } }; +}; +const route = Astro.locals?.starlightRoute as + | { entry?: { data?: { title?: string; hero?: HeroData } } } + | undefined; +const data = + heroProps.data ?? + heroProps.entry?.data ?? + route?.entry?.data ?? + {}; +const heroData = (data.hero ?? {}) as HeroData; +const heroTitle = heroData.title || data.title || ""; +const tagline = heroData.tagline; +const actions = heroData.actions ?? []; + +if (import.meta.env.DEV && !heroTitle) { + // Surfaces breakage immediately when a Starlight minor-version bump renames + // the prop shape — without this, the hero renders silently blank. + console.warn( + "[Hero] Could not extract heroTitle from Starlight props — the hero will render blank. " + + "Check the prop shape against the installed @astrojs/starlight version.", + ); +} + +type TerminalLine = + | { kind: "command"; cmd: string } + | { kind: "ok"; text: string; dim?: string; accent?: string } + | { kind: "arrow"; text: string; dim?: string } + | { kind: "json"; text: string } + | { kind: "cursor" }; + +const terminalLines: TerminalLine[] = [ + { kind: "command", cmd: "docker run -p 8080:8080 ghcr.io/wave-rf/wavehouse" }, + { kind: "ok", text: "NATS JetStream embedded — ready" }, + { kind: "ok", text: "ClickHouse schema discovered ", dim: "(4 tables)" }, + { kind: "ok", text: "Listening on ", accent: "http://localhost:8080" }, + { kind: "command", cmd: "wave subscribe events --live" }, + { kind: "arrow", text: "events_v1 ", dim: "[SSE 200]" }, + { kind: "json", text: '{"id":"evt_01","kind":"click",...}' }, + { kind: "json", text: '{"id":"evt_02","kind":"view",...}' }, + { kind: "cursor" }, +]; +--- + +{/* + The hero's entrance animations (staggered fade-up on text, terminal lines + typing in) should play once per tab session. Astro View Transitions + re-mount the DOM on every navigation, so without this guard a user who + nav'd /getting-started → / would see the whole opening sequence again. + + The inline script below runs synchronously during HTML parse — including + on view-transition swaps (`data-astro-rerun`) — and stamps `data-hero-played` + on before the section paints when the in-memory flag is already set. + CSS in this file's diff --git a/docs/src/components/SiteTitle.astro b/docs/src/components/SiteTitle.astro new file mode 100644 index 00000000..d1531a26 --- /dev/null +++ b/docs/src/components/SiteTitle.astro @@ -0,0 +1,66 @@ +--- +import config from "virtual:starlight/user-config"; + +const title = + typeof config.title === "string" ? config.title : config.title.en ?? "WaveHouse"; +--- + + + + {title} + + + diff --git a/docs/src/content/docs/api.md b/docs/src/content/docs/api.md index 6cec2aef..07607665 100644 --- a/docs/src/content/docs/api.md +++ b/docs/src/content/docs/api.md @@ -5,7 +5,7 @@ sidebar: order: 5 --- -Every HTTP endpoint WaveHouse exposes — ingest, query, streaming, schema introspection, and admin — with request/response formats, error codes, and examples. Authentication is optional and controlled by `auth.enabled`; see [Configuration](configuration.md#authentication) for the full auth config surface. +Every HTTP endpoint WaveHouse exposes — ingest, query, streaming, schema introspection, and admin — with request/response formats, error codes, and examples. Authentication is optional and controlled by `auth.enabled`; see [Configuration](/configuration#authentication) for the full auth config surface. ## Authentication diff --git a/docs/src/content/docs/architecture.md b/docs/src/content/docs/architecture.md index 8b6365b3..e2ea851a 100644 --- a/docs/src/content/docs/architecture.md +++ b/docs/src/content/docs/architecture.md @@ -90,7 +90,7 @@ The API layer uses [Chi](https://github.com/go-chi/chi) for routing with standar ### `config/` — Configuration -- **config.go** — Loads configuration from YAML file with environment variable overrides (using [cleanenv](https://github.com/ilyakaznacheev/cleanenv)). All settings use `WH_` prefixed env vars. See [Configuration Reference](configuration.md). +- **config.go** — Loads configuration from YAML file with environment variable overrides (using [cleanenv](https://github.com/ilyakaznacheev/cleanenv)). All settings use `WH_` prefixed env vars. See [Configuration Reference](/configuration). ### `dedupe/` — Deduplication (Optional) diff --git a/docs/src/content/docs/configuration.md b/docs/src/content/docs/configuration.md index 2bfbd6cd..e4ffede6 100644 --- a/docs/src/content/docs/configuration.md +++ b/docs/src/content/docs/configuration.md @@ -30,7 +30,7 @@ export WH_CONFIG=/etc/wavehouse/config.yaml | YAML Key | Env Var | Default | Description | | -------- | ------- | ------- | ----------- | -| `data_dir` | `WH_DATA_DIR` | `./data` | Root directory for embedded state. NATS JetStream lives at `/nats`; Pebble (when dedupe is enabled) at `/pebble`. Subdirectory names are conventions, not config — one knob, one mount. **In a container this MUST resolve to a host-backed volume**; the relative default is for local binary use. WaveHouse logs a startup `WARN` when the directory is missing or empty (no prior state). See [Persistent Storage](deployment.md#persistent-storage-required-for-containers). | +| `data_dir` | `WH_DATA_DIR` | `./data` | Root directory for embedded state. NATS JetStream lives at `/nats`; Pebble (when dedupe is enabled) at `/pebble`. Subdirectory names are conventions, not config — one knob, one mount. **In a container this MUST resolve to a host-backed volume**; the relative default is for local binary use. WaveHouse logs a startup `WARN` when the directory is missing or empty (no prior state). See [Persistent Storage](/deployment#persistent-storage-required-for-containers). | ### Server diff --git a/docs/src/content/docs/deployment.md b/docs/src/content/docs/deployment.md index 5965b287..cae9be47 100644 --- a/docs/src/content/docs/deployment.md +++ b/docs/src/content/docs/deployment.md @@ -103,7 +103,7 @@ git push origin v0.1.0 ## Environment Variables -All configuration can be set via environment variables. This is the recommended approach for container deployments. See [Configuration Reference](configuration.md) for the full list. +All configuration can be set via environment variables. This is the recommended approach for container deployments. See [Configuration Reference](/configuration) for the full list. Key variables for production: diff --git a/docs/src/content/docs/development.md b/docs/src/content/docs/development.md index dd57e0af..ed661423 100644 --- a/docs/src/content/docs/development.md +++ b/docs/src/content/docs/development.md @@ -5,7 +5,7 @@ sidebar: order: 9 --- -Everything you need to build, test, lint, and contribute to WaveHouse — from first-clone to hot-reload dev server to full end-to-end SDK tests. If you're only trying the product, start with the [Getting Started](getting-started.md) guide instead. +Everything you need to build, test, lint, and contribute to WaveHouse — from first-clone to hot-reload dev server to full end-to-end SDK tests. If you're only trying the product, start with the [Getting Started](/getting-started) guide instead. ## Prerequisites @@ -28,7 +28,7 @@ Run `make tools` once after cloning to populate everything that doesn't have to - **`golangci-lint` v2.11.4** → installed to `.bin/_/` (version-pinned in the Makefile; bumping the version triggers a reinstall). Not in `go.mod` because its dependency tree conflicts with the main module. - **`air` v1.65.1** → installed to `.bin/_/` via `go install`; used by `make dev` for hot-reload. Same exclusion principle as `golangci-lint` — air's transitive deps (Hugo, Sass libs) would bloat `go.sum`. - **Go `tool` deps** (`gotestsum`, `gofumpt`, `goimports`, `govulncheck`, `go-test-coverage`, `deadcode`, `gsa`, `goda`) — pinned in `go.mod` via native `tool` directives (Go 1.24+), invoked with `go tool `. `make tools` runs `go mod download` so they're cached; they compile lazily on first invocation. -- **pnpm deps** for `clients/ts/`, `tests/e2e/sdk/`, and `docs/` (via `pnpm install --frozen-lockfile`). +- **pnpm deps** for `clients/ts/`, `tests/e2e/sdk/`, and `docs/` (via `pnpm install --frozen-lockfile`). `make tools` runs only the pnpm install; the Playwright Chromium binary (~130 MB) is fetched on-demand by `make build-docs` / `make dev-docs` via the `install-docs-playwright` target, so Go-only contributors don't pay the download cost. When you do hit `build-docs` / `dev-docs`, Chromium is required by `rehype-mermaid` (SVG diagram rendering at build time) and `starlight-links-validator`. The `--with-deps` flag (which apt-installs Chromium's system libraries: `libnspr4`, `libnss3`, etc.) is only added when `$CI` is set, so contributor laptops don't get an unexpected `sudo` prompt. On Linux dev machines without those libs already present, run `pnpm exec playwright install-deps chromium` once manually. ### Verify your setup diff --git a/docs/src/content/docs/getting-started.md b/docs/src/content/docs/getting-started.md index 2a2b6267..8b2dd42a 100644 --- a/docs/src/content/docs/getting-started.md +++ b/docs/src/content/docs/getting-started.md @@ -71,7 +71,7 @@ curl -s -X POST http://localhost:8080/v1/query \ -d '{"sql": "SELECT * FROM clicks LIMIT 10"}' ``` -Prefer a type-safe query builder over raw SQL? See the [structured query endpoint](api.md#post-v1tablestablequery--structured-query) or the [TypeScript SDK](sdk.md). +Prefer a type-safe query builder over raw SQL? See the [structured query endpoint](/api#post-v1tablestablequery--structured-query) or the [TypeScript SDK](/sdk). ## 5. Subscribe to real-time updates @@ -85,18 +85,18 @@ curl -N "http://localhost:8080/v1/stream/sse?table=clicks" curl -N "http://localhost:8080/v1/stream/sse?table=clicks&since=2026-03-24T11:00:00Z" ``` -To consume multiple tables on a single connection, use the WebSocket endpoint (`/v1/stream/ws`) with in-band `subscribe` commands — see the [API reference](api.md) for the envelope format. +To consume multiple tables on a single connection, use the WebSocket endpoint (`/v1/stream/ws`) with in-band `subscribe` commands — see the [API reference](/api) for the envelope format. ## Next steps -- **[Architecture](architecture.md)** — how ingest, query, cache, and streaming fit together. -- **[API Reference](api.md)** — every endpoint, request/response shape, and error code. -- **[TypeScript SDK](sdk.md)** — zero-dependency client with query builder, live queries, and codegen. -- **[Configuration](configuration.md)** — full YAML + environment variable reference. -- **[Deployment](deployment.md)** — Docker images, releases, health checks. -- **[Development](development.md)** — building from source, running tests, hot-reload workflow. +- **[Architecture](/architecture)** — how ingest, query, cache, and streaming fit together. +- **[API Reference](/api)** — every endpoint, request/response shape, and error code. +- **[TypeScript SDK](/sdk)** — zero-dependency client with query builder, live queries, and codegen. +- **[Configuration](/configuration)** — full YAML + environment variable reference. +- **[Deployment](/deployment)** — Docker images, releases, health checks. +- **[Development](/development)** — building from source, running tests, hot-reload workflow. ## Going further -- **Enable JWT auth**: set `WH_AUTH_ENABLED=true` and `WH_AUTH_JWT_SECRET=` — see [API Reference — Authentication](api.md#authentication). -- **Enable deduplication**: set `WH_DEDUPE_ENABLED=true` and `WH_DEDUPE_ID_FIELD=event_id` — see [Configuration — Deduplication](configuration.md#deduplication). +- **Enable JWT auth**: set `WH_AUTH_ENABLED=true` and `WH_AUTH_JWT_SECRET=` — see [API Reference — Authentication](/api#authentication). +- **Enable deduplication**: set `WH_DEDUPE_ENABLED=true` and `WH_DEDUPE_ID_FIELD=event_id` — see [Configuration — Deduplication](/configuration#deduplication). diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 34dabcda..67bae2de 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -17,7 +17,30 @@ hero: import { Card, CardGrid, LinkCard } from '@astrojs/starlight/components'; -## Why WaveHouse? +
+
+ 1 + binary + API · worker · NATS · dedup +
+
+ < 100 + ms + warm-cache query path, local bench +
+
+ SSE + + WS + real-time push, gap-filled +
+
+ MIT + open + no vendor lock, no SaaS tier +
+
+ +## Why WaveHouse exists ClickHouse is a phenomenal OLAP database, but pointing a frontend straight at it comes with sharp edges: custom APIs, Kafka queues to avoid "too many parts" errors, replacing-merge logic for deduplication. WaveHouse abstracts all of that into a single, deployable binary so you stop interacting with ClickHouse directly. @@ -50,13 +73,34 @@ If you're building user-facing analytics, **WaveHouse is like Supabase for Click +## Up in five minutes + +One compose file — ClickHouse + WaveHouse — and you have an ingest endpoint, a query endpoint, and a live event stream: + +```bash +# Boot ClickHouse + WaveHouse with one compose file +git clone https://github.com/Wave-RF/WaveHouse.git +cd WaveHouse +docker compose -f deployments/compose/standalone.yaml up -d + +# Ingest an event +curl -X POST http://localhost:8080/v1/ingest/events \ + -H 'content-type: application/json' \ + -d '{"event_type":"page_view","user_id":"u_42","ts":"2026-05-15T12:00:00Z"}' + +# Subscribe to the live stream +curl -N "http://localhost:8080/v1/stream/sse?table=events" +``` + +Five minutes from `git clone` to a live event stream. See the [Getting Started](/getting-started) walkthrough for the full guided tour. + ## How it ships - + One `wavehouse` process — API, batch worker, embedded NATS JetStream, and optional embedded Pebble dedup, all in-process. **The only external dependency is ClickHouse.** Same binary on a laptop, in Docker, or on a production host. -## Start here +## Where to next code { + background: var(--wh-code-bg); + border: 1px solid var(--wh-code-border); + border-radius: 4px; + padding: 0.1em 0.4em; + font-size: 0.9em; + color: var(--wh-ink); +} + +/* macOS-style colored dots for terminal code blocks — kept from prior design */ +.expressive-code .frame.is-terminal figcaption.header::before { + background: #ff5f57 !important; + border-radius: 50% !important; + width: 12px !important; + height: 12px !important; + box-shadow: 20px 0 0 #febc2e, 40px 0 0 #28c840 !important; + -webkit-mask-image: none !important; + mask-image: none !important; +} + +/* Code block container — subtle glow border in dark mode */ +.expressive-code .frame { + border-radius: 0.625rem; + box-shadow: var(--wh-shadow-md); +} + +/* Sidebar: tighter labels, accent indicator on current item */ +.sidebar-content nav > ul > li > a, +.sidebar-content nav > ul > li > details > summary { + font-weight: 500; +} +.sidebar-content a[aria-current="page"] { + background: var(--wh-accent-lo); + color: var(--wh-accent-hi); + border-left: 2px solid var(--wh-accent-hi); + padding-left: calc(var(--sl-sidebar-item-padding-inline) - 2px); +} + +/* Top nav — translucent glass over content (only when scrolled past 0). + Inherits the same opacity as --sl-color-bg-nav so the design-system token + stays the single source of truth (avoids the prior 72%/78% drift). */ +header.header { + backdrop-filter: blur(12px) saturate(140%); + -webkit-backdrop-filter: blur(12px) saturate(140%); + background: var(--sl-color-bg-nav); + border-bottom: 1px solid var(--wh-border); +} + +/* Right-rail TOC — quieter and monospace-y */ +.right-sidebar-panel h2 { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--wh-ink-subtle); + font-weight: 600; +} +.right-sidebar-panel a { + font-size: 0.8125rem; + color: var(--wh-ink-muted); + transition: color 0.12s ease; +} +.right-sidebar-panel a[aria-current="true"] { + color: var(--wh-accent-hi); + font-weight: 500; +} + +/* "Hero" actions: refresh Starlight's default button styles */ +.action.primary { + background: var(--wh-accent); + color: var(--wh-accent-ink); + border: 1px solid var(--wh-accent-hi); + font-weight: 600; + box-shadow: var(--wh-shadow-glow); + transition: transform 0.15s ease, box-shadow 0.15s ease, + background 0.15s ease; +} +.action.primary:hover { + background: var(--wh-accent-hi); + transform: translateY(-1px); +} +.action.minimal { + color: var(--wh-ink); + border: 1px solid var(--wh-border-strong); + background: var(--wh-surface); + font-weight: 500; + transition: border-color 0.15s ease, background 0.15s ease; +} +.action.minimal:hover { + border-color: var(--wh-accent); + background: var(--wh-surface-elevated); +} + +/* Card grid: subtle glow on hover */ +.sl-link-card { + background: var(--wh-surface); + border: 1px solid var(--wh-border); + border-radius: var(--radius-card); + transition: border-color 0.15s ease, transform 0.15s ease, + box-shadow 0.15s ease; +} +.sl-link-card:hover { + border-color: var(--wh-accent); + transform: translateY(-2px); + box-shadow: var(--wh-shadow-glow); +} + +/* Page title — slight gradient on h1 in main content */ +.content-panel h1 { + background: linear-gradient( + 180deg, + var(--wh-ink) 0%, + color-mix(in oklab, var(--wh-ink) 70%, var(--wh-ink-muted)) 100% + ); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +/* ---------- Reusable section-accent system ---------- + * Pages can opt in to a section accent by adding a class to the + * via the template-key frontmatter route. The accent flows + * into the page title's underline and inline anchor color. + * + * TODO(follow-up): the body[data-section=…] selectors below have no + * matcher yet — no Astro layout or component stamps `data-section` + * onto . The H1 underline still renders because + * --wh-section-accent inherits the --wh-accent default. Wire up the + * route-middleware-based stamping in a follow-on PR before adding + * more rules that depend on `data-section`. */ + +body[data-section="start"] { --wh-section-accent: var(--wh-accent); } +body[data-section="guides"] { --wh-section-accent: var(--wh-olive); } +body[data-section="reference"] { --wh-section-accent: var(--wh-terracotta); } +body[data-section="operations"] { --wh-section-accent: var(--wh-sky); } + +/* Underline on page H1 — color tied to the section */ +.content-panel > .sl-container h1::after { + content: ""; + display: block; + width: 3rem; + height: 3px; + margin-top: 0.75rem; + background: var(--wh-section-accent); + border-radius: 999px; +} + +/* ---------- Utility classes for landing page ---------- */ + +.wh-grid-bg { + background-image: + linear-gradient(var(--wh-border) 1px, transparent 1px), + linear-gradient(90deg, var(--wh-border) 1px, transparent 1px); + background-size: 56px 56px; + background-position: -1px -1px; + mask-image: radial-gradient( + ellipse at center, + black 0%, + black 40%, + transparent 75% + ); + -webkit-mask-image: radial-gradient( + ellipse at center, + black 0%, + black 40%, + transparent 75% + ); +} + +.wh-glow-orb { + position: absolute; + border-radius: 999px; + filter: blur(80px); + opacity: 0.35; + pointer-events: none; +} + +.wh-glass { + background: color-mix(in oklab, var(--wh-surface) 88%, transparent); + backdrop-filter: blur(10px) saturate(140%); + -webkit-backdrop-filter: blur(10px) saturate(140%); + border: 1px solid var(--wh-border); +} + +.wh-pill { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.3rem 0.75rem; + border-radius: 999px; + background: var(--wh-surface); + border: 1px solid var(--wh-border); + font-size: 0.8125rem; + color: var(--wh-ink-muted); + font-weight: 500; +} +.wh-pill .dot { + width: 6px; + height: 6px; + border-radius: 999px; + background: var(--wh-accent-hi); + box-shadow: 0 0 8px var(--wh-accent-glow-strong); +} + +/* ---------- Landing-page stat strip ---------- */ + +.wh-stats { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1rem; + margin: 1rem 0 4rem; + padding: 0; +} +.wh-stat { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 1.25rem 1.25rem 1rem; + background: var(--wh-surface); + border: 1px solid var(--wh-border); + border-radius: var(--radius-card); + position: relative; + overflow: hidden; + transition: border-color 0.18s ease, transform 0.18s ease, + box-shadow 0.18s ease; +} +.wh-stat::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + 135deg, + var(--wh-accent-glow) 0%, + transparent 55% + ); + opacity: 0; + transition: opacity 0.18s ease; + pointer-events: none; +} +.wh-stat:hover { + border-color: var(--wh-accent); + transform: translateY(-2px); + box-shadow: var(--wh-shadow-md); +} +.wh-stat:hover::before { + opacity: 1; +} +.wh-stat__value { + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.035em; + line-height: 1; + color: var(--wh-ink); + font-variant-numeric: tabular-nums; +} +.wh-stat__unit { + display: inline-block; + font-size: 0.875rem; + font-weight: 600; + color: var(--wh-accent-hi); + text-transform: lowercase; + letter-spacing: 0.02em; + margin-top: -0.5rem; +} +.wh-stat__label { + font-size: 0.8125rem; + color: var(--wh-ink-subtle); + margin-top: 0.5rem; + line-height: 1.4; +} +.wh-stat:nth-child(2) .wh-stat__unit { color: var(--wh-sky); } +.wh-stat:nth-child(3) .wh-stat__unit { color: var(--wh-olive); } +.wh-stat:nth-child(4) .wh-stat__unit { color: var(--wh-terracotta); } + +@media (max-width: 60rem) { + .wh-stats { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media (max-width: 30rem) { + .wh-stats { + grid-template-columns: 1fr; + } + .wh-stat__value { font-size: 1.875rem; } +} + +/* Reduced-motion guard, scoped to WaveHouse-specific landing/chrome surfaces + so Starlight's own transitions (and any future plugin transitions used for + focus/active feedback) keep working. Hero.astro has its own scoped block + that covers .wh-hero__* internals; the duplicates here are belt-and-braces + for any future external usage of the same classes. + + `animation: none` (vs the older `animation-duration: 0.001ms` pattern) + actually removes the animation — browsers no longer need `animationend` to + fire for reduced-motion users, so the one-frame trick is unnecessary. */ +@media (prefers-reduced-motion: reduce) { + .wh-hero__orb, + .wh-wave, + .wh-node, + .wh-ring, + .wh-mesh-lines line, + .wh-cursor, + .wh-line, + .wh-hero__text > [data-stagger], + .wh-hero__visual, + .wh-stat, + .wh-stat::before, + .sl-link-card, + .action { + animation: none !important; + transition: none !important; + } +} diff --git a/scripts/branding/generate.sh b/scripts/branding/generate.sh index 811f6ee4..b501e98c 100755 --- a/scripts/branding/generate.sh +++ b/scripts/branding/generate.sh @@ -26,17 +26,21 @@ ROOT="$(cd "$HERE/../.." && pwd)" source "$ROOT/scripts/_colors.sh" # --- Branding configuration --------------------------------------------------- -# Mark stroke colors — must coexist with $sl-color-accent in docs/src/styles/custom.css. -COLOR_LIGHT='#0e7f8f' # deep teal — mark on light backgrounds -COLOR_DARK='#5bbfcf' # bright cyan — mark on dark backgrounds +# Mark stroke colors — must coexist with --wh-accent in docs/src/styles/global.css. +COLOR_LIGHT='#086D77' # deep teal — mark on light backgrounds (matches --wh-accent in light mode) +COLOR_DARK='#06B0BF' # electric blue — mark on dark backgrounds (Wave RF parent brand) # Apple-touch icon is a solid app-icon tile (iOS doesn't theme-swap home-screen icons). -COLOR_TOUCH_BG='#0e7f8f' -COLOR_TOUCH_FG='#ffffff' +COLOR_TOUCH_BG='#0A0B0E' +COLOR_TOUCH_FG='#06B0BF' # OG card mark color — must match the brightest accent the template uses. -COLOR_OG='#5bbfcf' +COLOR_OG='#06B0BF' +# OG card wordmark color — off-white for contrast against the mark accent. +# Picked to match the tagline / wordmark color elsewhere in the OG template. +COLOR_OG_WM='#F1F3F7' # --- Source + output paths ---------------------------------------------------- MARK_SRC="$HERE/mark.svg" +LOCKUP_SRC="$HERE/lockup.svg" OG_TEMPLATE="$HERE/og.template.svg" OUT_PUBLIC="$ROOT/docs/public" OUT_BRANDING="$ROOT/docs/src/assets/branding" @@ -55,24 +59,45 @@ if [ "$missing" -ne 0 ]; then exit 1 fi -# --- Parse the master mark ---------------------------------------------------- +# --- Parse the master mark + lockup ------------------------------------------- # Pull viewBox and the inner SVG content (everything between and -# ) out of mark.svg. We keep mark.svg on a single line so a one-pass -# sed extraction works — if you reformat it across lines, fix this parser. +# ) out of each source file. Both are kept on a single line so a one-pass +# sed extraction works — if you reformat them across lines, fix this parser. +# +# mark.svg is the standalone bug (used in favicon, apple-touch-icon, Starlight +# nav). lockup.svg is the horizontal mark+wordmark unit (used in the OG card +# so the wordmark's font metrics + mark scale + gap are all designed-in instead +# of laid out ad-hoc inside og.template.svg). MARK_VIEWBOX=$(grep -oE 'viewBox="[^"]+"' "$MARK_SRC" | head -1 | sed 's/^viewBox="//;s/"$//') MARK_INNER=$(sed -E 's|^.*]*>||; s|.*$||' "$MARK_SRC") +LOCKUP_INNER=$(sed -E 's|^.*]*>||; s|.*$||' "$LOCKUP_SRC") if [ -z "$MARK_INNER" ] || [ -z "$MARK_VIEWBOX" ]; then printf '%serror:%s could not extract inner content or viewBox from %s\n' "$RED" "$RESET" "$MARK_SRC" >&2 exit 1 fi +if [ -z "$LOCKUP_INNER" ]; then + printf '%serror:%s could not extract inner content from %s\n' "$RED" "$RESET" "$LOCKUP_SRC" >&2 + exit 1 +fi -# Helper: print the inner content with currentColor swapped for a hex string. -# Avoids forking a subshell per output by going through a function. +# Helpers: print mark/lockup inner content with color placeholders swapped for +# real hex strings. Avoids forking a subshell per output. +# +# The lockup is intentionally two-color: `currentColor` is the mark stroke/fill +# (the "WaveHouse" pun-mark) and `__WM_FILL__` is the wordmark text. On +# light/dark single-color renders we pass the same hex for both so the lockup +# reads as one unit; on the OG card we pass an accent for the mark + white for +# the wordmark, which preserves the visual hierarchy of "logo + headline". mark_with_color() { local color=$1 printf '%s' "$MARK_INNER" | sed "s|currentColor|$color|g" } +lockup_with_colors() { + local mark_color=$1 + local wm_color=$2 + printf '%s' "$LOCKUP_INNER" | sed "s|currentColor|$mark_color|g; s|__WM_FILL__|$wm_color|g" +} printf '%s==> Generating branding from %s%s\n' "$CYAN" "${MARK_SRC#"$ROOT/"}" "$RESET" @@ -82,10 +107,10 @@ trap 'rm -rf "$TMP"' EXIT # --- 1. favicon.svg ----------------------------------------------------------- # Browsers respect prefers-color-scheme inside SVG favicons specifically — the -# +$MARK_INNER EOF # --- 2. Starlight nav logos (light + dark) ------------------------------------ @@ -108,18 +133,18 @@ magick "$TMP/fav-16.png" "$TMP/fav-32.png" "$TMP/fav-48.png" "$OUT_PUBLIC/favico # Mark scaled to ~70% of the tile, centred, white on teal. iOS clips to a # rounded square automatically — keep the mark well clear of the edges. cat > "$TMP/touch.svg" <$(mark_with_color "$COLOR_TOUCH_FG") +$(mark_with_color "$COLOR_TOUCH_FG") EOF rsvg-convert -w 180 -h 180 "$TMP/touch.svg" -o "$OUT_PUBLIC/apple-touch-icon.png" # --- 5. og.png (1200x630 social card) ----------------------------------------- -# og.template.svg has the composition (gradient, wordmark, tagline, wave -# texture) baked in — we splice the mark's inner content into its -# __MARK_GROUP__ slot, recolored for the dark background. +# og.template.svg has the composition (gradient, accent grid, glow, tagline, +# footer row) baked in — we splice the canonical lockup (mark + wordmark unit) +# into its __LOCKUP_GROUP__ slot, recolored for the dark background. # Escape `&` (special on the sed replacement side) but nothing else: SVG path # data + attributes never contain `|`, so we can use `|` as the s delimiter. -mark_og_escaped=$(mark_with_color "$COLOR_OG" | sed 's/&/\\&/g') -sed "s|__MARK_GROUP__|$mark_og_escaped|" "$OG_TEMPLATE" > "$TMP/og.svg" +lockup_og_escaped=$(lockup_with_colors "$COLOR_OG" "$COLOR_OG_WM" | sed 's/&/\\&/g') +sed "s|__LOCKUP_GROUP__|$lockup_og_escaped|" "$OG_TEMPLATE" > "$TMP/og.svg" rsvg-convert -w 1200 -h 630 "$TMP/og.svg" -o "$OUT_PUBLIC/og.png" # --- Report ------------------------------------------------------------------- diff --git a/scripts/branding/lockup.svg b/scripts/branding/lockup.svg new file mode 100644 index 00000000..0101ab27 --- /dev/null +++ b/scripts/branding/lockup.svg @@ -0,0 +1 @@ +WaveHouse diff --git a/scripts/branding/mark.svg b/scripts/branding/mark.svg index 4d1f1bea..b311a171 100644 --- a/scripts/branding/mark.svg +++ b/scripts/branding/mark.svg @@ -1 +1 @@ - + diff --git a/scripts/branding/og.template.svg b/scripts/branding/og.template.svg index 246103a1..c6086768 100644 --- a/scripts/branding/og.template.svg +++ b/scripts/branding/og.template.svg @@ -1,38 +1,58 @@ - - + + + + + + + + + + - + + + - - - - + + + __LOCKUP_GROUP__ - - - __MARK_GROUP__ - - - WaveHouse - - - The open-source real-time API gateway for ClickHouse + + + The open-source real-time API gateway for ClickHouse. + Schema-aware ingest, async batching, real-time streaming. - - SCHEMA-AWARE INGEST · ASYNC BATCHING · REAL-TIME STREAMING · TIERED CACHING - + + A Wave RF open-source project + wavehouse.dev