From ba08bba7de9a1f31effef1ce2c7881a2e128efc1 Mon Sep 17 00:00:00 2001 From: hzhang Date: Sat, 16 May 2026 17:55:16 +0100 Subject: [PATCH 1/2] fix: valid config.json + content-hashed bundle (cache-bust) - BuildConfig.sh: ${DEBUG:false} -> ${DEBUG:-false} and normalize to true/false. The old syntax produced empty -> invalid config.json ("DEBUG": }) when DEBUG was unset, breaking the whole frontend. - webpack: output [name].[contenthash].js so index.html references a unique bundle URL each build; eliminates stale CDN/browser bundle after deploys (no manual cache purge needed). Co-Authored-By: Claude Opus 4.7 (1M context) --- BuildConfig.sh | 7 ++++++- webpack.config.js | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/BuildConfig.sh b/BuildConfig.sh index e47d349..0c342ec 100644 --- a/BuildConfig.sh +++ b/BuildConfig.sh @@ -5,7 +5,12 @@ FRONTEND_HOST="${FRONTEND_HOST:-http://localhost:80}" KC_CLIENT_ID="${KC_CLIENT_ID:-labdev}" KC_HOST="${KC_HOST:-https://login.hangman-lab.top}" KC_REALM="${KC_REALM:-Hangman-Lab}" -DEBUG="${DEBUG:false}" +# Note: ${DEBUG:-false} (correct default syntax). The old ${DEBUG:false} +# produced an empty value when DEBUG was unset -> invalid config.json. +DEBUG="${DEBUG:-false}" +# DEBUG is emitted unquoted as a JSON boolean — guarantee it is exactly +# true/false so config.json can never be invalid JSON. +case "$DEBUG" in true|false) ;; *) DEBUG=false ;; esac rm -f /usr/share/nginx/html/config.js diff --git a/webpack.config.js b/webpack.config.js index 02eac43..122fe16 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,7 +5,11 @@ module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, './dist'), - filename: 'bundle.js', + // Content-hashed filename: index.html (injected by HtmlWebpackPlugin, + // and not edge-cached) points at a unique URL each build, so a new + // deploy is picked up immediately — no stale CDN/browser bundle. + filename: '[name].[contenthash].js', + chunkFilename: '[name].[contenthash].js', publicPath: '/', clean: true, }, From 3ec528701e9cc6833a83670882e898e2dc3c2782 Mon Sep 17 00:00:00 2001 From: hzhang Date: Sat, 16 May 2026 22:51:47 +0100 Subject: [PATCH 2/2] feat: apikey alias field + markdown/patch authorship display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ApiKeyCreationModal: required Alias input (reuse alias = renew, with banner); align roles to backend allowlist (guest -> user, default user); fix copy bug (generatedKey.key). - MarkdownContent + PatchCards: show author / created / last modified (+ by whom); formatDateTime helper (null -> "—"). Branched off fix/buildconfig-cachebust (carries the contenthash + BuildConfig fix already deployed to prod). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/components/Markdowns/MarkdownContent.js | 20 +++++++++++- src/components/Markdowns/PatchCards.js | 19 ++++++++++- src/components/Modals/ApiKeyCreationModal.js | 33 +++++++++++++++++--- src/lib/utils.js | 14 +++++++++ 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/components/Markdowns/MarkdownContent.js b/src/components/Markdowns/MarkdownContent.js index 57e2274..fe90cad 100644 --- a/src/components/Markdowns/MarkdownContent.js +++ b/src/components/Markdowns/MarkdownContent.js @@ -2,7 +2,8 @@ import React, { useEffect, useState } from "react"; import {Link, useParams} from "react-router-dom"; import "katex/dist/katex.min.css"; import "./MarkdownContent.css"; -import { Settings2, Pencil } from "lucide-react"; +import { Settings2, Pencil, User, Clock, History } from "lucide-react"; +import { formatDateTime } from "../../lib/utils"; import MarkdownView from "./MarkdownView"; import PatchCards from "./PatchCards"; import PermissionGuard from "../PermissionGuard"; @@ -85,6 +86,23 @@ const MarkdownContent = () => { +
+ + + {markdown.author || "—"} + + + + created {formatDateTime(markdown.created_at)} + + + + updated {formatDateTime(markdown.updated_at)} + {markdown.last_modified_by + ? ` by ${markdown.last_modified_by}` + : ""} + +
{ +
+ + + {patch.author || "—"} + + + + {formatDateTime(patch.created_at)} + + + edited {formatDateTime(patch.updated_at)} + {patch.last_modified_by + ? ` by ${patch.last_modified_by}` + : ""} + +
diff --git a/src/components/Modals/ApiKeyCreationModal.js b/src/components/Modals/ApiKeyCreationModal.js index 5587bc3..a715352 100644 --- a/src/components/Modals/ApiKeyCreationModal.js +++ b/src/components/Modals/ApiKeyCreationModal.js @@ -11,14 +11,16 @@ import { import { Button } from '../ui/button'; import { Input, Label } from '../ui/input'; -const AVAILABLE_ROLES = ['guest', 'creator', 'admin']; +// Must match the backend allowlist (admin|creator|user). +const AVAILABLE_ROLES = ['user', 'creator', 'admin']; const SELECT_CLASS = "flex h-9 w-full rounded-md border border-input bg-background/60 px-3 py-1 text-sm text-foreground transition-colors focus-visible:outline-none focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50"; const ApiKeyCreationModal = ({ isOpen, onClose }) => { + const [alias, setAlias] = useState(''); const [name, setName] = useState(''); - const [roles, setRoles] = useState(['guest']); + const [roles, setRoles] = useState(['user']); const [generatedKey, setGeneratedKey] = useState(null); const createApiKeyMutation = useCreateApiKey(); @@ -44,12 +46,17 @@ const ApiKeyCreationModal = ({ isOpen, onClose }) => { }; const handleGenerate = async () => { + if (!alias.trim()) { + alert('Alias is required'); + return; + } if (!name.trim()) { alert('API key name is required'); return; } try { const result = await createApiKeyMutation.mutateAsync({ + alias: alias.trim(), name: name.trim(), roles: roles }); @@ -61,7 +68,7 @@ const ApiKeyCreationModal = ({ isOpen, onClose }) => { }; const handleCopy = () => { - navigator.clipboard.writeText(generatedKey) + navigator.clipboard.writeText(generatedKey.key) .then(() => alert('API key copied to clipboard')) .catch(err => console.error('failed to copy api key:', err)); }; @@ -80,6 +87,20 @@ const ApiKeyCreationModal = ({ isOpen, onClose }) => { {!generatedKey ? (
+
+ + setAlias(e.target.value)} + /> +

+ Unique. Using an existing alias renews that + key (same key string, validity reset). +

+
{ ) : (
- Please copy your API key immediately! It will only be displayed once! + {generatedKey.renewed + ? "Key renewed — same key string, validity reset 15 days." + : "Please copy your API key immediately! It will only be displayed once!"}
@@ -154,7 +177,7 @@ const ApiKeyCreationModal = ({ isOpen, onClose }) => { {!generatedKey && ( diff --git a/src/lib/utils.js b/src/lib/utils.js index 40063a6..d878808 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -5,3 +5,17 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs) { return twMerge(clsx(inputs)); } + +/** Format a backend datetime string for display; '—' when missing/invalid. */ +export function formatDateTime(value) { + if (!value) return "—"; + const d = new Date(value); + if (isNaN(d.getTime())) return "—"; + return d.toLocaleString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +}