From d0721f6b3e3eff84cb309acffaeb5cf0065e851c Mon Sep 17 00:00:00 2001
From: Harm Manders
Date: Fri, 11 Sep 2026 20:48:44 +0200
Subject: [PATCH 1/9] Add admin-togglable feature flags, gate AI monster
generator
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds a small, code-defined feature flag registry backed by Firebase,
an admin page to toggle flags, and a Vuex module that fetches current
values once per app load (client boot / each SSR render) — no live
push to open tabs, no redeploy required for a toggle to take effect.
Wires up the first flag, monster_generator, in two places: hides the
"Generate from description" entry point in the New Monster dialog when
disabled, and rejects POST /ai/generate-monster server-side so the
kill switch is authoritative even if a request bypasses the UI.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_017AuRuGWkBbBzeKpqtrq9qR
---
.../feature-flags-admin/.openspec.yaml | 2 +
.../changes/feature-flags-admin/design.md | 83 +++++++++++++++++++
.../changes/feature-flags-admin/proposal.md | 29 +++++++
.../specs/feature-flags/spec.md | 71 ++++++++++++++++
openspec/changes/feature-flags-admin/tasks.md | 56 +++++++++++++
src-ssr/api/index.js | 20 +++++
src/router/routes.js | 18 ++++
src/services/featureFlags.js | 19 +++++
src/store/index.js | 2 +
src/store/modules/featureFlags.js | 62 ++++++++++++++
src/store/modules/general.js | 3 +
src/utils/featureFlags.js | 14 ++++
src/views/Admin/FeatureFlags.vue | 66 +++++++++++++++
src/views/Admin/index.vue | 5 ++
src/views/UserContent/Npcs/EditNpc.vue | 2 +
15 files changed, 452 insertions(+)
create mode 100644 openspec/changes/feature-flags-admin/.openspec.yaml
create mode 100644 openspec/changes/feature-flags-admin/design.md
create mode 100644 openspec/changes/feature-flags-admin/proposal.md
create mode 100644 openspec/changes/feature-flags-admin/specs/feature-flags/spec.md
create mode 100644 openspec/changes/feature-flags-admin/tasks.md
create mode 100644 src/services/featureFlags.js
create mode 100644 src/store/modules/featureFlags.js
create mode 100644 src/utils/featureFlags.js
create mode 100644 src/views/Admin/FeatureFlags.vue
diff --git a/openspec/changes/feature-flags-admin/.openspec.yaml b/openspec/changes/feature-flags-admin/.openspec.yaml
new file mode 100644
index 000000000..515eaae39
--- /dev/null
+++ b/openspec/changes/feature-flags-admin/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-09-11
diff --git a/openspec/changes/feature-flags-admin/design.md b/openspec/changes/feature-flags-admin/design.md
new file mode 100644
index 000000000..975002205
--- /dev/null
+++ b/openspec/changes/feature-flags-admin/design.md
@@ -0,0 +1,83 @@
+## Context
+
+Shieldmaiden has no existing feature-flag mechanism. Toggleable behavior today is either a Vuex/UI concern (e.g. `userSettings`) or hardcoded. The first concrete need is a kill switch for the AI monster generator (`src/components/npcs/GenerateMonster.vue` → `POST /ai/generate-monster` in `src-ssr/api/index.js` → the external `MONSTER_GENERATOR_API_URL` service), so it can be turned off quickly if the external API misbehaves or gets expensive, without a release.
+
+The codebase already has a `/admin` section (`src/layouts/admin.vue`, gated client-side via `preFetch` on `store.getters.userInfo.admin`) with several simple admin pages that read/write Firebase Realtime Database paths directly from the client SDK (e.g. `src/services/promotions.js`, `src/views/Admin/Promotions.vue`), and a small standalone Vuex module pattern for lightweight admin-adjacent state (`src/store/modules/contentReports.js`). The app runs in Quasar SSR mode behind Express + PM2 in a long-running Docker container (`src-ssr/index.js`, `Dockerfile`), but per the product decision below, flags are read fresh on every render rather than cached for the process lifetime — so the container's lifetime doesn't matter here.
+
+## Goals / Non-Goals
+
+**Goals:**
+- A fixed, code-defined registry of boolean flags (id, label, default), extensible by adding an entry — no schema migration needed for a new flag.
+- Firebase-backed storage so an admin toggle is durable and consistent with how other admin tools already persist state.
+- An admin page consistent with the existing `/admin` pages' look, auth gating, and direct-Firebase-write style.
+- A flag change is visible on the next page load/reload (client re-fetch on boot, fresh read on every SSR render) — confirmed with the user that this is the desired freshness bar, explicitly **not** requiring a redeploy or a live push to open tabs.
+- The `monster_generator` flag is enforced in two places: hiding the client entry point (UX) and rejecting the server endpoint (the actual kill switch).
+
+**Non-Goals:**
+- Real-time/live propagation to already-open tabs (no `onValue`/`.on("value")` listeners for flags, no websockets).
+- Per-user, per-tier, or percentage-rollout flags — this is a single global boolean per flag.
+- Admin-created/deleted flags — the registry is code-only; the admin page only toggles existing entries.
+- A generic "requires redeploy" build-time-constant model — explicitly rejected in favor of the simpler Firebase-read-per-load model (see Decision 1).
+
+## Decisions
+
+### 1. Freshness model: read fresh per load, not cached for the server process lifetime
+
+Two designs were considered for how a toggle reaches running clients:
+- **(a) Read fresh on every load/render.** The client fetches flag state once during app boot (mirroring `checkExtensionInstalled` in `src/store/modules/general.js`); SSR reads it fresh on every `renderToString` call (since Quasar SSR already re-runs boot/store `initialize` per request). A reload always shows the current value. No redeploy needed, ever.
+- **(b) Cache in memory for the life of the SSR Node process.** Read once at process boot into a module-level singleton in `src-ssr`, outside the per-request Vuex lifecycle. A reload hits the same process and gets the stale value; only a new deploy (new PM2/Docker process) re-reads and picks up the change.
+
+Confirmed with the user: **(a)**. It's simpler (reuses the existing per-request boot fetch pattern, no new server-lifetime cache module to build/reason about), matches how every other piece of admin-toggled data in this app already works (promotions, vouchers — visible next load, no redeploy), and still fully satisfies the actual need (a fast kill switch that doesn't require a release).
+
+### 2. Registry lives in one shared module, importable from both client and server
+
+`src/utils/featureFlags.js` exports a plain object/array registry, e.g.:
+```js
+const FEATURE_FLAGS = {
+ monster_generator: { label: "AI Monster Generator", default: true },
+};
+```
+This file has no Firebase or Vue dependency, so it can be `require`d from `src-ssr/api/index.js` (CommonJS-compatible, following the existing pattern where `src-ssr/api/index.js` already imports plain services from `src/services/`) and imported from the Vuex module and admin page. One registry, one source of truth for ids/labels/defaults on both sides.
+
+**Alternative considered:** duplicate the registry (one for client, one for server). Rejected — guarantees drift (e.g. a flag id typo'd differently in each place silently fails open).
+
+### 3. Storage shape and read/write helpers
+
+Firebase path: `feature_flags/` → `{ enabled: boolean }` (object, not a bare boolean) to leave room for future metadata (e.g. `updated_at`) without a breaking shape change, consistent with `promotions`' shape.
+
+`src/services/featureFlags.js` (mirrors `src/services/promotions.js` / `src/services/contentReports.js`):
+- `getAllFlags()` — one `.once("value")` read of `feature_flags`, merged over the registry defaults so every registered flag always has a defined value even if never written.
+- `setFlagEnabled(id, enabled)` — `FEATURE_FLAGS_REF.child(id).child("enabled").set(enabled)`.
+
+`src/store/modules/featureFlags.js` (namespaced, mirrors `contentReports.js`):
+- State: `{ flags: {} }` (id → boolean, already merged with defaults).
+- Action `fetch_flags()` — calls the service once, commits the result. Called from `general.js`'s `initialize()` action, in both the authenticated and unauthenticated branches (flags aren't user-specific, and future flags may gate public-facing behavior).
+- Getter `isFlagEnabled: (state) => (id) => state.flags[id] ?? FEATURE_FLAGS[id]?.default ?? true` — reads the fetched value, falling back to the registry default (covers "fetch hasn't resolved yet" and "fetch failed" the same way).
+
+Server side (`src-ssr/api/index.js`): a small local helper using the existing `admin.database()` instance already initialized in that file:
+```js
+async function isFlagEnabled(id) {
+ const snap = await admin.database().ref(`feature_flags/${id}/enabled`).once("value");
+ const val = snap.val();
+ return val === null ? (FEATURE_FLAGS[id]?.default ?? true) : val;
+}
+```
+No new service class needed server-side — this mirrors the existing inline `admin.database()` usage already in that route handler for tiers/users/patrons.
+
+### 4. Admin UI: simple list with toggles, no new nav pattern
+
+`src/views/Admin/FeatureFlags.vue`: one `q-table`/`q-list` iterating the registry, each row a `q-toggle` bound to the flag's current stored state, calling `setFlagEnabled` on change (optimistic local update + Firebase write, same interaction pattern as `Promotions.vue`'s enable/disable buttons). Added to `src/views/Admin/index.vue`'s `items` list and as a new child route under `/admin` in `src/router/routes.js`, following the existing `vouchers`/`promotions` route shape exactly (flat child, no nested `:id`).
+
+**Alternative considered:** reuse `q-table` with row actions like `Promotions.vue`. Rejected as overkill for a handful of boolean rows — a plain list with inline toggles is less code and clearer for this shape of data.
+
+### 5. Monster generator enforcement in two places
+
+- **Client (UX only):** in `EditNpc.vue`, wrap the existing "Generate from description" `
OR
-
+ {{ flag.label }}
From cf10333e104b2c317917d54a194520eefc7d1cfd Mon Sep 17 00:00:00 2001
From: Harm Manders
Date: Fri, 11 Sep 2026 20:56:04 +0200
Subject: [PATCH 4/9] Gate remaining monster generator entry points found in QA
- EditNpc.vue: hide the "OR" divider along with the "Generate from
description" button so it doesn't dangle when the flag is off.
- Npcs.vue: the NPC list page has its own "Generate" toolbar button
and overflow-menu item that also open the AI generator; gate both.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_017AuRuGWkBbBzeKpqtrq9qR
---
.../specs/feature-flags/spec.md | 14 +++++++++-----
openspec/changes/feature-flags-admin/tasks.md | 2 ++
src/views/UserContent/Npcs/EditNpc.vue | 19 ++++++++++---------
src/views/UserContent/Npcs/Npcs.vue | 5 +++--
4 files changed, 24 insertions(+), 16 deletions(-)
diff --git a/openspec/changes/feature-flags-admin/specs/feature-flags/spec.md b/openspec/changes/feature-flags-admin/specs/feature-flags/spec.md
index b09e9d6a8..90efc5c2f 100644
--- a/openspec/changes/feature-flags-admin/specs/feature-flags/spec.md
+++ b/openspec/changes/feature-flags-admin/specs/feature-flags/spec.md
@@ -49,15 +49,19 @@ Flag state SHALL NOT be pushed live to already-open clients. A flag change SHALL
- **THEN** the reloaded page reflects the flag as disabled, without any redeploy of the application
### Requirement: Monster generator flag gates the AI generator's entry point
-When the `monster_generator` flag is disabled, the client SHALL NOT present the "Generate from description" option in the New Monster dialog (`EditNpc.vue`).
+When the `monster_generator` flag is disabled, the client SHALL NOT present any entry point into the AI monster generator: the "Generate from description" option (and its preceding "OR" divider) in the New Monster dialog (`EditNpc.vue`), and the "Generate" button/menu item on the NPC list page (`Npcs.vue`).
-#### Scenario: Entry point hidden while disabled
+#### Scenario: Entry point hidden in the New Monster dialog while disabled
- **WHEN** the `monster_generator` flag is disabled
-- **THEN** the New Monster dialog shows only "Copy existing monster" and "Create from scratch", without a "Generate from description" option
+- **THEN** the New Monster dialog shows only "Copy existing monster" and "Create from scratch", without a "Generate from description" option or the "OR" divider that would otherwise precede it
-#### Scenario: Entry point shown while enabled
+#### Scenario: Entry point hidden on the NPC list page while disabled
+- **WHEN** the `monster_generator` flag is disabled
+- **THEN** the NPC list page's toolbar button and overflow-menu item for "Generate" are both absent, regardless of NPC slot/AI credit state
+
+#### Scenario: Entry points shown while enabled
- **WHEN** the `monster_generator` flag is enabled (including its default state)
-- **THEN** the New Monster dialog shows the "Generate from description" option as it does today
+- **THEN** both the New Monster dialog's "Generate from description" option and the NPC list page's "Generate" entry points behave as they did before this change (still subject to their existing slot/credit conditions)
### Requirement: Monster generator flag is enforced server-side
The `POST /ai/generate-monster` endpoint SHALL check the `monster_generator` flag before generating a monster or spending AI credits, independent of whether the request came from the gated client UI.
diff --git a/openspec/changes/feature-flags-admin/tasks.md b/openspec/changes/feature-flags-admin/tasks.md
index 46c070c46..6466feefa 100644
--- a/openspec/changes/feature-flags-admin/tasks.md
+++ b/openspec/changes/feature-flags-admin/tasks.md
@@ -30,6 +30,8 @@
## 6. Monster generator: client entry point
- [x] 6.1 In `src/views/UserContent/Npcs/EditNpc.vue`, map `feature_flags/isFlagEnabled` and wrap the "Generate from description" button with `v-if="isFlagEnabled('monster_generator')"`
+- [x] 6.2 Fix dangling "OR" divider: wrap both the divider and the "Generate from description" button together in `EditNpc.vue`, found during manual QA
+- [x] 6.3 Found during manual QA: the NPC list page (`src/views/UserContent/Npcs/Npcs.vue`) has its own "Generate" entry points (toolbar button + overflow-menu item) that also call the AI generator — gate both with `isFlagEnabled('monster_generator')`
## 7. Monster generator: server enforcement
diff --git a/src/views/UserContent/Npcs/EditNpc.vue b/src/views/UserContent/Npcs/EditNpc.vue
index e9fee1257..c8d0d47bb 100644
--- a/src/views/UserContent/Npcs/EditNpc.vue
+++ b/src/views/UserContent/Npcs/EditNpc.vue
@@ -150,15 +150,16 @@
-
OR
-
+
+
OR
+
+
Copy an existing monster
diff --git a/src/views/UserContent/Npcs/Npcs.vue b/src/views/UserContent/Npcs/Npcs.vue
index 0e22de8fb..4951c50df 100644
--- a/src/views/UserContent/Npcs/Npcs.vue
+++ b/src/views/UserContent/Npcs/Npcs.vue
@@ -14,7 +14,7 @@
Export 800) {
return ["avatar", "name", "type", "challenge_rating", "actions"];
From e9272cabf0dfcd551027b39005a61b616616c905 Mon Sep 17 00:00:00 2001
From: Harm Manders
Date: Fri, 11 Sep 2026 21:09:56 +0200
Subject: [PATCH 5/9] Gate the remaining monster generator entry point on
/content/import
ImportContent/index.vue has its own "Generate" button opening
GenerateMonster.vue, separate from the NPC list and New Monster
dialog. Verified via grep that all GenerateMonster.vue usages
(3 total) are now gated by the monster_generator flag.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_017AuRuGWkBbBzeKpqtrq9qR
---
.../feature-flags-admin/specs/feature-flags/spec.md | 8 ++++++--
openspec/changes/feature-flags-admin/tasks.md | 1 +
src/views/UserContent/ImportContent/index.vue | 9 ++++++++-
3 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/openspec/changes/feature-flags-admin/specs/feature-flags/spec.md b/openspec/changes/feature-flags-admin/specs/feature-flags/spec.md
index 90efc5c2f..0f0d3bd78 100644
--- a/openspec/changes/feature-flags-admin/specs/feature-flags/spec.md
+++ b/openspec/changes/feature-flags-admin/specs/feature-flags/spec.md
@@ -49,7 +49,7 @@ Flag state SHALL NOT be pushed live to already-open clients. A flag change SHALL
- **THEN** the reloaded page reflects the flag as disabled, without any redeploy of the application
### Requirement: Monster generator flag gates the AI generator's entry point
-When the `monster_generator` flag is disabled, the client SHALL NOT present any entry point into the AI monster generator: the "Generate from description" option (and its preceding "OR" divider) in the New Monster dialog (`EditNpc.vue`), and the "Generate" button/menu item on the NPC list page (`Npcs.vue`).
+When the `monster_generator` flag is disabled, the client SHALL NOT present any entry point into the AI monster generator (every usage of `GenerateMonster.vue`): the "Generate from description" option (and its preceding "OR" divider) in the New Monster dialog (`EditNpc.vue`), the "Generate" button/menu item on the NPC list page (`Npcs.vue`), and the "Generate" button on the generic content import page (`ImportContent/index.vue`).
#### Scenario: Entry point hidden in the New Monster dialog while disabled
- **WHEN** the `monster_generator` flag is disabled
@@ -59,9 +59,13 @@ When the `monster_generator` flag is disabled, the client SHALL NOT present any
- **WHEN** the `monster_generator` flag is disabled
- **THEN** the NPC list page's toolbar button and overflow-menu item for "Generate" are both absent, regardless of NPC slot/AI credit state
+#### Scenario: Entry point hidden on the content import page while disabled
+- **WHEN** the `monster_generator` flag is disabled
+- **THEN** the "Generate" button on `/content/import` is absent, regardless of which content type is being imported
+
#### Scenario: Entry points shown while enabled
- **WHEN** the `monster_generator` flag is enabled (including its default state)
-- **THEN** both the New Monster dialog's "Generate from description" option and the NPC list page's "Generate" entry points behave as they did before this change (still subject to their existing slot/credit conditions)
+- **THEN** all three entry points behave as they did before this change (still subject to their existing slot/credit/tier conditions)
### Requirement: Monster generator flag is enforced server-side
The `POST /ai/generate-monster` endpoint SHALL check the `monster_generator` flag before generating a monster or spending AI credits, independent of whether the request came from the gated client UI.
diff --git a/openspec/changes/feature-flags-admin/tasks.md b/openspec/changes/feature-flags-admin/tasks.md
index 6466feefa..5e5ca4690 100644
--- a/openspec/changes/feature-flags-admin/tasks.md
+++ b/openspec/changes/feature-flags-admin/tasks.md
@@ -32,6 +32,7 @@
- [x] 6.1 In `src/views/UserContent/Npcs/EditNpc.vue`, map `feature_flags/isFlagEnabled` and wrap the "Generate from description" button with `v-if="isFlagEnabled('monster_generator')"`
- [x] 6.2 Fix dangling "OR" divider: wrap both the divider and the "Generate from description" button together in `EditNpc.vue`, found during manual QA
- [x] 6.3 Found during manual QA: the NPC list page (`src/views/UserContent/Npcs/Npcs.vue`) has its own "Generate" entry points (toolbar button + overflow-menu item) that also call the AI generator — gate both with `isFlagEnabled('monster_generator')`
+- [x] 6.4 Found during manual QA: the generic content import page (`src/views/UserContent/ImportContent/index.vue`, `/content/import`) also has its own "Generate" button opening the same `GenerateMonster.vue` — gate it too. Confirmed via `grep -rln "GenerateMonster" src` that these are now all three (and only three) usage sites
## 7. Monster generator: server enforcement
diff --git a/src/views/UserContent/ImportContent/index.vue b/src/views/UserContent/ImportContent/index.vue
index 7937fb927..af7dd64d0 100644
--- a/src/views/UserContent/ImportContent/index.vue
+++ b/src/views/UserContent/ImportContent/index.vue
@@ -3,7 +3,13 @@
-
+
@@ -36,6 +42,7 @@ export default {
},
computed: {
...mapGetters(["tier", "ai"]),
+ ...mapGetters("feature_flags", ["isFlagEnabled"]),
},
data() {
return {
From 7e9f968af947b3c53d8a0ede62c07d664c5e79d3 Mon Sep 17 00:00:00 2001
From: Harm Manders
Date: Fri, 11 Sep 2026 21:15:22 +0200
Subject: [PATCH 6/9] Check off manual QA confirmed by user
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_017AuRuGWkBbBzeKpqtrq9qR
---
openspec/changes/feature-flags-admin/tasks.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/openspec/changes/feature-flags-admin/tasks.md b/openspec/changes/feature-flags-admin/tasks.md
index 5e5ca4690..f212154c9 100644
--- a/openspec/changes/feature-flags-admin/tasks.md
+++ b/openspec/changes/feature-flags-admin/tasks.md
@@ -46,10 +46,10 @@
## 9. Manual verification
-- [ ] 9.1 Run `npm run ssr`; confirm `/admin/feature-flags` lists `monster_generator` toggled on by default, and is unreachable when signed in as a non-admin
-- [ ] 9.2 Toggle `monster_generator` off on the admin page; reload the New Monster dialog in `EditNpc.vue` and confirm "Generate from description" is gone
-- [ ] 9.3 With the flag off, call `POST /ai/generate-monster` directly (e.g. via curl/Postman with a valid token) and confirm it's rejected without spending credits or calling the external API
-- [ ] 9.4 Toggle the flag back on, reload, and confirm both the entry point and the endpoint work as before
+- [x] 9.1 Run `npm run ssr`; confirm `/admin/feature-flags` lists `monster_generator` toggled on by default, and is unreachable when signed in as a non-admin — confirmed by user ("ff page works")
+- [x] 9.2 Toggle `monster_generator` off on the admin page; reload the New Monster dialog in `EditNpc.vue` and confirm "Generate from description" is gone — confirmed by user
+- [x] 9.3 With the flag off, call `POST /ai/generate-monster` directly (e.g. via curl/Postman with a valid token) and confirm it's rejected without spending credits or calling the external API — confirmed by user ("API successfully blocks it")
+- [x] 9.4 Toggle the flag back on, reload, and confirm both the entry point and the endpoint work as before — confirmed by user ("toggle works")
- [ ] 9.5 Delete the `feature_flags/monster_generator` node entirely (simulating "never toggled") and confirm the feature behaves as enabled (registry default)
- [x] 9.6 Run `npm run lint` and fix any issues introduced
From 524b1dbc511d67f23af11a62f22dff93766eae55 Mon Sep 17 00:00:00 2001
From: Harm Manders
Date: Fri, 11 Sep 2026 21:15:55 +0200
Subject: [PATCH 7/9] Check off final manual QA item
All 27 tasks confirmed complete.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_017AuRuGWkBbBzeKpqtrq9qR
---
openspec/changes/feature-flags-admin/tasks.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/openspec/changes/feature-flags-admin/tasks.md b/openspec/changes/feature-flags-admin/tasks.md
index f212154c9..ddb0cfeef 100644
--- a/openspec/changes/feature-flags-admin/tasks.md
+++ b/openspec/changes/feature-flags-admin/tasks.md
@@ -50,7 +50,7 @@
- [x] 9.2 Toggle `monster_generator` off on the admin page; reload the New Monster dialog in `EditNpc.vue` and confirm "Generate from description" is gone — confirmed by user
- [x] 9.3 With the flag off, call `POST /ai/generate-monster` directly (e.g. via curl/Postman with a valid token) and confirm it's rejected without spending credits or calling the external API — confirmed by user ("API successfully blocks it")
- [x] 9.4 Toggle the flag back on, reload, and confirm both the entry point and the endpoint work as before — confirmed by user ("toggle works")
-- [ ] 9.5 Delete the `feature_flags/monster_generator` node entirely (simulating "never toggled") and confirm the feature behaves as enabled (registry default)
+- [x] 9.5 Delete the `feature_flags/monster_generator` node entirely (simulating "never toggled") and confirm the feature behaves as enabled (registry default) — confirmed by user ("Delete node works, defaults to true")
- [x] 9.6 Run `npm run lint` and fix any issues introduced
## 10. Wrap up
From 711401264995b64ef44a4238547031660c66c395 Mon Sep 17 00:00:00 2001
From: Harm Manders
Date: Fri, 11 Sep 2026 21:19:35 +0200
Subject: [PATCH 8/9] Fix production SSR build: remove optional chaining from
src-ssr
src-ssr/*.js is not run through Babel (see the warning comment in
src-ssr/index.js), and this project's webpack 4 parser doesn't
understand the ES2020 optional-chaining/nullish-coalescing syntax
used in isFlagEnabled's fallback lookup. Dev mode (npm run ssr) didn't
catch this since it takes a different, more lenient path; the
production build (npx quasar build -m ssr, used by the Dockerfile)
failed to parse it.
Replaced with a plain flagDefault() helper using only pre-ES2020
syntax. Verified with a full `npx quasar build -m ssr` run.
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_017AuRuGWkBbBzeKpqtrq9qR
---
src-ssr/api/index.js | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src-ssr/api/index.js b/src-ssr/api/index.js
index f921c07df..26617a326 100644
--- a/src-ssr/api/index.js
+++ b/src-ssr/api/index.js
@@ -31,14 +31,18 @@ const router = new Router();
* Reads a feature flag's stored value, falling back to its registry default
* when unset or when the read fails (fail-open, never an accidental kill switch)
*/
+function flagDefault(id) {
+ return FEATURE_FLAGS[id] && FEATURE_FLAGS[id].default !== undefined ? FEATURE_FLAGS[id].default : true;
+}
+
async function isFlagEnabled(id) {
try {
const snapshot = await admin.database().ref(`feature_flags/${id}/enabled`).once("value");
const value = snapshot.val();
- return value === null ? FEATURE_FLAGS[id]?.default ?? true : value;
+ return value === null ? flagDefault(id) : value;
} catch (error) {
console.error(`Error reading feature flag "${id}":`, error);
- return FEATURE_FLAGS[id]?.default ?? true;
+ return flagDefault(id);
}
}
From 0e2aefad9680a7de4281394d9f442df26d853b75 Mon Sep 17 00:00:00 2001
From: Harm Manders
Date: Fri, 11 Sep 2026 21:19:55 +0200
Subject: [PATCH 9/9] Note the production SSR build fix in the task checklist
Co-Authored-By: Claude Sonnet 5
Claude-Session: https://claude.ai/code/session_017AuRuGWkBbBzeKpqtrq9qR
---
openspec/changes/feature-flags-admin/tasks.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/openspec/changes/feature-flags-admin/tasks.md b/openspec/changes/feature-flags-admin/tasks.md
index ddb0cfeef..7af270a0b 100644
--- a/openspec/changes/feature-flags-admin/tasks.md
+++ b/openspec/changes/feature-flags-admin/tasks.md
@@ -38,6 +38,7 @@
- [x] 7.1 In `src-ssr/api/index.js`, `require` `FEATURE_FLAGS` from `src/utils/featureFlags.js` and add a small `isFlagEnabled(id)` helper using the existing `admin.database()` instance (fallback to registry default when no stored value or on read error)
- [x] 7.2 In `router.post("/ai/generate-monster", ...)`, check `isFlagEnabled("monster_generator")` right after token verification and before the credits lookup; if disabled, respond with a 4xx and a clear message, without calling `MonsterGenerator.generateMonster` or touching credits
+- [x] 7.3 Found via Docker/CI build failure: `src-ssr/*.js` isn't Babel-transpiled, and webpack 4's parser can't handle the ES2020 optional-chaining/nullish-coalescing (`?.`/`??`) used in the flag-default fallback. `npm run ssr` (dev) didn't catch it. Replaced with a plain `flagDefault()` helper; verified with a full `npx quasar build -m ssr`
## 8. Firebase rules (manual, outside this repo's tracked files)