Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7acc61978d | ||
|
|
f322df540d | ||
|
|
d72a3ca27f | ||
|
|
910db15b61 |
@@ -8,7 +8,7 @@ During play the UI has no persistent location, task, inventory, economy, status,
|
||||
|
||||
If the player asks to find or approach someone and no NPC is present, the direct dialogue scene may propose one person and an opening line. Rust creates that NPC only for the explicit request and commits the first exchange idempotently; a later turn continues the saved conversation. Clear requests about the current location are answered from its saved facts without asking the player to restate what they want to inspect.
|
||||
|
||||
The separate `/admin` route provides an authenticated, read-only operations view for player search/profile/trace, YAML prompt assets, generated Luau rules with validation results, and the append-only audit log. All privileged data comes from the Rust `/api/admin/...` API; the browser never connects directly to PostgreSQL. Backend endpoints require `accounts.is_admin=true`.
|
||||
The separate `/admin` route provides an authenticated operations view for player search/profile/trace, YAML prompt assets, generated Luau rules with validation results, runtime debug logging, and the append-only audit log. All privileged data comes from the Rust `/api/admin/...` API; the browser never connects directly to PostgreSQL. Backend endpoints require `accounts.is_admin=true`. The Driftloggar view lets an administrator switch `debug=true` on or off without restarting pods; the setting is stored in PostgreSQL and propagated to running replicas. Verbose pod logs include request, AI-call, validation, and commit metadata but never raw player text, prompts, model responses, sessions, or secrets. Full protected prompt/provider exchanges remain in the audited player trace.
|
||||
|
||||
The game is text- and dialogue-based and uses no images. Place descriptions include sound, smell, air/temperature, touch/surface, and spatial-orientation cues. Visible details may also be described in words, but are never required to understand the scene or play. The transcript is screen-reader announced, and Enter submits the input while Shift+Enter inserts a newline; no separate send button is required.
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@ apiVersion: v2
|
||||
name: ai-spel-frontend
|
||||
description: Static web frontend for the Swedish life simulator
|
||||
type: application
|
||||
version: 0.1.32
|
||||
appVersion: "0.1.32"
|
||||
version: 0.1.34
|
||||
appVersion: "0.1.34"
|
||||
+47
-1
@@ -63,6 +63,7 @@
|
||||
<button type="button" data-admin-view="players">Spelare</button>
|
||||
<button type="button" data-admin-view="prompts">Prompter</button>
|
||||
<button type="button" data-admin-view="rules">Luau-regler</button>
|
||||
<button type="button" data-admin-view="logging">Driftloggar</button>
|
||||
<button type="button" data-admin-view="audit">Revisionslogg</button>
|
||||
<button type="button" id="admin-logout">Logga ut</button>
|
||||
</nav>
|
||||
@@ -116,6 +117,24 @@
|
||||
}
|
||||
return line;
|
||||
}
|
||||
function biographyContextText(context) {
|
||||
if (!context || typeof context !== 'object') return '';
|
||||
const details = [];
|
||||
if (typeof context.current_situation === 'string' && context.current_situation.trim()) {
|
||||
details.push(`Nuvarande situation: ${context.current_situation.trim()}.`);
|
||||
}
|
||||
for (const [key, label] of [
|
||||
['skills', 'Färdigheter'],
|
||||
['possessions', 'Ägodelar som nämnts'],
|
||||
['obligations', 'Åtaganden'],
|
||||
['past_events', 'Tidigare händelser'],
|
||||
['known_people', 'Kända personer'],
|
||||
]) {
|
||||
const values = context[key];
|
||||
if (Array.isArray(values) && values.length) details.push(`${label}: ${values.join(', ')}.`);
|
||||
}
|
||||
return details.length ? `\n\nStartuppgifter att granska: ${details.join(' ')}` : '';
|
||||
}
|
||||
function explainDialogueError(error) {
|
||||
const code = String(error?.message || '');
|
||||
const known = {
|
||||
@@ -392,6 +411,32 @@
|
||||
adminElement('h2', 'Revisionslogg');
|
||||
for (const entry of result.entries) adminJson(`${entry.created_at} · ${entry.action} · ${entry.admin_account_id || entry.operator_identity}`, entry);
|
||||
}
|
||||
async function loadAdminLogging() {
|
||||
adminContent.replaceChildren();
|
||||
const result = await request('/api/admin/settings/logging');
|
||||
adminElement('h2', 'Driftloggar');
|
||||
adminElement('p', 'debug=true skriver detaljerade tekniska händelser för HTTP, AI-anrop, validering och commit. debug=false behåller korta tekniska åtkomstloggar och fel. Ändringen sparas i databasen och börjar gälla utan omstart. Spelartext, promptinnehåll, AI-svar, sessionsuppgifter och hemligheter skrivs aldrig till poddloggarna.');
|
||||
const form = adminElement('form');
|
||||
const label = adminElement('label', undefined, form);
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox'; checkbox.name = 'debug'; checkbox.checked = result.debug;
|
||||
label.append(checkbox, document.createTextNode(' Aktivera verbose felsökningsloggar (debug=true)'));
|
||||
const save = adminElement('button', 'Spara loggnivå', form); save.type = 'submit';
|
||||
const status = adminElement('p', `Aktuell nivå: debug=${result.debug}`);
|
||||
status.setAttribute('role', 'status');
|
||||
form.addEventListener('submit', async event => {
|
||||
event.preventDefault(); save.disabled = true;
|
||||
try {
|
||||
const updated = await request('/api/admin/settings/logging', {
|
||||
method: 'PUT', headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify({debug: checkbox.checked}),
|
||||
});
|
||||
status.textContent = `Aktuell nivå: debug=${updated.debug}. Ändringen gäller utan omstart.`;
|
||||
} catch (error) {
|
||||
status.textContent = `Loggnivån kunde inte ändras: ${error.message}`;
|
||||
} finally { save.disabled = false; }
|
||||
});
|
||||
}
|
||||
async function openAdmin() {
|
||||
showAdmin();
|
||||
adminContent.setAttribute('aria-busy', 'true');
|
||||
@@ -515,7 +560,7 @@
|
||||
body: JSON.stringify({input: message, current_draft: characterDraft}),
|
||||
});
|
||||
characterDraft = proposal.draft;
|
||||
appendLine('assistant', `${proposal.message}\n\nFörslag: ${characterDraft.name}, ${characterDraft.age} år.\n${characterDraft.biography}\n\nSkriv ändringar eller “godkänn”.`);
|
||||
appendLine('assistant', `${proposal.message}\n\nFörslag: ${characterDraft.name}, ${characterDraft.age} år.\n${characterDraft.biography}${biographyContextText(characterDraft.life_context)}\n\nSkriv ändringar eller “godkänn”.`);
|
||||
} catch (error) {
|
||||
appendLine('assistant', `Jag kunde inte ta fram ett biografiförslag: ${error.message}. Försök igen när tjänsten svarar.`);
|
||||
}
|
||||
@@ -632,6 +677,7 @@
|
||||
if (button.dataset.adminView === 'players') await loadAdminPlayers();
|
||||
else if (button.dataset.adminView === 'prompts') await loadAdminPrompts();
|
||||
else if (button.dataset.adminView === 'rules') await loadAdminRules();
|
||||
else if (button.dataset.adminView === 'logging') await loadAdminLogging();
|
||||
else await loadAdminAudit();
|
||||
} catch (error) { adminContent.replaceChildren(); adminElement('p', `Vyn kunde inte läsas: ${error.message}`); }
|
||||
finally { adminContent.setAttribute('aria-busy', 'false'); }
|
||||
|
||||
@@ -36,6 +36,8 @@ grep -q 'id="admin-view"' index.html
|
||||
grep -q '/api/admin/players?' index.html
|
||||
grep -q "request('/api/admin/prompts'" index.html
|
||||
grep -q "request('/api/admin/rules'" index.html
|
||||
grep -q "request('/api/admin/settings/logging'" index.html
|
||||
grep -q 'data-admin-view="logging"' index.html
|
||||
grep -q '/api/admin/audit?limit=200' index.html
|
||||
grep -q 'entry.data.external_call' index.html
|
||||
grep -q 'Externt tjänsteanrop' index.html
|
||||
|
||||
Reference in New Issue
Block a user