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