create-volt 0.44.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ All notable changes to `create-volt` are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.45.0] - 2026-06-29
8
+
9
+ ### Added
10
+ - **Buy AI credits from the config wizard.** When an app uses the hosted gateway
11
+ (`VOLT_AI_TOKEN` set), the `--edit` wizard shows an **AI credits** card — live
12
+ balance + tier, and top-up buttons that open Stripe Checkout. The purchase flow
13
+ lives in the (shell-gated) config only; the running app never exposes it.
14
+ Proxied via `/setup/ai-credits` + `/setup/ai-credits/checkout` to the gateway.
15
+
7
16
  ## [0.44.0] - 2026-06-29
8
17
 
9
18
  ### Added
@@ -584,6 +593,7 @@ All notable changes to `create-volt` are documented here. The format follows
584
593
  watching and full-page hot reload. Supports `--skip-install` and `--force`,
585
594
  and auto-detects npm / pnpm / yarn / bun for the install step.
586
595
 
596
+ [0.45.0]: https://github.com/MIR-2025/volt/releases/tag/v0.45.0
587
597
  [0.44.0]: https://github.com/MIR-2025/volt/releases/tag/v0.44.0
588
598
  [0.43.0]: https://github.com/MIR-2025/volt/releases/tag/v0.43.0
589
599
  [0.42.0]: https://github.com/MIR-2025/volt/releases/tag/v0.42.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-volt",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "Scaffold a new Volt app — no-build, signals-based UI with Socket.io hot reload.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -341,6 +341,42 @@ function startSetup() {
341
341
  }
342
342
  return;
343
343
  }
344
+ // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
345
+ // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
346
+ // config only, never in the running app. ---
347
+ if (req.method === "GET" && p === "/setup/ai-credits") {
348
+ const env = readEnvFile();
349
+ res.setHeader("Content-Type", "application/json");
350
+ if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
351
+ const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
352
+ fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
353
+ .then((r) => r.json())
354
+ .then((j) => res.end(JSON.stringify(j)))
355
+ .catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
356
+ return;
357
+ }
358
+ if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
359
+ let cbody = "";
360
+ req.on("data", (c) => (cbody += c));
361
+ req.on("end", () => {
362
+ const env = readEnvFile();
363
+ res.setHeader("Content-Type", "application/json");
364
+ if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
365
+ const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
366
+ let amountUsd = 0;
367
+ try {
368
+ amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
369
+ } catch {
370
+ /* bad json */
371
+ }
372
+ const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
373
+ fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
374
+ .then((r) => r.json())
375
+ .then((j) => res.end(JSON.stringify(j)))
376
+ .catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
377
+ });
378
+ return;
379
+ }
344
380
  // --- content manager: list / read / write / delete pages + posts ---
345
381
  if (req.method === "GET" && p === "/setup/content") {
346
382
  const list = (type) => {
@@ -284,6 +284,21 @@ async function doUpgrade() {
284
284
  status("Upgrade request failed.");
285
285
  }
286
286
  }
287
+
288
+ // AI credits — config-only purchase flow (gateway mode). Hidden unless a
289
+ // VOLT_AI_TOKEN is set and the gateway answers.
290
+ const aiCredits = signal(null); // { ok, tier, creditBalanceUsd, payments } | { ok:false }
291
+ fetch("/setup/ai-credits").then((r) => r.json()).then((c) => aiCredits(c)).catch(() => {});
292
+ async function buyCredits(amountUsd) {
293
+ status("Starting checkout…");
294
+ try {
295
+ const r = await (await fetch("/setup/ai-credits/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amountUsd }) })).json();
296
+ if (r.ok && r.url) window.open(r.url, "_blank");
297
+ else status("Checkout failed: " + (r.error || "?"));
298
+ } catch {
299
+ status("Checkout request failed.");
300
+ }
301
+ }
287
302
  const items = signal({ pages: [], posts: [] });
288
303
  const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
289
304
  const loadItems = async () => items(await (await fetch("/setup/content")).json());
@@ -339,6 +354,7 @@ const manageView = () =>
339
354
 
340
355
  const configView = () =>
341
356
  html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
357
+ ${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
342
358
  ${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
343
359
  <div class="card-x p-4 mb-3">
344
360
  <h2 class="h6 mb-3">Settings</h2>
@@ -341,6 +341,42 @@ function startSetup() {
341
341
  }
342
342
  return;
343
343
  }
344
+ // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
345
+ // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
346
+ // config only, never in the running app. ---
347
+ if (req.method === "GET" && p === "/setup/ai-credits") {
348
+ const env = readEnvFile();
349
+ res.setHeader("Content-Type", "application/json");
350
+ if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
351
+ const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
352
+ fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
353
+ .then((r) => r.json())
354
+ .then((j) => res.end(JSON.stringify(j)))
355
+ .catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
356
+ return;
357
+ }
358
+ if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
359
+ let cbody = "";
360
+ req.on("data", (c) => (cbody += c));
361
+ req.on("end", () => {
362
+ const env = readEnvFile();
363
+ res.setHeader("Content-Type", "application/json");
364
+ if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
365
+ const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
366
+ let amountUsd = 0;
367
+ try {
368
+ amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
369
+ } catch {
370
+ /* bad json */
371
+ }
372
+ const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
373
+ fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
374
+ .then((r) => r.json())
375
+ .then((j) => res.end(JSON.stringify(j)))
376
+ .catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
377
+ });
378
+ return;
379
+ }
344
380
  // --- content manager: list / read / write / delete pages + posts ---
345
381
  if (req.method === "GET" && p === "/setup/content") {
346
382
  const list = (type) => {
@@ -284,6 +284,21 @@ async function doUpgrade() {
284
284
  status("Upgrade request failed.");
285
285
  }
286
286
  }
287
+
288
+ // AI credits — config-only purchase flow (gateway mode). Hidden unless a
289
+ // VOLT_AI_TOKEN is set and the gateway answers.
290
+ const aiCredits = signal(null); // { ok, tier, creditBalanceUsd, payments } | { ok:false }
291
+ fetch("/setup/ai-credits").then((r) => r.json()).then((c) => aiCredits(c)).catch(() => {});
292
+ async function buyCredits(amountUsd) {
293
+ status("Starting checkout…");
294
+ try {
295
+ const r = await (await fetch("/setup/ai-credits/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amountUsd }) })).json();
296
+ if (r.ok && r.url) window.open(r.url, "_blank");
297
+ else status("Checkout failed: " + (r.error || "?"));
298
+ } catch {
299
+ status("Checkout request failed.");
300
+ }
301
+ }
287
302
  const items = signal({ pages: [], posts: [] });
288
303
  const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
289
304
  const loadItems = async () => items(await (await fetch("/setup/content")).json());
@@ -339,6 +354,7 @@ const manageView = () =>
339
354
 
340
355
  const configView = () =>
341
356
  html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
357
+ ${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
342
358
  ${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
343
359
  <div class="card-x p-4 mb-3">
344
360
  <h2 class="h6 mb-3">Settings</h2>
@@ -341,6 +341,42 @@ function startSetup() {
341
341
  }
342
342
  return;
343
343
  }
344
+ // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
345
+ // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
346
+ // config only, never in the running app. ---
347
+ if (req.method === "GET" && p === "/setup/ai-credits") {
348
+ const env = readEnvFile();
349
+ res.setHeader("Content-Type", "application/json");
350
+ if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
351
+ const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
352
+ fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
353
+ .then((r) => r.json())
354
+ .then((j) => res.end(JSON.stringify(j)))
355
+ .catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
356
+ return;
357
+ }
358
+ if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
359
+ let cbody = "";
360
+ req.on("data", (c) => (cbody += c));
361
+ req.on("end", () => {
362
+ const env = readEnvFile();
363
+ res.setHeader("Content-Type", "application/json");
364
+ if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
365
+ const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
366
+ let amountUsd = 0;
367
+ try {
368
+ amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
369
+ } catch {
370
+ /* bad json */
371
+ }
372
+ const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
373
+ fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
374
+ .then((r) => r.json())
375
+ .then((j) => res.end(JSON.stringify(j)))
376
+ .catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
377
+ });
378
+ return;
379
+ }
344
380
  // --- content manager: list / read / write / delete pages + posts ---
345
381
  if (req.method === "GET" && p === "/setup/content") {
346
382
  const list = (type) => {
@@ -284,6 +284,21 @@ async function doUpgrade() {
284
284
  status("Upgrade request failed.");
285
285
  }
286
286
  }
287
+
288
+ // AI credits — config-only purchase flow (gateway mode). Hidden unless a
289
+ // VOLT_AI_TOKEN is set and the gateway answers.
290
+ const aiCredits = signal(null); // { ok, tier, creditBalanceUsd, payments } | { ok:false }
291
+ fetch("/setup/ai-credits").then((r) => r.json()).then((c) => aiCredits(c)).catch(() => {});
292
+ async function buyCredits(amountUsd) {
293
+ status("Starting checkout…");
294
+ try {
295
+ const r = await (await fetch("/setup/ai-credits/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amountUsd }) })).json();
296
+ if (r.ok && r.url) window.open(r.url, "_blank");
297
+ else status("Checkout failed: " + (r.error || "?"));
298
+ } catch {
299
+ status("Checkout request failed.");
300
+ }
301
+ }
287
302
  const items = signal({ pages: [], posts: [] });
288
303
  const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
289
304
  const loadItems = async () => items(await (await fetch("/setup/content")).json());
@@ -339,6 +354,7 @@ const manageView = () =>
339
354
 
340
355
  const configView = () =>
341
356
  html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
357
+ ${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
342
358
  ${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
343
359
  <div class="card-x p-4 mb-3">
344
360
  <h2 class="h6 mb-3">Settings</h2>
@@ -367,6 +367,42 @@ function startSetup() {
367
367
  }
368
368
  return;
369
369
  }
370
+ // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
371
+ // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
372
+ // config only, never in the running app. ---
373
+ if (req.method === "GET" && p === "/setup/ai-credits") {
374
+ const env = readEnvFile();
375
+ res.setHeader("Content-Type", "application/json");
376
+ if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
377
+ const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
378
+ fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
379
+ .then((r) => r.json())
380
+ .then((j) => res.end(JSON.stringify(j)))
381
+ .catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
382
+ return;
383
+ }
384
+ if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
385
+ let cbody = "";
386
+ req.on("data", (c) => (cbody += c));
387
+ req.on("end", () => {
388
+ const env = readEnvFile();
389
+ res.setHeader("Content-Type", "application/json");
390
+ if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
391
+ const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
392
+ let amountUsd = 0;
393
+ try {
394
+ amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
395
+ } catch {
396
+ /* bad json */
397
+ }
398
+ const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
399
+ fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
400
+ .then((r) => r.json())
401
+ .then((j) => res.end(JSON.stringify(j)))
402
+ .catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
403
+ });
404
+ return;
405
+ }
370
406
  // --- content manager: list / read / write / delete pages + posts ---
371
407
  if (req.method === "GET" && p === "/setup/content") {
372
408
  const list = (type) => {
@@ -284,6 +284,21 @@ async function doUpgrade() {
284
284
  status("Upgrade request failed.");
285
285
  }
286
286
  }
287
+
288
+ // AI credits — config-only purchase flow (gateway mode). Hidden unless a
289
+ // VOLT_AI_TOKEN is set and the gateway answers.
290
+ const aiCredits = signal(null); // { ok, tier, creditBalanceUsd, payments } | { ok:false }
291
+ fetch("/setup/ai-credits").then((r) => r.json()).then((c) => aiCredits(c)).catch(() => {});
292
+ async function buyCredits(amountUsd) {
293
+ status("Starting checkout…");
294
+ try {
295
+ const r = await (await fetch("/setup/ai-credits/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amountUsd }) })).json();
296
+ if (r.ok && r.url) window.open(r.url, "_blank");
297
+ else status("Checkout failed: " + (r.error || "?"));
298
+ } catch {
299
+ status("Checkout request failed.");
300
+ }
301
+ }
287
302
  const items = signal({ pages: [], posts: [] });
288
303
  const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
289
304
  const loadItems = async () => items(await (await fetch("/setup/content")).json());
@@ -339,6 +354,7 @@ const manageView = () =>
339
354
 
340
355
  const configView = () =>
341
356
  html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
357
+ ${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
342
358
  ${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
343
359
  <div class="card-x p-4 mb-3">
344
360
  <h2 class="h6 mb-3">Settings</h2>