diff --git a/app.js b/app.js
index 5419eed..1d9c082 100644
--- a/app.js
+++ b/app.js
@@ -1,7 +1,10 @@
import { bucketForBristolType, buildTimmySummary, detectUrgentFlags, detectUrgentText, exportLedger, hasUrgentLedgerContext, importLedger, photoQualityMessage, sanitizeEntry, urgentChatMessage } from './src/domain.js';
import { mergeVisualSuggestion } from './src/analysis.js';
-const runtimeConfig = window.__TIMMY_CONFIG__ || { basePath: '/', stagingLabel: '' };
+const runtimeConfig = {
+ basePath: document.querySelector('meta[name="timmy-base-path"]')?.content || '/',
+ stagingLabel: document.querySelector('meta[name="timmy-staging-label"]')?.content || '',
+};
const BASE_PATH = runtimeConfig.basePath || '/';
const APP_ROOT = BASE_PATH === '/' ? '/' : `${BASE_PATH}/`;
function appPath(path='') { return `${APP_ROOT}${String(path).replace(/^\/+/, '')}`; }
@@ -154,7 +157,7 @@ async function runAiAnalysis(){showPhotoFirst('analyzing');try{const response=aw
function openLogger(){form=draft();photoDataUrl='';photoHint='';aiSuggestion=null;showLogStep(1)}
function showLogStep(step){
document.querySelector('.sheet-backdrop')?.remove();
- const wrap=document.createElement('div');wrap.className='sheet-backdrop';wrap.innerHTML=``;document.body.append(wrap);
+ const wrap=document.createElement('div');wrap.className='sheet-backdrop';wrap.innerHTML=``;document.body.append(wrap);
document.querySelector('#close-sheet').onclick=()=>wrap.remove();wrap.onclick=e=>{if(e.target===wrap)wrap.remove()};bindStep(step);
}
function stepBody(step){
diff --git a/server.mjs b/server.mjs
index bdac8e9..82f2ba1 100644
--- a/server.mjs
+++ b/server.mjs
@@ -31,6 +31,7 @@ const appRoot=basePath?`${basePath}/`:'/';
const stagingLabel=process.env.TIMMY_STAGING_LABEL?`Staging · ${release} · ${commit.slice(0,12)}`:'';
function sendJson(res,status,value){res.writeHead(status,{'content-type':'application/json; charset=utf-8','cache-control':'no-store','x-content-type-options':'nosniff'});res.end(JSON.stringify(value));}
+function escapeHtmlAttribute(value){return String(value).replace(/[&<>"]/g,character=>({'&':'&','<':'<','>':'>','"':'"'}[character]));}
function readJson(req,maxBytes=6*1024*1024){return new Promise((resolve,reject)=>{let size=0,tooLarge=false;const chunks=[];req.on('data',chunk=>{size+=chunk.length;if(size>maxBytes){tooLarge=true;return}chunks.push(chunk)});req.on('end',()=>{if(tooLarge)return reject(new AgentGatewayError(413,'Request is too large.'));try{resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')))}catch{return reject(new AgentGatewayError(400,'Invalid JSON request.'))}});req.on('error',reject)})}
function cookie(req,name){for(const part of String(req.headers.cookie||'').split(';')){const [key,...value]=part.trim().split('=');if(key===name)return decodeURIComponent(value.join('='))}return ''}
function requestOrigin(req){return String(req.headers.origin||'').replace(/\/$/,'')}
@@ -71,8 +72,8 @@ http.createServer(async(req,res)=>{
const info=await stat(path);if(info.isDirectory())path=join(path,'index.html');
let body=await readFile(path);
if(appPath==='/'||appPath==='/index.html'){
- const browserConfig=JSON.stringify({basePath:publicBasePath,stagingLabel}).replace(/',`
\n `).replace('\n