Deploy to Cloudflare Workers and rebrand to Diagrav

Adds Workers + static-assets configs for the app (app.diagrav.com) and a
separate marketing/landing site (diagrav.com), plus the deployment runbook
used to stand up Supabase Cloud, Google OAuth, Resend SMTP, and Cloudflare
DNS/custom domains. Renames the app from "AV Planner" to "Diagrav" to match
the new domain.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
2026-09-28 10:05:59 -05:00
co-authored by Claude Sonnet 5
parent a5c2ad87e1
commit 0a7f2f7fee
10 changed files with 814 additions and 2 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# AV Planner # Diagrav
Plan out AV/network installs: define devices and their ports, wire them together 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 on a canvas, and get a bill of materials (devices + cables, with lengths) for
+188
View File
@@ -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.
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AV Planner</title> <title>Diagrav</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+174
View File
@@ -0,0 +1,174 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Diagrav — Plan your wiring before you buy a single cable</title>
<meta name="description" content="Diagrav is a visual tool for planning AV and DAWless synth setups: drag devices onto a canvas, wire them up with real connector compatibility checking, and get an automatic shopping list." />
<link rel="canonical" href="https://diagrav.com/" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://diagrav.com/" />
<meta property="og:title" content="Diagrav" />
<meta property="og:description" content="Plan your AV or DAWless synth wiring visually, catch incompatible connections before you buy anything, and get an automatic shopping list." />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<nav class="nav">
<div class="wrap">
<a class="brand" href="#top"><span class="dot"></span>Diagrav</a>
<div class="nav-right">
<div class="nav-links">
<a href="#features">Features</a>
<a href="#canvas">How it works</a>
<a href="#library">Library</a>
</div>
<div class="nav-auth">
<a class="nav-login" href="https://app.diagrav.com/">Log in</a>
<a class="btn btn-primary btn-sm" href="https://app.diagrav.com/">Sign up</a>
</div>
</div>
</div>
</nav>
<header class="hero" id="top">
<div class="wrap">
<span class="eyebrow">🔌 Plan the whole rig before you buy anything</span>
<h1>Wire up your gear <span class="accent">on screen</span><br>before you touch a cable</h1>
<p class="lede">
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.
</p>
<div class="hero-shot">
<img src="images/canvas.jpg" alt="A wired synth rig diagram in Diagrav, showing a MIDI keyboard, drum machine, synth, mixer, and studio monitors connected with labeled cables" />
</div>
</div>
</header>
<section id="features">
<div class="wrap">
<div class="section-head">
<h2>Everything you need to plan a rig, nothing you don't</h2>
<p>Built for the moment before a purchase — when you're still figuring out what actually connects to what.</p>
</div>
<div class="features">
<div class="feature">
<div class="icon">🖱️</div>
<h3>Drag-and-drop canvas</h3>
<p>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.</p>
</div>
<div class="feature">
<div class="icon">✅</div>
<h3>Real compatibility checking</h3>
<p>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.</p>
</div>
<div class="feature">
<div class="icon">🧾</div>
<h3>Automatic shopping list</h3>
<p>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.</p>
</div>
<div class="feature">
<div class="icon">🎚️</div>
<h3>Multi-cable bundles</h3>
<p>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.</p>
</div>
<div class="feature">
<div class="icon">📚</div>
<h3>A growing shared library</h3>
<p>Connector types, cable types, and device templates are shared across everyone using the app — with your own private additions for anything niche or proprietary.</p>
</div>
<div class="feature">
<div class="icon">🤝</div>
<h3>Share &amp; collaborate</h3>
<p>Share a diagram with view or edit access, keep a version history as it evolves, and pick up right where you left off.</p>
</div>
</div>
</div>
</section>
<section id="canvas">
<div class="wrap">
<div class="showcase-row">
<div class="showcase-copy">
<div class="showcase-tag">The canvas</div>
<h3>See your whole signal path at a glance</h3>
<p>
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.
</p>
<ul>
<li>Devices collapse to a compact view when you don't need the detail</li>
<li>MIDI, audio, video, network, and power connectors, side by side</li>
<li>Bundle related runs (like a stereo pair) into one visually distinct cable</li>
</ul>
</div>
<div class="showcase-shot">
<img src="images/canvas.jpg" alt="Wired device diagram on the Diagrav canvas" />
</div>
</div>
<div class="showcase-row reverse">
<div class="showcase-copy">
<div class="showcase-tag">The shopping list</div>
<h3>A bill of materials that updates itself</h3>
<p>
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.
</p>
<ul>
<li>Cables grouped by type, then by length, so you know exactly what to buy and how many</li>
<li>Device and cable costs roll up into one grand total</li>
<li>Anything missing a cost is flagged, never silently treated as free</li>
</ul>
</div>
<div class="showcase-shot">
<img src="images/bom.jpg" alt="Bill of materials panel showing cables grouped by type and length with a running cost total" />
</div>
</div>
<div class="showcase-row" id="library">
<div class="showcase-copy">
<div class="showcase-tag">The library</div>
<h3>A device catalog that grows with you</h3>
<p>
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.
</p>
<ul>
<li>Organize by category or by manufacturer, whichever matches how you think</li>
<li>Add your own devices and connector types in minutes</li>
<li>Submit your additions back to the shared catalog for everyone else to use</li>
</ul>
</div>
<div class="showcase-shot">
<img src="images/palette.jpg" alt="Device library panel organized by category, showing built-in and custom devices" />
</div>
</div>
</div>
</section>
<section>
<div class="wrap">
<div class="strip">
<h2>Stop guessing which cable you need</h2>
<p>Plan the wiring first. Buy the right cables once.</p>
<a class="btn btn-primary btn-lg strip-cta" href="https://app.diagrav.com/">Sign up</a>
</div>
</div>
</section>
<footer>
<div class="wrap" style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:12px;">
<span>Diagrav</span>
<span>Built for planning AV and DAWless synth setups.</span>
</div>
</footer>
</body>
</html>
+412
View File
@@ -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;
}
+19
View File
@@ -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
}
]
}
+19
View File
@@ -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
}
]
}