Compare commits

..
4 Commits
Author SHA1 Message Date
Gitea Actions 4101be2447 Release 0.1.74 [auto-version-bump] 2026-10-24 03:26:39 +02:00
Ein Anderssono f54f672104 Match JSON indentation source literally
Build and publish frontend / publish (push) Successful in 55s
2026-10-24 03:26:12 +02:00
Ein Anderssono 0010743024 Update source checks for formatted JSON
Build and publish frontend / publish (push) Failing after 3s
2026-10-24 03:24:52 +02:00
Ein Anderssono a746c29acb Make admin JSON responses easier to scan
Build and publish frontend / publish (push) Failing after 2s
2026-10-24 03:23:37 +02:00
5 changed files with 73 additions and 8 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.73 0.1.74
+2 -2
View File
@@ -2,5 +2,5 @@ apiVersion: v2
name: ai-spel-frontend name: ai-spel-frontend
description: Static web frontend for the Swedish life simulator description: Static web frontend for the Swedish life simulator
type: application type: application
version: 0.1.73 version: 0.1.74
appVersion: "0.1.73" appVersion: "0.1.74"
+53 -4
View File
@@ -35,7 +35,23 @@
#admin-content .admin-simple-conversation article > .admin-story-player, #admin-content .admin-simple-conversation article > .admin-story-player,
#admin-content .admin-simple-conversation article > .admin-story-reply { display: block; margin-block: .85rem; white-space: pre-wrap; } #admin-content .admin-simple-conversation article > .admin-story-reply { display: block; margin-block: .85rem; white-space: pre-wrap; }
#admin-content .admin-simple-conversation article > .admin-story-player { font-weight: 650; } #admin-content .admin-simple-conversation article > .admin-story-player { font-weight: 650; }
#admin-content pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 32rem; overflow: auto; } #admin-content pre, #debug-trace pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 32rem; overflow: auto; }
.json-view { box-sizing: border-box; margin-block: .65rem; padding: .8rem; border: 1px solid #cbd5e1; border-radius: .4rem; background: #f8fafc; color: #1e293b; tab-size: 2; font: .84rem/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; }
.json-token-key { color: #075985; font-weight: 700; }
.json-token-string { color: #166534; }
.json-token-number { color: #6d28d9; font-weight: 650; }
.json-token-boolean { color: #b45309; font-weight: 750; }
.json-token-null { color: #be123c; font-style: italic; }
.json-token-punctuation { color: #64748b; }
@media (prefers-color-scheme: dark) {
.json-view { border-color: #475569; background: #111827; color: #e5e7eb; }
.json-token-key { color: #7dd3fc; }
.json-token-string { color: #86efac; }
.json-token-number { color: #c4b5fd; }
.json-token-boolean { color: #fcd34d; }
.json-token-null { color: #fda4af; }
.json-token-punctuation { color: #94a3b8; }
}
#admin-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; margin-block: 1rem; } #admin-toolbar { display: flex; flex-wrap: wrap; gap: .5rem; margin-block: 1rem; }
#player-search { display: flex; flex-wrap: wrap; align-items: end; gap: .5rem; } #player-search { display: flex; flex-wrap: wrap; align-items: end; gap: .5rem; }
body.debug-layout { max-width: none; margin: 0; padding: .5rem; height: 100vh; overflow: hidden; } body.debug-layout { max-width: none; margin: 0; padding: .5rem; height: 100vh; overflow: hidden; }
@@ -622,10 +638,43 @@
parent.append(element); parent.append(element);
return element; return element;
} }
function appendFormattedJson(pre, value, stringAsText = false) {
pre.className = 'json-view';
pre.setAttribute('aria-label', 'Formaterat JSON-svar');
const code = document.createElement('code');
if (stringAsText && typeof value === 'string') {
code.textContent = value;
pre.replaceChildren(code);
return pre;
}
const source = JSON.stringify(value, null, '\t') ?? 'null';
const tokens = /(\"(?:\\.|[^\"\\])*\"(?=\s*:))|(\"(?:\\.|[^\"\\])*\")|(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b)|(\bnull\b)|([{}\[\],:])|(\s+)/g;
let offset = 0;
for (const match of source.matchAll(tokens)) {
if (match.index > offset) code.append(document.createTextNode(source.slice(offset, match.index)));
const token = match[0];
if (match[7]) {
code.append(document.createTextNode(token));
} else {
const span = document.createElement('span');
span.className = match[1] ? 'json-token-key'
: match[2] ? 'json-token-string'
: match[3] ? 'json-token-number'
: match[4] ? 'json-token-boolean'
: match[5] ? 'json-token-null' : 'json-token-punctuation';
span.textContent = token;
code.append(span);
}
offset = match.index + token.length;
}
if (offset < source.length) code.append(document.createTextNode(source.slice(offset)));
pre.replaceChildren(code);
return pre;
}
function adminJson(title, value, parent = adminContent) { function adminJson(title, value, parent = adminContent) {
const details = adminElement('details', undefined, parent); const details = adminElement('details', undefined, parent);
adminElement('summary', title, details); adminElement('summary', title, details);
adminElement('pre', JSON.stringify(value, null, 2), details); appendFormattedJson(adminElement('pre', undefined, details), value);
return details; return details;
} }
async function loadAdminPlayers(query = '', offset = 0) { async function loadAdminPlayers(query = '', offset = 0) {
@@ -1163,7 +1212,7 @@
const valueHeading = document.createElement('strong'); const valueHeading = document.createElement('strong');
valueHeading.textContent = name; valueHeading.textContent = name;
const pre = document.createElement('pre'); const pre = document.createElement('pre');
pre.textContent = typeof value === 'string' ? value : JSON.stringify(value ?? 'Ingen uppgift sparad', null, 2); appendFormattedJson(pre, value ?? 'Ingen uppgift sparad', true);
card.append(valueHeading, pre); card.append(valueHeading, pre);
}; };
addValue('Indata', input); addValue('Indata', input);
@@ -1224,7 +1273,7 @@
const rawSummary = document.createElement('summary'); const rawSummary = document.createElement('summary');
rawSummary.textContent = 'Visa all rå spårdata'; rawSummary.textContent = 'Visa all rå spårdata';
const pre = document.createElement('pre'); const pre = document.createElement('pre');
pre.textContent = JSON.stringify(group, null, 2); appendFormattedJson(pre, group);
raw.append(rawSummary, pre); raw.append(rawSummary, pre);
details.append(summary, chain, raw); details.append(summary, chain, raw);
debugTrace.append(details); debugTrace.append(details);
+4 -1
View File
@@ -99,7 +99,10 @@ grep -q 'id="debug-panel"' index.html
grep -q "query AdminPlayerTrace" index.html grep -q "query AdminPlayerTrace" index.html
grep -q 'adminPlayerTrace(accountId: \$accountId, limit: \$limit, offset: \$offset)' index.html grep -q 'adminPlayerTrace(accountId: \$accountId, limit: \$limit, offset: \$offset)' index.html
grep -q 'function renderDecisionTrace' index.html grep -q 'function renderDecisionTrace' index.html
grep -q 'JSON.stringify(group, null, 2)' index.html grep -q 'appendFormattedJson(pre, group)' index.html
grep -Fq "JSON.stringify(value, null, '\\t')" index.html
grep -q 'json-token-key' index.html
grep -q 'json-token-string' index.html
grep -q 'debug-enabled' index.html grep -q 'debug-enabled' index.html
grep -q '/api/admin/players?' index.html grep -q '/api/admin/players?' index.html
grep -q 'Välj konto att följa' index.html grep -q 'Välj konto att följa' index.html
+13
View File
@@ -42,3 +42,16 @@ test('admin can switch to a storyteller-only reading view and return to diagnost
assert.match(page, /article > :not\(\.admin-story-player\):not\(\.admin-story-reply\)/); assert.match(page, /article > :not\(\.admin-story-player\):not\(\.admin-story-reply\)/);
assert.match(page, /article > \.admin-story-player,\s*#admin-content \.admin-simple-conversation article > \.admin-story-reply/); assert.match(page, /article > \.admin-story-player,\s*#admin-content \.admin-simple-conversation article > \.admin-story-reply/);
}); });
test('admin JSON responses use tab-indented syntax-colored accessible output', () => {
assert.match(page, /function appendFormattedJson/);
assert.match(page, /JSON\.stringify\(value, null, '\\t'\)/);
assert.match(page, /json-token-key/);
assert.match(page, /json-token-string/);
assert.match(page, /json-token-number/);
assert.match(page, /json-token-boolean/);
assert.match(page, /json-token-null/);
assert.match(page, /setAttribute\('aria-label', 'Formaterat JSON-svar'\)/);
assert.match(page, /appendFormattedJson\(pre, group\)/);
});