From 3e87fdeab44e835e7aadba8061e20f70860214b4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 18:29:30 +0000 Subject: [PATCH 1/2] Generate Brief discovery JSON from env at build/deploy Stop treating the checked-in public/.well-known/brief.json as source of truth. Add scripts/generate-brief-config.js (BRIEF_CLIENT_CONFIG or BRIEF_*/FIREBASE_*/VUE_APP_* compose, optional fetch) and wire it into prebuild, preserve, and deploy.js so every gh-pages publish refreshes the brochure from env in sync with mvtt-server client-config. Co-authored-by: Sander Vonk --- .env.test | 10 +- .gitignore | 3 + README.md | 4 + deploy.js | 8 ++ example.env | 37 ++++- package.json | 3 + public/.well-known/.gitkeep | 0 public/.well-known/brief.json | 16 --- scripts/generate-brief-config.js | 235 +++++++++++++++++++++++++++++++ 9 files changed, 298 insertions(+), 18 deletions(-) create mode 100644 public/.well-known/.gitkeep delete mode 100644 public/.well-known/brief.json create mode 100644 scripts/generate-brief-config.js diff --git a/.env.test b/.env.test index 07363569..1b743b4c 100644 --- a/.env.test +++ b/.env.test @@ -59,4 +59,12 @@ FIREBASE_appId=1:1070634963357:web:2a4abc725fff24a5199f74 # the measurement ID for the Firebase project FIREBASE_measurementId=G-6LNX1KCR8E # the server region for the Firebase project -FIREBASE_serverRegion=us-central1 \ No newline at end of file +FIREBASE_serverRegion=us-central1 + +# Brief brochure stub for offline CI (placeholders — not production CMUTT values) +BRIEF_NAME=[BETA] +BRIEF_API_BASE=https://api.mvtt.app +BRIEF_BRAND_COLOR=f5c14b +BRIEF_FIREBASE_APP_ID=1:1070634963357:ios:teststub00000000000000 +BRIEF_FIREBASE_CLIENT_ID=1070634963357-teststub.apps.googleusercontent.com +BRIEF_FIREBASE_REVERSED_CLIENT_ID=com.googleusercontent.apps.1070634963357-teststub \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0f8167e6..49f1cb72 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ node_modules .env.local .env.*.local +# Generated at build/deploy from BRIEF_* / FIREBASE_* / VUE_APP_* (see scripts/generate-brief-config.js) +/public/.well-known/brief.json + # Log files npm-debug.log* yarn-debug.log* diff --git a/README.md b/README.md index 63466352..f525fe9d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ use `npm run serve -- --mode [mode]` to run with alternative `.env.[mode]` or `. nvm use 18 && npm run deploy ``` +Production Brief discovery (`/.well-known/brief.json` on `cmutt.app` / `mvtt.app`) is **generated at deploy time** from env — not a hand-edited JSON file. `deploy.js` runs `scripts/generate-brief-config.js` before `vue-cli-service build`, then publishes `dist/` to `gh-pages`. CI `npm run build` also regenerates via `prebuild`, but **live Pages only updates when you run `npm run deploy`** (or otherwise publish that `dist`). + +Keep production `BRIEF_*` / `BRIEF_CLIENT_CONFIG` in sync with mvtt-server’s client-config env (same public allowlist). See `example.env` for the field list. Never commit service accounts; Firebase client fields are OK. + or optionally when cmu-tt is side-by-side (requires all changes to be pushed to remote for full parity): ``` diff --git a/deploy.js b/deploy.js index 8f7c783c..388f150f 100644 --- a/deploy.js +++ b/deploy.js @@ -77,6 +77,14 @@ function deploy() { centerMsg("\x1b[35m\x1b[2m%s\x1b[0m", "NOTE: JSDocs may lose content during compiling"); } + // Refresh Brief discovery JSON from env before build (also runs via npm prebuild). + // deploy calls vue-cli-service directly, so invoke the generator explicitly here. + runCommand( + `node scripts/generate-brief-config.js${mode ? ` --mode ${mode}` : ""}`, + "brief-config", + "Generating public/.well-known/brief.json from env" + ); + runCommand( `vue-cli-service build${mode ? ` --mode ${mode}` : ""}`, "build", diff --git a/example.env b/example.env index f0be25ef..dc543a26 100644 --- a/example.env +++ b/example.env @@ -39,4 +39,39 @@ FIREBASE_appId=1:1000000000000:web:AAAAAAAAAAAAAAAAAAAAAA # the measurement ID for the Firebase project FIREBASE_measurementId=G-AAAAAAAAAA # the server region for the Firebase project -FIREBASE_serverRegion=us-central1 \ No newline at end of file +FIREBASE_serverRegion=us-central1 + +# ============================================================================= +# Brief client-config (public/.well-known/brief.json) — generated at build/deploy +# ============================================================================= +# NEVER commit real secrets. These Firebase *client* fields are public (same +# values as in the iOS plist / web SDK). Do NOT put service accounts, Admin SDK +# keys, or shared API secrets here. +# +# Keep production values in sync with mvtt-server BRIEF_* / client-config env +# (same allowlist schema). Prefer one documented contract; env compose is the +# source of truth for this Pages site — never hand-edit brief.json. +# +# Option A (preferred for parity with API): single JSON blob matching +# GET /api/v1/client-config — name, apiBase, brandColor, firebase{...} +# BRIEF_CLIENT_CONFIG={"name":"ORGTT","apiBase":"https://api.example.com","brandColor":"#f5c14b","firebase":{"apiKey":"...","projectId":"...","appId":"1:...:ios:...","gcmSenderId":"...","clientId":"...","reversedClientId":"...","storageBucket":"...","databaseURL":null,"authDomain":"..."}} +# +# Option B: compose from discrete vars (reuses FIREBASE_* / VUE_APP_* where they match) +BRIEF_NAME=ORGTT +BRIEF_API_BASE=https://api.example.com +BRIEF_BRAND_COLOR=f5c14b +# iOS-only Brief fields (do NOT reuse web FIREBASE_appId) +BRIEF_FIREBASE_APP_ID=1:1000000000000:ios:AAAAAAAAAAAAAAAAAAAAAA +BRIEF_FIREBASE_CLIENT_ID=1000000000000-xxxxxxxx.apps.googleusercontent.com +BRIEF_FIREBASE_REVERSED_CLIENT_ID=com.googleusercontent.apps.1000000000000-xxxxxxxx +# Optional overrides (default to FIREBASE_* / null): +# BRIEF_FIREBASE_API_KEY= +# BRIEF_FIREBASE_PROJECT_ID= +# BRIEF_FIREBASE_GCM_SENDER_ID= +# BRIEF_FIREBASE_STORAGE_BUCKET= +# BRIEF_FIREBASE_AUTH_DOMAIN= +# BRIEF_FIREBASE_DATABASE_URL= +# +# Option C (CI only): fetch at build time when BRIEF_FETCH_CONFIG=1 +# BRIEF_FETCH_CONFIG=0 +# BRIEF_CONFIG_URL=https://api.example.com/api/v1/client-config \ No newline at end of file diff --git a/package.json b/package.json index 6b3a61ec..1b37875a 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "description": "A more effective way to manage test schedules, long-term assignments, and exam stress.", "private": true, "scripts": { + "generate:brief-config": "node scripts/generate-brief-config.js", + "prebuild": "node scripts/generate-brief-config.js", + "preserve": "node scripts/generate-brief-config.js", "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint", diff --git a/public/.well-known/.gitkeep b/public/.well-known/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/public/.well-known/brief.json b/public/.well-known/brief.json deleted file mode 100644 index 9b6ea8fc..00000000 --- a/public/.well-known/brief.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "CMUTT", - "apiBase": "https://api.cmutt.app", - "brandColor": "#C41230", - "firebase": { - "apiKey": "AIzaSyCvNooyuA5JVKU8AwHutIJGkagoRDhs0TM", - "projectId": "cmu-tt", - "appId": "1:764642585239:ios:1584fa0707a43c4fad97d2", - "gcmSenderId": "764642585239", - "clientId": "764642585239-dfv7pc6c9egc04hgu389iqq98jg5vnur.apps.googleusercontent.com", - "reversedClientId": "com.googleusercontent.apps.764642585239-dfv7pc6c9egc04hgu389iqq98jg5vnur", - "storageBucket": "cmu-tt.firebasestorage.app", - "databaseURL": null, - "authDomain": "cmu-tt.firebaseapp.com" - } -} diff --git a/scripts/generate-brief-config.js b/scripts/generate-brief-config.js new file mode 100644 index 00000000..d0fdb49b --- /dev/null +++ b/scripts/generate-brief-config.js @@ -0,0 +1,235 @@ +#!/usr/bin/env node +/** + * Generate public/.well-known/brief.json from env (single source of truth). + * + * Priority: + * 1. BRIEF_CLIENT_CONFIG — full JSON blob (same schema as API client-config) + * 2. Compose from discrete BRIEF_* / VUE_APP_* / FIREBASE_* vars + * 3. If BRIEF_FETCH_CONFIG=1 — fetch BRIEF_CONFIG_URL or API client-config + * 4. Minimal stub so offline CI still builds + * + * Keep production BRIEF_* values in sync with mvtt-server (same public allowlist). + * Never put service accounts or server secrets here — Firebase web/iOS client + * fields are intentionally public. + */ + +const fs = require("fs"); +const path = require("path"); + +const ROOT = path.resolve(__dirname, ".."); +const OUT_PATH = path.join(ROOT, "public", ".well-known", "brief.json"); + +const DEFAULT_FETCH_URL = "https://api.cmutt.app/api/v1/client-config"; + +/** Parse a simple KEY=VALUE .env file (no export, no multiline values). */ +function parseEnvFile(filePath) { + if (!fs.existsSync(filePath)) return {}; + const out = {}; + for (const raw of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) { + const line = raw.trim(); + if (!line || line.startsWith("#")) continue; + const eq = line.indexOf("="); + if (eq <= 0) continue; + const key = line.slice(0, eq).trim(); + let value = line.slice(eq + 1).trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + out[key] = value; + } + return out; +} + +/** + * Load Vue-CLI-style env files for a mode without overwriting existing process.env. + * Order (lowest → highest precedence among files): .env, .env.local, .env.[mode], .env.[mode].local + * Existing process.env wins over all files. + */ +function loadEnvFiles(mode) { + const names = [".env", ".env.local"]; + if (mode && mode !== "development") { + names.push(`.env.${mode}`, `.env.${mode}.local`); + } else if (mode === "development") { + names.push(".env.development", ".env.development.local"); + } else { + // production default when no mode (matches vue-cli-service build) + names.push(".env.production", ".env.production.local"); + } + + const merged = {}; + for (const name of names) { + Object.assign(merged, parseEnvFile(path.join(ROOT, name))); + } + for (const [key, value] of Object.entries(merged)) { + if (process.env[key] === undefined) { + process.env[key] = value; + } + } +} + +function argMode() { + const idx = process.argv.indexOf("--mode"); + if (idx >= 0 && process.argv[idx + 1]) return process.argv[idx + 1]; + // npm run build -- --mode test → vue-cli gets it; prebuild may see npm_config_argv + const fromEnv = process.env.VUE_CLI_MODE || process.env.npm_config_mode; + if (fromEnv) return fromEnv; + // CI copies .env.test → .env; treat as production build mode for file loading + return process.env.NODE_ENV === "development" ? "development" : "production"; +} + +function withHash(color) { + if (!color) return undefined; + return color.startsWith("#") ? color : `#${color}`; +} + +function apiBaseFromEnv() { + if (process.env.BRIEF_API_BASE) return process.env.BRIEF_API_BASE.replace(/\/$/, ""); + const host = process.env.VUE_APP_BRAND_DOMAIN__API; + if (host) return `https://${host.replace(/^https?:\/\//, "").replace(/\/$/, "")}`; + return undefined; +} + +function composeFromDiscrete() { + const name = + process.env.BRIEF_NAME || + process.env.VUE_APP_BRAND_NAME_SHORT || + process.env.VUE_APP_BRAND_NAME_LONG; + const apiBase = apiBaseFromEnv(); + const brandColor = + withHash(process.env.BRIEF_BRAND_COLOR) || withHash(process.env.VUE_APP_THEME_COLOR); + + const firebase = { + apiKey: process.env.BRIEF_FIREBASE_API_KEY || process.env.FIREBASE_apiKey || null, + projectId: process.env.BRIEF_FIREBASE_PROJECT_ID || process.env.FIREBASE_projectId || null, + // iOS Brief appId — do NOT reuse web FIREBASE_appId + appId: process.env.BRIEF_FIREBASE_APP_ID || null, + gcmSenderId: + process.env.BRIEF_FIREBASE_GCM_SENDER_ID || + process.env.FIREBASE_messagingSenderId || + null, + clientId: process.env.BRIEF_FIREBASE_CLIENT_ID || null, + reversedClientId: process.env.BRIEF_FIREBASE_REVERSED_CLIENT_ID || null, + storageBucket: + process.env.BRIEF_FIREBASE_STORAGE_BUCKET || process.env.FIREBASE_storageBucket || null, + databaseURL: + process.env.BRIEF_FIREBASE_DATABASE_URL === undefined + ? null + : process.env.BRIEF_FIREBASE_DATABASE_URL || null, + authDomain: process.env.BRIEF_FIREBASE_AUTH_DOMAIN || process.env.FIREBASE_authDomain || null, + }; + + if (!name && !apiBase && !Object.values(firebase).some((v) => v != null && v !== "")) { + return null; + } + + return { + name: name || "Task Tracker", + apiBase: apiBase || "", + brandColor: brandColor || "#000000", + firebase, + }; +} + +function minimalStub() { + return { + name: process.env.VUE_APP_BRAND_NAME_SHORT || "Task Tracker", + apiBase: apiBaseFromEnv() || "", + brandColor: withHash(process.env.VUE_APP_THEME_COLOR) || "#000000", + firebase: { + apiKey: process.env.FIREBASE_apiKey || null, + projectId: process.env.FIREBASE_projectId || null, + appId: null, + gcmSenderId: process.env.FIREBASE_messagingSenderId || null, + clientId: null, + reversedClientId: null, + storageBucket: process.env.FIREBASE_storageBucket || null, + databaseURL: null, + authDomain: process.env.FIREBASE_authDomain || null, + }, + }; +} + +function parseClientConfigBlob() { + const raw = process.env.BRIEF_CLIENT_CONFIG; + if (!raw || !raw.trim()) return null; + try { + const parsed = JSON.parse(raw); + if (!parsed || typeof parsed !== "object") { + throw new Error("BRIEF_CLIENT_CONFIG must be a JSON object"); + } + return parsed; + } catch (err) { + console.error(`[generate-brief-config] Invalid BRIEF_CLIENT_CONFIG: ${err.message}`); + process.exit(1); + } +} + +async function fetchRemoteConfig() { + const url = process.env.BRIEF_CONFIG_URL || DEFAULT_FETCH_URL; + console.log(`[generate-brief-config] Fetching ${url}`); + const res = await fetch(url, { + headers: { Accept: "application/json" }, + }); + if (!res.ok) { + throw new Error(`HTTP ${res.status} from ${url}`); + } + return res.json(); +} + +function writeConfig(config) { + fs.mkdirSync(path.dirname(OUT_PATH), { recursive: true }); + fs.writeFileSync(OUT_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf8"); + console.log(`[generate-brief-config] Wrote ${path.relative(ROOT, OUT_PATH)}`); +} + +async function main() { + const mode = argMode(); + loadEnvFiles(mode); + + let config = parseClientConfigBlob(); + let source = "BRIEF_CLIENT_CONFIG"; + + if (!config) { + config = composeFromDiscrete(); + source = "env compose (BRIEF_* / VUE_APP_* / FIREBASE_*)"; + } + + const hasIosFields = + config && + config.firebase && + (config.firebase.appId || config.firebase.clientId || config.firebase.reversedClientId); + + if ( + (!config || !hasIosFields) && + (process.env.BRIEF_FETCH_CONFIG === "1" || process.env.BRIEF_FETCH_CONFIG === "true") + ) { + try { + config = await fetchRemoteConfig(); + source = "remote fetch"; + } catch (err) { + console.warn(`[generate-brief-config] Fetch failed: ${err.message}`); + if (!config) { + config = minimalStub(); + source = "minimal stub (fetch failed)"; + } else { + source = `${source} (fetch failed; kept compose)`; + } + } + } + + if (!config) { + config = minimalStub(); + source = "minimal stub"; + } + + writeConfig(config); + console.log(`[generate-brief-config] Source: ${source}`); +} + +main().catch((err) => { + console.error(`[generate-brief-config] ${err.stack || err}`); + process.exit(1); +}); From e3e9b758ab59a42eb119ffe2c5a1dd6d76a89bd3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 18:38:36 +0000 Subject: [PATCH 2/2] Prefer live API fetch for production Brief SSOT Document BRIEF_FETCH_CONFIG=1 + BRIEF_CONFIG_URL as the preferred cmutt deploy path so Pages copies client-config from the API (server .env is the only human edit). Discrete BRIEF_* remain for offline CI. When fetch is enabled, the generator tries the remote URL first. Co-authored-by: Sander Vonk --- README.md | 11 ++++-- example.env | 27 +++++++++----- scripts/generate-brief-config.js | 61 +++++++++++++++++--------------- 3 files changed, 59 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index f525fe9d..534a2c0e 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,16 @@ use `npm run serve -- --mode [mode]` to run with alternative `.env.[mode]` or `. nvm use 18 && npm run deploy ``` -Production Brief discovery (`/.well-known/brief.json` on `cmutt.app` / `mvtt.app`) is **generated at deploy time** from env — not a hand-edited JSON file. `deploy.js` runs `scripts/generate-brief-config.js` before `vue-cli-service build`, then publishes `dist/` to `gh-pages`. CI `npm run build` also regenerates via `prebuild`, but **live Pages only updates when you run `npm run deploy`** (or otherwise publish that `dist`). +Production Brief discovery (`/.well-known/brief.json` on `cmutt.app`) is **generated at deploy time** — never a committed production JSON file. `deploy.js` runs `scripts/generate-brief-config.js` before `vue-cli-service build`, then publishes `dist/` to `gh-pages`. CI `npm run build` also regenerates via `prebuild`, but **live Pages only updates when you run `npm run deploy`**. -Keep production `BRIEF_*` / `BRIEF_CLIENT_CONFIG` in sync with mvtt-server’s client-config env (same public allowlist). See `example.env` for the field list. Never commit service accounts; Firebase client fields are OK. +**Production cmutt (preferred SSOT):** copy brochure config from the live API so humans only edit server env (e.g. mvtt-server `.env.cmu-tt`). In the deploy `.env` / `.env.local`: + +``` +BRIEF_FETCH_CONFIG=1 +BRIEF_CONFIG_URL=https://api.cmutt.app/api/v1/client-config +``` + +Discrete `BRIEF_*` (and `.env.test`) remain for offline CI and forks — see `example.env`. Never commit service accounts; Firebase client fields are OK. or optionally when cmu-tt is side-by-side (requires all changes to be pushed to remote for full parity): diff --git a/example.env b/example.env index dc543a26..950a20cf 100644 --- a/example.env +++ b/example.env @@ -46,14 +46,23 @@ FIREBASE_serverRegion=us-central1 # ============================================================================= # NEVER commit real secrets. These Firebase *client* fields are public (same # values as in the iOS plist / web SDK). Do NOT put service accounts, Admin SDK -# keys, or shared API secrets here. +# keys, or shared API secrets here. Never commit a hand-edited production +# brief.json — it is always generated. # -# Keep production values in sync with mvtt-server BRIEF_* / client-config env -# (same allowlist schema). Prefer one documented contract; env compose is the -# source of truth for this Pages site — never hand-edit brief.json. +# --- Production cmutt deploy (PREFERRED) — Option C: fetch from live API --- +# Humans edit brochure values only in mvtt-server `.env.cmu-tt` (or equivalent). +# Pages copies GET /api/v1/client-config into /.well-known/brief.json at deploy. +# Put these in the *production* deploy env (e.g. .env / .env.local used by +# `npm run deploy`), not in this example or in .env.test: # -# Option A (preferred for parity with API): single JSON blob matching -# GET /api/v1/client-config — name, apiBase, brandColor, firebase{...} +# BRIEF_FETCH_CONFIG=1 +# BRIEF_CONFIG_URL=https://api.cmutt.app/api/v1/client-config +# +# --- Offline CI / forks — Option B: discrete BRIEF_* (below) --- +# .env.test uses these so `npm run build` works without network. Forks without +# a live API can keep composing locally the same way. +# +# --- Optional — Option A: single JSON blob --- # BRIEF_CLIENT_CONFIG={"name":"ORGTT","apiBase":"https://api.example.com","brandColor":"#f5c14b","firebase":{"apiKey":"...","projectId":"...","appId":"1:...:ios:...","gcmSenderId":"...","clientId":"...","reversedClientId":"...","storageBucket":"...","databaseURL":null,"authDomain":"..."}} # # Option B: compose from discrete vars (reuses FIREBASE_* / VUE_APP_* where they match) @@ -72,6 +81,6 @@ BRIEF_FIREBASE_REVERSED_CLIENT_ID=com.googleusercontent.apps.1000000000000-xxxxx # BRIEF_FIREBASE_AUTH_DOMAIN= # BRIEF_FIREBASE_DATABASE_URL= # -# Option C (CI only): fetch at build time when BRIEF_FETCH_CONFIG=1 -# BRIEF_FETCH_CONFIG=0 -# BRIEF_CONFIG_URL=https://api.example.com/api/v1/client-config \ No newline at end of file +# Option C (production cmutt — preferred; see block above): +# BRIEF_FETCH_CONFIG=1 +# BRIEF_CONFIG_URL=https://api.cmutt.app/api/v1/client-config diff --git a/scripts/generate-brief-config.js b/scripts/generate-brief-config.js index d0fdb49b..e14916f9 100644 --- a/scripts/generate-brief-config.js +++ b/scripts/generate-brief-config.js @@ -3,14 +3,17 @@ * Generate public/.well-known/brief.json from env (single source of truth). * * Priority: - * 1. BRIEF_CLIENT_CONFIG — full JSON blob (same schema as API client-config) - * 2. Compose from discrete BRIEF_* / VUE_APP_* / FIREBASE_* vars - * 3. If BRIEF_FETCH_CONFIG=1 — fetch BRIEF_CONFIG_URL or API client-config + * 1. If BRIEF_FETCH_CONFIG=1 — fetch BRIEF_CONFIG_URL (production cmutt: + * https://api.cmutt.app/api/v1/client-config). Server .env is the only + * place humans edit brochure values; Pages copies the live API response. + * 2. BRIEF_CLIENT_CONFIG — full JSON blob (same schema as API client-config) + * 3. Compose from discrete BRIEF_* / VUE_APP_* / FIREBASE_* vars + * (offline CI / .env.test / forks) * 4. Minimal stub so offline CI still builds * - * Keep production BRIEF_* values in sync with mvtt-server (same public allowlist). * Never put service accounts or server secrets here — Firebase web/iOS client - * fields are intentionally public. + * fields are intentionally public. Never commit a hand-edited production + * brief.json. */ const fs = require("fs"); @@ -185,44 +188,44 @@ function writeConfig(config) { console.log(`[generate-brief-config] Wrote ${path.relative(ROOT, OUT_PATH)}`); } +function wantFetch() { + return process.env.BRIEF_FETCH_CONFIG === "1" || process.env.BRIEF_FETCH_CONFIG === "true"; +} + async function main() { const mode = argMode(); loadEnvFiles(mode); - let config = parseClientConfigBlob(); - let source = "BRIEF_CLIENT_CONFIG"; - - if (!config) { - config = composeFromDiscrete(); - source = "env compose (BRIEF_* / VUE_APP_* / FIREBASE_*)"; - } - - const hasIosFields = - config && - config.firebase && - (config.firebase.appId || config.firebase.clientId || config.firebase.reversedClientId); + let config = null; + let source = null; - if ( - (!config || !hasIosFields) && - (process.env.BRIEF_FETCH_CONFIG === "1" || process.env.BRIEF_FETCH_CONFIG === "true") - ) { + // Production cmutt: prefer live API so server .env is the only human SSOT. + if (wantFetch()) { try { config = await fetchRemoteConfig(); - source = "remote fetch"; + source = "remote fetch (BRIEF_FETCH_CONFIG)"; } catch (err) { console.warn(`[generate-brief-config] Fetch failed: ${err.message}`); - if (!config) { - config = minimalStub(); - source = "minimal stub (fetch failed)"; - } else { - source = `${source} (fetch failed; kept compose)`; - } + } + } + + if (!config) { + config = parseClientConfigBlob(); + if (config) source = "BRIEF_CLIENT_CONFIG"; + } + + if (!config) { + config = composeFromDiscrete(); + if (config) { + source = wantFetch() + ? "env compose (fetch failed; BRIEF_* / VUE_APP_* / FIREBASE_*)" + : "env compose (BRIEF_* / VUE_APP_* / FIREBASE_*)"; } } if (!config) { config = minimalStub(); - source = "minimal stub"; + source = wantFetch() ? "minimal stub (fetch failed)" : "minimal stub"; } writeConfig(config);