#!/usr/bin/env bash set -euo pipefail test -s index.html grep -q '' index.html grep -q 'id="frontend-version">__FRONTEND_VERSION__' index.html grep -q '' index.html grep -q 'const FEATURE_FLAGS = Object.freeze({localAI: false})' index.html grep -q 'if (!FEATURE_FLAGS.localAI) return Promise.resolve(null)' index.html grep -q "script.src = '/static/' + 'wllama-gemma3-v2/local-ai.js'" index.html ! grep -q '' index.html grep -q '' index.html grep -q 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0' nginx/default.conf grep -q 'Cross-Origin-Embedder-Policy "require-corp"' nginx/default.conf grep -q 'add_header Cross-Origin-Embedder-Policy "require-corp" always;' nginx/default.conf grep -q 'add_header Cross-Origin-Resource-Policy "same-origin" always;' nginx/default.conf grep -q 'application/javascript js mjs;' nginx/default.conf grep -q "request('/version')" index.html grep -q 'id="static-version">hämtar version…' index.html grep -q "request('/static/version.json', {cache: 'no-store'})" index.html grep -Fq "credentials: 'same-origin'" index.html grep -q 'id="dialogue" aria-label="Dialoghistorik" role="log" aria-live="polite"' index.html grep -q 'id="message" name="message"' index.html grep -q 'id="auth-intro"' index.html grep -q 'Skapa konto med ett användarnamn och ett lösenord på minst tolv tecken' index.html grep -q 'id="auth-repeat-intro"' index.html grep -q 'function playFirstVisitIntroduction' index.html grep -q 'id="auth-voice-status" role="status" aria-live="polite"' index.html grep -q 'activateFromUserGesture' index.html grep -q 'function showSpeechRecovery' index.html grep -q "retry.textContent = 'Läs senaste repliken'" index.html grep -q "location.pathname !== '/admin'" index.html grep -q "if (options.speak !== false && location.pathname !== '/admin')" index.html grep -q 'id="microphone" aria-pressed="false"' index.html grep -q 'type="submit" id="send-message">Skicka' index.html grep -q 'id="voice-status" role="status" aria-live="polite"' index.html grep -q 'id="speech-progress" aria-label="Nedladdning av svensk röstmodell"' index.html grep -q 'id="speech-progress-value"' index.html grep -q 'MB hämtade' index.html grep -q 'loading-runtime-assets' piper-worker.js grep -q 'Röstmodellen är inläst' voice.js grep -q "window.GameVoice.createVoiceController" index.html grep -q "addVoiceButton(item, 'Testa ljudet'" index.html grep -q 'Jag förstår – starta mikrofonen' index.html grep -q 'supportsRecognition' voice.js grep -q 'characterVoiceProfile' voice.js grep -q 'utterance.volume = 1' voice.js grep -q 'Webbläsaren har startat talsyntesen' voice.js grep -q 'WebKit versions before Safari 27' voice.js grep -q "voiceIdentity: speakingNpc.voice_identity" index.html grep -q "role: 'narrator', voiceIdentity: 'neutral'" index.html grep -q 'COPY voice.js /usr/share/nginx/html/voice.js' Dockerfile grep -Fq 'if (!currentAccount.is_admin && FEATURE_FLAGS.localAI)' index.html grep -Fq 'localAI?.start(currentAccount.id)' index.html grep -q "type === 'terms-required'" index.html grep -q 'Gemma-villkor' index.html grep -q 'lokal ai godkänn villkor' index.html grep -q 'lokal ai avvisa villkor' index.html grep -q 'client_assistance: clientAssistance' index.html grep -q 'COPY public/piper-runtime.js /usr/share/nginx/html/piper-runtime.js' Dockerfile grep -q 'COPY public/tts/piper-wasm-v1/' Dockerfile grep -q 'build:piper' .gitea/workflows/publish.yaml grep -q 'Piper' piper-runtime.js grep -q 'sv_SE-nst-medium' piper-worker.js grep -q "new Worker(workerUrl, {type: 'module'})" piper-runtime.js grep -q 'worker.js?v=' piper-runtime.js grep -q 'worker.onmessageerror' piper-runtime.js grep -q 'localPiperModelUrl' piper-worker.js grep -q 'External Piper asset blocked' piper-worker.js grep -q 'trackDownloadProgress' piper-worker.js grep -q 'new ReadableStream' piper-download-progress.mjs grep -q 'createOnnxWasmPaths(self.location.origin)' piper-worker.js grep -q 'configureOnnxRuntime(onnxRuntime)' piper-worker.js grep -q 'numThreads = 1' piper-onnx-config.mjs grep -q "hardwareConcurrency'" piper-onnx-config.mjs grep -q 'Piper could not be restricted to one processor thread' piper-onnx-config.mjs grep -q '/static/piper-wasm-v1/onnx/' piper-onnx-config.mjs grep -q 'PIPER_MODEL_ORIGIN' piper-model-routing.mjs grep -q '/static/piper-wasm-v1/voices/' piper-model-routing.mjs grep -q 'ai-spel-static' README.md grep -q 'ATTRIBUTION.txt' index.html grep -q "SpeechRecognition || win.webkitSpeechRecognition" voice.js grep -q "submitMessage(text, 'voice')" index.html grep -q 'Spelets repliker läses alltid upp' index.html grep -q "event.key === 'Enter' && !event.shiftKey" index.html grep -q "request('/api/character/draft'" index.html grep -q "request('/api/world/draft'" index.html grep -q "request('/api/dialogue'" index.html grep -q "request('/api/presence/heartbeat'" index.html grep -q "request('/api/players/nearby'" index.html grep -q "request('/api/player/messages'" index.html grep -q "Säg till Robin" index.html grep -q "result.intent === 'move' || result.intent === 'meet'" index.html grep -q 'dialogue_temporarily_unavailable:' index.html grep -q 'id="admin-view"' index.html grep -q 'id="play-layout"' index.html grep -q 'id="debug-panel"' index.html grep -q "request('/api/admin/self/trace?limit=200')" index.html grep -q 'function renderDecisionTrace' index.html grep -q 'JSON.stringify(group, null, 2)' index.html grep -q 'debug-enabled' 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 'data-admin-view="npcs"' index.html grep -q '/api/admin/npcs?q=' index.html grep -q '/api/admin/npcs/${encodeURIComponent(npcId)}/profile' index.html grep -q '/api/admin/npcs/${encodeURIComponent(npcId)}/identity' index.html grep -q '/api/admin/npcs/${encodeURIComponent(npcId)}/memories/${encodeURIComponent(memory.id)}' index.html grep -q '/api/admin/npcs/${encodeURIComponent(npcId)}/possessions' index.html grep -q 'Egenskaper och framtida profilfält' index.html grep -q 'Minnesrekonstruktioner' index.html grep -q 'entry.data.external_call' index.html grep -q 'Externt tjänsteanrop' index.html grep -q "request('/api/world/start'" index.html grep -q "request('/api/character'" index.html grep -q 'location = /healthz' nginx/default.conf if grep -Eq 'id="(new-character|world-setup|shop|buy-offer|balance|inventory|place-heading|npc-heading)"|Platsens sinnesintryck|Ekonomi och ägodelar|Köp på platsen|id="status"|id="logout"' index.html; then echo 'The active player UI must not contain permanent data panels, forms, or action buttons.' >&2 exit 1 fi if sed -n '/
/p' index.html | grep '/dev/null; then echo 'Only the send and microphone controls may appear beside the game text input.' >&2 exit 1 fi node --test tests/*.test.cjs echo 'Frontend source checks passed.'