diff --git a/README.md b/README.md index 5270f2f..75d7f6a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# AV Planner +# Diagrav Plan out AV/network installs: define devices and their ports, wire them together on a canvas, and get a bill of materials (devices + cables, with lengths) for diff --git a/deployment-plan.md b/deployment-plan.md new file mode 100644 index 0000000..cddf6b9 --- /dev/null +++ b/deployment-plan.md @@ -0,0 +1,188 @@ +# Deployment & CI/CD planning notes + +Discussion notes only — nothing here has been implemented yet. Written so a +different Claude Code conversation on this repo can pick up where this one +left off without re-deriving it. Nothing below is committed to as final; it's +a record of what was discussed and concluded, plus open decisions. + +## Hosting shape (discussed, not yet implemented) + +The app is a static frontend (Vite build, no server of its own) + Supabase +backend (Postgres, Auth, RLS, one Edge Function). [organized-ideas.md](organized-ideas.md) +already commits to this being provider-agnostic (Supabase Cloud, self-hosted +Supabase Docker stack, or export the Postgres DB elsewhere — no app code +changes needed either way). + +To go live: +1. **Backend**: create a Supabase Cloud project, `supabase link` + `supabase db push` + to apply the 15 existing migrations, `supabase functions deploy admin-user-action`. + Set real Google OAuth credentials for the production redirect URL — right + now `supabase/config.toml` and `vite.config.ts` are hardcoded to + `127.0.0.1` with `skip_nonce_check = true` for local dev; this needs a + production config pass. Needs a real SMTP provider for auth emails + (Resend suggested in organized-ideas.md as the default) since Inbucket + (local fake mail catcher) doesn't exist in the cloud. +2. **Frontend**: `npm run build` → static `dist/`, deploy to any static host + (Vercel/Netlify/Cloudflare Pages, or self-hosted). Set + `VITE_SUPABASE_URL` / `VITE_SUPABASE_ANON_KEY` as build-time env vars + pointing at the real Supabase project (Vite bakes these in at build + time — not runtime-configurable without a refactor). + +## Marketing site (`site/`) + +A separate, informational "what is this thing" page lives in `site/` at the +repo root — plain hand-written HTML/CSS (`index.html` + `style.css` + +`images/`), no build step, not part of the Vite app or its `src/` tree at +all. Screenshots under `images/` are real captures from the app (a demo +DAWless rig wired up for the purpose), referenced with relative paths, so +the folder is fully self-contained. + +Deploys the same way the app's frontend would, just simpler — no env vars to +bake in, no build command, just serve the static files as-is. Same free-tier +host options apply (Vercel/Netlify/Cloudflare Pages, or self-hosted). + +**Domain purchased: `diagrav.com`.** The app was renamed from "AV Planner" to +**Diagrav** to match. The site's nav ("Log in" / "Sign up") and its closing +CTA ("Sign up free") now link to `https://app.diagrav.com/` — so the +subdomain-split option below is the working assumption baked into the site +as of now, even though neither `diagrav.com` nor `app.diagrav.com` is +actually deployed anywhere yet. Those links will 404 until the app is +actually live at that subdomain; treat this as a leaning, not a final +decision — swapping to the path-based option only means changing the few +`href`s in `site/index.html`, nothing structural. +- **Subdomain split (current assumption)**: bare `diagrav.com` for the + marketing site, `app.diagrav.com` for the actual tool — cleanest + separation, two hosting targets instead of one. +- **Same domain, different path** (marketing at `/`, app at `/app` or + similar) — one hosting target, but means the static host needs to route + `/` to `site/` and everything else to the Vite `dist/` output, which the + simple "just deploy `dist/`" flow above doesn't currently account for. + +Whichever is chosen, this needs its own line in the CI/CD plan below +(currently only the app's frontend/backend deploy steps are scoped) — it's a +third deployable alongside "frontend" and "backend," not an afterthought +folded into the frontend step. + +## Cost / free-tier findings + +Provider: **Supabase**, since the app already codes directly against its +SDK/Auth/RLS model (`src/data/Supabase*.ts`) — switching providers would mean +rewriting those repositories. + +Current Supabase Free plan limits (confirmed against supabase.com/pricing, +not from training data — re-verify if this doc is read much later): + +| Resource | Free limit | +|---|---| +| Database size | 500 MB | +| Egress | 10 GB/mo (5 GB cached + 5 GB uncached) | +| Monthly active users | 50,000 | +| Edge Function invocations | 500,000/mo | +| File storage | 1 GB | +| Realtime concurrent connections | 200 | +| Inactivity | paused after 1 week idle, max 2 active projects | + +**Conclusion: this app is storage-bound, not bandwidth-bound.** File storage, +realtime, and edge function limits are all non-issues (no file uploads, no +`supabase.channel(...)` usage anywhere in `src/`, and the one edge function +is admin-only/rare). Egress is generous relative to what this app transfers +per session. The one limit likely to actually bite is **database size** +(500 MB) — driven less by user-count/traffic than by long-term accumulation +in `diagrams` + `diagram_snapshots`. + +**Specific risk found**: `SupabaseDiagramRepository.ts` (`maybeWriteSnapshot`, +~line 70-93) writes a new version-history snapshot row every 5 minutes of +active editing, with **no retention/expiry** — they accumulate indefinitely. +A user who edits diagrams regularly for months will grow the DB more than a +burst of new signups would. **Suggested follow-up (not yet done): add +snapshot retention** — e.g. keep the last N snapshots per diagram, or prune +anything older than ~90 days. This is the highest-leverage lever for keeping +DB size (and therefore cost/free-tier headroom) under control. + +Rough scale ballpark discussed: solo/small-team use won't come close to any +limit for years. Somewhere in the ~300–1,500 regular-active-user range with +several diagrams each is roughly where the 500MB DB cap would start to bite, +well before the 10GB egress cap or 50k MAU cap would matter. Treat this as a +rough estimate, not a measured number — worth checking real diagram JSON +size in Studio once there's real usage. + +Estimated cost: **$0/mo** to start (Supabase Free + Vercel/Netlify/Cloudflare +Pages free tier + Resend free tier for auth email), **$25/mo** once you want +Supabase Pro (no project auto-pause, higher limits across the board). + +## CI/CD plan: Gitea + Woodpecker, pushbutton staging/prod + +Repo is self-hosted on Gitea (`git.halfbinary.net/aarbit/av-planner`) with +Woodpecker CI already in use elsewhere. No `.woodpecker.yml` exists yet in +this repo. + +**Desired flow**: PR merges to `main` → pipeline runs lint/typecheck/build +and stops → a human clicks a "Deploy" button to push to staging, and +separately clicks it again to promote to production. Not fully automatic on +merge — deploys to both environments are manual/pushbutton. + +**Mechanism (confirmed against current Woodpecker docs)**: Woodpecker has a +built-in **deploy event** for exactly this. Enable **"Allow deployments"** in +the repo's Woodpecker project settings, and any successful pipeline run gets +a **Deploy** button in the UI. Clicking it re-runs that pipeline with +`event: deploy` and a `deploy_to` string you type (e.g. `staging` or +`production`), exposed to steps as `$CI_PIPELINE_DEPLOY_TARGET`. Steps gate +on it, e.g.: + +```yaml +steps: + - name: deploy-staging + when: + - event: deploy + evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "staging"' + - name: deploy-prod + when: + - event: deploy + evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"' +``` + +**What needs to exist**: +1. **Two Supabase projects** — staging and prod. This exactly fills the free + tier's "2 active projects" cap (see above) — no free-tier headroom left + for a third project if one is ever wanted later. +2. **Two frontend hosting targets** — two sites/deployments (SaaS host or + self-hosted alongside Gitea/Woodpecker — not yet decided which). +3. **Known wrinkle**: because Vite bakes `VITE_SUPABASE_URL`/`VITE_SUPABASE_ANON_KEY` + in at build time, you can't build once and promote the identical artifact + to both environments — each deploy target needs its own build with its + own env vars baked in. Not hard, just means "deploy" reruns the build + rather than reusing one artifact. A non-issue at this app's scale. +4. **Migrations run per-target** via Supabase CLI, scripted per environment: + ```bash + supabase link --project-ref $SUPABASE_PROJECT_REF + supabase db push + supabase functions deploy admin-user-action + ``` +5. **Secrets needed in Woodpecker** (scoped per target): `SUPABASE_ACCESS_TOKEN`, + staging/prod `SUPABASE_PROJECT_REF`, staging/prod `VITE_SUPABASE_URL` / + `VITE_SUPABASE_ANON_KEY`, plus whatever the frontend host needs (API + token, or SSH key if self-hosted). +6. **Google OAuth**: needs its own authorized redirect URI added in Google + Cloud console for each of the staging and prod domains — one-time setup, + easy to forget. + +**Difficulty estimate discussed**: roughly a focused afternoon — nothing here +fights the tooling (no containers to orchestrate, no server process to run). +Breakdown: `.woodpecker.yml` (~1–2 hrs), second Supabase project + migration +check (~30 min), frontend hosting target wiring (~30–60 min), secrets + +"Allow deployments" toggle (~15 min), end-to-end test/debug (~1–2 hrs). + +## Open decisions (not yet made) + +- Which frontend host: SaaS (Vercel/Netlify/Cloudflare Pages) vs self-hosted + alongside the existing Gitea/Woodpecker infra. +- Where the marketing site (`site/`, see above) lives relative to the app — + leaning subdomain (`diagrav.com` + `app.diagrav.com`, per the links already + in `site/index.html`) over same-domain/different-path, but not finalized — + and whether it gets its own line in the eventual `.woodpecker.yml` or + piggybacks on the frontend deploy step. +- Whether/when to add diagram-snapshot retention (the storage-risk item + above) — not done yet, just identified. +- Actual `.woodpecker.yml` has not been written yet — this doc captures the + plan/shape only, per the user's request to keep this conversation + discussion-only and not make repo changes here. diff --git a/index.html b/index.html index 106969e..99753f4 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - AV Planner + Diagrav
diff --git a/site/images/bom.jpg b/site/images/bom.jpg new file mode 100644 index 0000000..20ca968 Binary files /dev/null and b/site/images/bom.jpg differ diff --git a/site/images/canvas.jpg b/site/images/canvas.jpg new file mode 100644 index 0000000..d4c3ecf Binary files /dev/null and b/site/images/canvas.jpg differ diff --git a/site/images/palette.jpg b/site/images/palette.jpg new file mode 100644 index 0000000..fec1bf6 Binary files /dev/null and b/site/images/palette.jpg differ diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..a976b31 --- /dev/null +++ b/site/index.html @@ -0,0 +1,174 @@ + + + + + + Diagrav — Plan your wiring before you buy a single cable + + + + + + + + + + + + +
+
+ 🔌 Plan the whole rig before you buy anything +

Wire up your gear on screen
before you touch a cable

+

+ Diagrav is a visual planning tool for anyone wiring together audio/video gear or a + DAWless synth setup. Drag devices onto a canvas, connect their ports, and let the app + catch incompatible connections and total up your shopping list — before you've spent a dollar. +

+
+ A wired synth rig diagram in Diagrav, showing a MIDI keyboard, drum machine, synth, mixer, and studio monitors connected with labeled cables +
+
+
+ +
+
+
+

Everything you need to plan a rig, nothing you don't

+

Built for the moment before a purchase — when you're still figuring out what actually connects to what.

+
+
+
+
🖱️
+

Drag-and-drop canvas

+

Place devices from a library of ports and connectors, then draw cables between them exactly like you would on a whiteboard — except it remembers everything.

+
+
+
✅
+

Real compatibility checking

+

Every connector type knows what it physically mates with. Try to wire an XLR into a 1/4" jack and the app tells you why not, instead of finding out at the store.

+
+
+
🧾
+

Automatic shopping list

+

Every cable and device on the canvas rolls up into a bill of materials — grouped by cable type and length, with running costs, so you know exactly what to order.

+
+
+
🎚️
+

Multi-cable bundles

+

A stereo pair or an 8-channel snake is really one purchase, not several. Bundle the individual runs together so the shopping list counts it the way you'd actually buy it.

+
+
+
📚
+

A growing shared library

+

Connector types, cable types, and device templates are shared across everyone using the app — with your own private additions for anything niche or proprietary.

+
+
+
🤝
+

Share & collaborate

+

Share a diagram with view or edit access, keep a version history as it evolves, and pick up right where you left off.

+
+
+
+
+ +
+
+ +
+
+
The canvas
+

See your whole signal path at a glance

+

+ Drop a device onto the canvas, expand it to see its individual ports, and drag a + connection from one port to another. Every wire is a real object — it knows which + two ports it joins, what cable type it needs, and how long a run it is. +

+
    +
  • Devices collapse to a compact view when you don't need the detail
  • +
  • MIDI, audio, video, network, and power connectors, side by side
  • +
  • Bundle related runs (like a stereo pair) into one visually distinct cable
  • +
+
+
+ Wired device diagram on the Diagrav canvas +
+
+ +
+
+
The shopping list
+

A bill of materials that updates itself

+

+ As you wire things up, Diagrav keeps a running bill of materials in the side + panel — every cable type you'll need, grouped by length, with a running cost total. + Bundled cables count as the single item you'll actually order. +

+
    +
  • Cables grouped by type, then by length, so you know exactly what to buy and how many
  • +
  • Device and cable costs roll up into one grand total
  • +
  • Anything missing a cost is flagged, never silently treated as free
  • +
+
+
+ Bill of materials panel showing cables grouped by type and length with a running cost total +
+
+ +
+
+
The library
+

A device catalog that grows with you

+

+ Browse built-in devices and connectors by category or manufacturer, search across + everything at once, and drop in your own custom gear when something isn't in the + catalog yet — synths, mixers, adapters, whatever your rack actually has. +

+
    +
  • Organize by category or by manufacturer, whichever matches how you think
  • +
  • Add your own devices and connector types in minutes
  • +
  • Submit your additions back to the shared catalog for everyone else to use
  • +
+
+
+ Device library panel organized by category, showing built-in and custom devices +
+
+ +
+
+ +
+
+
+

Stop guessing which cable you need

+

Plan the wiring first. Buy the right cables once.

+ Sign up +
+
+
+ + + + + diff --git a/site/style.css b/site/style.css new file mode 100644 index 0000000..f3ac5c6 --- /dev/null +++ b/site/style.css @@ -0,0 +1,412 @@ +:root { + --ink: #1e1b2e; + --ink-soft: #4b4763; + --bg: #fafafa; + --panel: #ffffff; + --border: #e7e5f0; + --indigo: #4f46e5; + --indigo-dark: #4338ca; + --indigo-soft: #eef2ff; + --amber: #d97706; + --teal: #059669; + --shadow: 0 1px 2px rgba(30, 27, 46, 0.04), 0 8px 24px rgba(30, 27, 46, 0.06); + --max: 1080px; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + background: var(--bg); + color: var(--ink); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif; + line-height: 1.55; + -webkit-font-smoothing: antialiased; +} + +img { + max-width: 100%; + display: block; +} + +a { + color: var(--indigo); +} + +.wrap { + max-width: var(--max); + margin: 0 auto; + padding: 0 24px; +} + +/* ---------- Nav ---------- */ + +.nav { + border-bottom: 1px solid var(--border); + background: rgba(250, 250, 250, 0.85); + backdrop-filter: blur(8px); + position: sticky; + top: 0; + z-index: 10; +} + +.nav .wrap { + display: flex; + align-items: center; + justify-content: space-between; + height: 60px; +} + +.brand { + font-weight: 700; + font-size: 17px; + color: var(--ink); + text-decoration: none; + display: flex; + align-items: center; + gap: 8px; +} + +.brand .dot { + width: 9px; + height: 9px; + border-radius: 999px; + background: var(--indigo); + box-shadow: 0 0 0 3px var(--indigo-soft); +} + +.nav-right { + display: flex; + align-items: center; + gap: 32px; +} + +.nav-links { + display: flex; + gap: 28px; + font-size: 14px; + font-weight: 500; +} + +.nav-links a { + color: var(--ink-soft); + text-decoration: none; +} + +.nav-links a:hover { + color: var(--ink); +} + +.nav-auth { + display: flex; + align-items: center; + gap: 18px; +} + +.nav-login { + font-size: 14px; + font-weight: 500; + color: var(--ink-soft); + text-decoration: none; +} + +.nav-login:hover { + color: var(--ink); +} + +@media (max-width: 640px) { + .nav-links { + display: none; + } +} + +/* ---------- Buttons ---------- */ + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 8px; + font-weight: 600; + text-decoration: none; + white-space: nowrap; + border: 1px solid transparent; +} + +.btn-primary { + background: var(--indigo); + color: #fff; +} + +.btn-primary:hover { + background: var(--indigo-dark); +} + +.btn-sm { + font-size: 13px; + padding: 7px 16px; +} + +.btn-lg { + font-size: 15px; + padding: 13px 30px; +} + +/* ---------- Hero ---------- */ + +.hero { + padding: 88px 0 64px; + text-align: center; +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--indigo-dark); + background: var(--indigo-soft); + border-radius: 999px; + padding: 6px 14px; + margin-bottom: 24px; +} + +h1 { + font-size: clamp(32px, 5vw, 52px); + line-height: 1.12; + letter-spacing: -0.02em; + margin: 0 0 20px; +} + +h1 .accent { + color: var(--indigo); +} + +.lede { + font-size: clamp(16px, 2vw, 19px); + color: var(--ink-soft); + max-width: 640px; + margin: 0 auto 36px; +} + +.hero-shot { + margin-top: 8px; + border-radius: 16px; + border: 1px solid var(--border); + box-shadow: var(--shadow); + overflow: hidden; + max-width: 960px; + margin-left: auto; + margin-right: auto; +} + +.hero-shot img { + width: 100%; +} + +/* ---------- Section scaffolding ---------- */ + +section { + padding: 72px 0; +} + +.section-head { + max-width: 620px; + margin: 0 auto 48px; + text-align: center; +} + +.section-head h2 { + font-size: clamp(24px, 3.2vw, 32px); + letter-spacing: -0.01em; + margin: 0 0 12px; +} + +.section-head p { + color: var(--ink-soft); + margin: 0; + font-size: 16px; +} + +/* ---------- Feature grid ---------- */ + +.features { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} + +@media (max-width: 860px) { + .features { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 560px) { + .features { + grid-template-columns: 1fr; + } +} + +.feature { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 14px; + padding: 24px; +} + +.feature .icon { + width: 38px; + height: 38px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + margin-bottom: 16px; + background: var(--indigo-soft); +} + +.feature h3 { + font-size: 16px; + margin: 0 0 8px; +} + +.feature p { + font-size: 14px; + color: var(--ink-soft); + margin: 0; +} + +/* ---------- Showcase (alternating screenshot rows) ---------- */ + +.showcase-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; + align-items: center; + padding: 48px 0; + border-top: 1px solid var(--border); +} + +.showcase-row:first-of-type { + border-top: none; +} + +.showcase-row.reverse .showcase-copy { + order: 2; +} + +.showcase-row.reverse .showcase-shot { + order: 1; +} + +@media (max-width: 800px) { + .showcase-row, + .showcase-row.reverse .showcase-copy, + .showcase-row.reverse .showcase-shot { + order: initial; + } + .showcase-row { + grid-template-columns: 1fr; + gap: 24px; + } +} + +.showcase-tag { + font-size: 12px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--indigo); + margin-bottom: 10px; +} + +.showcase-copy h3 { + font-size: 22px; + margin: 0 0 12px; + letter-spacing: -0.01em; +} + +.showcase-copy p { + color: var(--ink-soft); + font-size: 15px; + margin: 0 0 12px; +} + +.showcase-copy ul { + margin: 16px 0 0; + padding: 0; + list-style: none; + font-size: 14px; + color: var(--ink-soft); +} + +.showcase-copy li { + padding-left: 22px; + position: relative; + margin-bottom: 8px; +} + +.showcase-copy li::before { + content: "✓"; + position: absolute; + left: 0; + color: var(--teal); + font-weight: 700; +} + +.showcase-shot { + border-radius: 14px; + border: 1px solid var(--border); + box-shadow: var(--shadow); + overflow: hidden; +} + +/* ---------- Callout strip ---------- */ + +.strip { + background: var(--ink); + color: #fff; + border-radius: 20px; + padding: 48px; + text-align: center; +} + +.strip h2 { + margin: 0 0 12px; + font-size: clamp(22px, 3vw, 28px); +} + +.strip p { + margin: 0 auto; + color: #c7c4d9; + max-width: 520px; + font-size: 15px; +} + +.strip-cta { + margin-top: 28px; +} + +/* ---------- Footer ---------- */ + +footer { + border-top: 1px solid var(--border); + padding: 32px 0; + font-size: 13px; + color: var(--ink-soft); + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} diff --git a/wrangler.app.jsonc b/wrangler.app.jsonc new file mode 100644 index 0000000..a75b6d0 --- /dev/null +++ b/wrangler.app.jsonc @@ -0,0 +1,19 @@ +// Deploys the built Vite app (dist/) as an assets-only Cloudflare Worker. +// No server-side code — this is purely static hosting for app.diagrav.com. +// Build first (with production VITE_* env vars baked in), then: +// npx wrangler deploy --config wrangler.app.jsonc +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "diagrav-app", + "compatibility_date": "2026-09-25", + "assets": { + "directory": "./dist", + "not_found_handling": "single-page-application" + }, + "routes": [ + { + "pattern": "app.diagrav.com", + "custom_domain": true + } + ] +} diff --git a/wrangler.site.jsonc b/wrangler.site.jsonc new file mode 100644 index 0000000..3c13a52 --- /dev/null +++ b/wrangler.site.jsonc @@ -0,0 +1,19 @@ +// Deploys the hand-written marketing site (site/) as an assets-only +// Cloudflare Worker for the bare diagrav.com domain. No build step — +// serves the folder as-is: +// npx wrangler deploy --config wrangler.site.jsonc +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "diagrav-site", + "compatibility_date": "2026-09-25", + "assets": { + "directory": "./site", + "not_found_handling": "404-page" + }, + "routes": [ + { + "pattern": "diagrav.com", + "custom_domain": true + } + ] +}