{
  "kind": "html",
  "title": "Pet activity monitor",
  "html": "<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Bridgit recipe</title>\n<style>\n:root{--ink:#2b1834;--paper:#fff8ed;--line:#e6dbe9;--muted:#6f5f75;--accent:#704394;--lav:#efe4f7;--ok:#e2f3e8}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:15px/1.5 system-ui,sans-serif}main{max-width:440px;margin:auto;padding:22px 18px 28px}.eyebrow{font-size:11px;letter-spacing:.12em;text-transform:uppercase;font-weight:700;color:var(--accent)}h1{font:600 28px/1.15 Georgia,serif;margin:7px 0 12px}.intro{color:var(--muted);font-size:14px}.panel{border:1px solid var(--line);border-radius:20px;background:white;padding:18px;margin:18px 0;box-shadow:0 8px 24px #2b183408}.pill{display:inline-block;border-radius:99px;padding:4px 10px;background:var(--lav);color:var(--accent);font-size:12px;font-weight:700}body[data-state=error] .pill{background:#fde9df;color:#a23520}body[data-state=stale] .pill{background:#fff0c7;color:#785513}h2{font-size:15px;margin:12px 0 5px}#updated,.note{color:var(--muted);font-size:12px}.tiles{display:grid;grid-template-columns:1fr 1fr;gap:10px}.tile{background:#efe4f7;border-radius:12px;padding:12px}.tile strong,.tile small{display:block}.tile strong{font-size:18px}.tile small{font-size:11px;color:var(--muted);margin-top:4px}.history{padding:0;list-style:none}.history li{display:flex;justify-content:space-between;gap:10px;border-bottom:1px solid var(--line);padding:10px 0}.history li>span:last-child{text-align:right}.metric{background:var(--paper);border-radius:12px;padding:12px;margin-top:12px;overflow-wrap:anywhere;font-size:13px}textarea{font:inherit;color:var(--ink);background:var(--paper);width:100%;min-height:120px;border:1px solid var(--line);border-radius:12px;padding:12px}.actions{display:grid;grid-template-columns:1fr 1fr;gap:9px}button{border:1px solid var(--line);border-radius:12px;background:white;color:var(--ink);font:600 14px system-ui;padding:13px 8px;cursor:pointer;min-height:46px}button.primary{grid-column:1/-1;background:var(--ink);color:white}button:disabled{opacity:.45;cursor:default}button:focus-visible,textarea:focus-visible{outline:3px solid var(--accent);outline-offset:3px}#confirmation{padding:12px;border-radius:12px;background:var(--ok);font-size:13px}footer{margin-top:18px;border-top:1px solid var(--line);padding-top:12px;font-size:12px;color:var(--muted)}\n</style></head>\n<body data-state=\"empty\"><main><p class=\"eyebrow\">Bridgit · Builder recipe</p><h1 id=\"title\"></h1><p class=\"intro\" id=\"intro\"></p>\n<section class=\"panel\" aria-live=\"polite\"><span class=\"pill\" id=\"state\">Empty</span><h2 id=\"message\"></h2><p id=\"updated\">No reading yet</p><div id=\"detail\" class=\"metric\" hidden></div><label id=\"draft-label\" hidden>Confirm or edit the draft<textarea id=\"draft\"></textarea></label></section>\n<div class=\"actions\"><button id=\"refresh\" class=\"primary\"></button><button id=\"pause\">Pause</button><button id=\"stop\">Stop</button><button id=\"answer\" class=\"primary\">Send confirmed draft</button></div><p id=\"confirmation\" role=\"status\" hidden></p>\n<footer id=\"boundary\"></footer><p class=\"note\">No automatic capture or polling. Refresh is explicit. A sent request is confirmed only after Bridgit persists the answer.</p></main>\n<script>\n// Authored visual language adapts docs/examples/pet-monitor-card.html and public/samples/action-card.html.\nvar RECIPE = /*RECIPE*/{\"mode\":\"watch\",\"refresh_label\":\"Read activity history\",\"read_suffix\":\"/tally\",\"freshness_seconds\":120,\"empty\":\"No visible activity samples yet. Gaps and absence are unknown coverage.\",\"owner_intro\":\"A little window into your pet’s day.\",\"owner_note\":\"Sampled observations, not continuous coverage. Gaps and absence are unknown.\",\"id\":\"pet-monitor\",\"title\":\"Pet activity monitor\",\"summary\":\"A little window into your pet’s day.\",\"boundary\":\"Sampled observations, not continuous coverage. Gaps and absence are unknown.\",\"resource_id\":null}/*END RECIPE*/;\nvar bridge = window.Bridgit || window.Bridget;\nvar busy=false, paused=false, stopped=false, revision=0, answered=false, sending=false, lastAt=null, lastBody=null;\nfunction el(id){return document.getElementById(id)}\nfunction show(state,message){document.body.dataset.state=state;el('state').textContent=state.charAt(0).toUpperCase()+state.slice(1);el('message').textContent=message;controls()}\nfunction controls(){el('refresh').disabled=busy||paused||stopped;el('pause').disabled=busy||stopped;el('stop').disabled=busy||stopped;el('answer').disabled=busy||answered||sending||stopped||paused||!el('draft').value.trim();el('pause').textContent=paused?'Resume':'Pause'}\nfunction esc(v){return String(v===null||v===undefined?'Unknown':v).replace(/[&<>\"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]})}\nfunction time(v){var n=typeof v==='number'?v:Date.parse(v);return Number.isFinite(n)?new Date(n).toLocaleString(undefined,{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}):'Time unavailable'}\nfunction tile(label,value){return '<div class=\"tile\"><strong>'+esc(value)+'</strong><small>'+esc(label)+'</small></div>'}\nfunction rows(items){return '<ul class=\"history\">'+items.map(function(i){return '<li>'+i+'</li>'}).join('')+'</ul>'}\nfunction detail(body){lastBody=body;el('detail').hidden=false;var html='',text=body.transcript===undefined?body.text:body.transcript;\nif(RECIPE.mode==='watch'&&RECIPE.read_suffix==='/tally'){\nvar today=body.today||{},labels=today.labels||{};html='<h2>Today’s observed activity</h2><div class=\"tiles\">'+tile('Observed minutes',today.watched_minutes===undefined?'—':today.watched_minutes)+tile('Readings',today.checks===undefined?'—':today.checks)+'</div>';\nhtml+=rows(Object.keys(labels).map(function(k){var v=labels[k];return '<span>'+esc(k)+'</span><span>'+esc(v.episodes)+' episodes · '+esc(v.minutes)+' min</span>'}));\nvar episodes=(body.episodes||[]).slice(-5).reverse();html+='<h2>Recent episodes</h2>'+rows(episodes.map(function(e){return '<span>'+esc(e.label)+'</span><span>'+esc(time(e.start))+' · '+esc(e.minutes)+' min</span>'}));if(!episodes.length)html+='<p>No episodes yet. Missing coverage is unknown.</p>';\n}else if(RECIPE.mode==='watch'){\nvar checks=body.recent_checks||[],last=body.last_check||{};html='<div class=\"tiles\">'+tile('Checks',body.checks===undefined?'—':body.checks)+tile('Change matches',body.matches===undefined?'—':body.matches)+'</div><h2>Recent image checks</h2>'+rows(checks.slice(-5).reverse().map(function(c){return '<span>'+esc(time(c.at))+'</span><span>'+esc(c.error?'Unavailable':c.skipped?'Skipped':c.matched?'Image changed':'No match / baseline')+'</span>'}));if(!checks.length)html+='<p>No retained checks yet.</p>';html+='<p class=\"note\">Image difference is not person or danger detection.</p>';\n}else if(RECIPE.mode==='weather'){\nvar now=body.now||{},next=body.next_24_hours||{};html='<h2>'+esc(now.summary||'Conditions unavailable')+'</h2><div class=\"tiles\">'+tile('Temperature',now.temperature_c===undefined?'—':now.temperature_c+' °C')+tile('Wind',now.wind_speed_mps===undefined?'—':now.wind_speed_mps+' m/s')+'</div><h2>Next 24 hours</h2><div class=\"tiles\">'+tile('Low / high',next.min_temperature_c===undefined||next.max_temperature_c===undefined?'—':next.min_temperature_c+' / '+next.max_temperature_c+' °C')+tile('Rain / snow',next.precipitation_mm===undefined?'—':next.precipitation_mm+' mm')+'</div><p class=\"note\">'+esc(typeof body.attribution==='string'?body.attribution:'Weather: MET Norway')+' · Forecast '+esc(time(body.forecast_updated_at))+'</p>';\n}else if(RECIPE.mode==='lan'){\nhtml='<h2>Devices shared with this agent</h2>'+rows((body.devices||[]).map(function(d){return '<span><strong>'+esc(d.name)+'</strong><br><small>'+esc(d.adapter)+'</small></span><span>'+esc(d.availability==='online'?'Verified online':d.availability==='not_checked'?'Not checked':'Availability unknown')+'<br><small>'+esc(d.checked_at?time(d.checked_at):'No current probe')+'</small></span>'}));if(!(body.devices||[]).length)html+='<p>No enrolled devices. Add one through owner-approved setup.</p>';html+='<p class=\"note\">Inventory refresh does not probe devices or move hardware.</p>';\n}else if(RECIPE.mode==='voice'){\nhtml='<div class=\"tiles\">'+tile('Bounded recording',body.duration_seconds===undefined?'—':body.duration_seconds+' seconds')+tile('Recognition',body.on_device===true?'On device':'Unconfirmed')+'</div><p>Review the transcript below before requesting a save.</p>';\n}else{html='<div class=\"tiles\">'+tile('Recognition',body.on_device===true?'On device':'Unconfirmed')+tile('Script',body.script||'Unknown')+'</div><p>Check amount, date and currency in the draft. OCR has not verified them.</p>'}\nel('detail').innerHTML=html;el('draft-label').hidden=typeof text!=='string';el('answer').hidden=typeof text!=='string';el('draft').value=typeof text==='string'?text:''}\nfunction sampleAt(body){return body.last_seen_at||body.last_check&&body.last_check.at||body.recorded_at||body.fetched_at||null}\nfunction render(body){detail(body);lastAt=sampleAt(body);el('updated').textContent=lastAt?'Source observed '+time(lastAt):'Read now; source observation time unavailable';if(body.last_check&&(body.last_check.error||body.last_check.skipped)){show('error',body.last_check.message||'Latest observation unavailable. Earlier readings are retained.');return}var empty=(RECIPE.mode==='ocr'&&(!body.text||!body.text.trim()))||(RECIPE.mode==='voice'&&(!body.transcript||!body.transcript.trim()))||Array.isArray(body.devices)&&body.devices.length===0||RECIPE.mode==='watch'&&!body.last_check&&!body.last_seen_at;var expiry=body.expires_at&&Date.parse(body.expires_at);var stale=body.stale===true||Number.isFinite(expiry)&&expiry<Date.now()||lastAt&&Date.now()-Date.parse(lastAt)>RECIPE.freshness_seconds*1000;show(stale?'stale':empty?'empty':'ready',stale?'This reading needs a refresh':empty?RECIPE.empty:'Reading received. Check its source time.');controls()}\nasync function act(request){if(!bridge||typeof bridge.act!=='function')throw Error('Open this card in a compatible Bridgit build. Browser preview has no device access.');var result=await bridge.act(request);if(!result||!result.ok)throw Error(result&&result.body&&result.body.message||result&&result.error||'Action failed');return result.body}\nfunction request(){if(RECIPE.mode==='watch'){if(!RECIPE.resource_id)throw Error('Controller must configure the watch_id returned by start.');return {method:'GET',path:'/v1/watches/'+encodeURIComponent(RECIPE.resource_id)+RECIPE.read_suffix}}if(RECIPE.mode==='ocr'){if(!Number.isInteger(RECIPE.gallery_id))throw Error('Controller must configure the owner-selected gallery_id.');return {method:'POST',path:'/v1/ocr',body:{gallery_id:RECIPE.gallery_id}}}if(RECIPE.mode==='voice')return {method:'POST',path:'/v1/mic/record',body:{duration_seconds:10,transcribe:true,return_audio:false,language:'en-US',store:[]}};if(RECIPE.mode==='weather')return {method:'GET',path:'/v1/weather',query:{hours:3}};return {method:'GET',path:'/v1/lan/devices'}}\nasync function refresh(){if(busy||paused||stopped)return;busy=true;var turn=++revision;show('loading',RECIPE.mode==='voice'?'Recording one 10-second clip…':'Reading from the phone…');try{var body=await act(request());if(turn===revision&&!stopped&&!paused)render(body)}catch(e){if(turn===revision&&!stopped&&!paused)show('error',e.message)}finally{busy=false;controls()}}\nasync function pause(){if(busy||stopped)return;busy=true;var next=!paused;revision++;try{if(RECIPE.mode==='watch'){if(!RECIPE.resource_id)throw Error('Start and configure a watch first.');var b=await act({method:'POST',path:'/v1/watches/'+encodeURIComponent(RECIPE.resource_id)+'/pause',body:{paused:next}});if(b.user_paused!==next)throw Error('Pause state was not confirmed by the phone.')}paused=next;show(paused?'paused':'empty',paused?'Paused. Existing readings keep their timestamps.':'Resumed. Refresh to read current state.')}catch(e){show('error',e.message)}finally{busy=false;controls()}}\nfunction send(action){if(answered||sending)return;if(!bridge||typeof bridge.respond!=='function'){show('error','Browser preview cannot send an answer.');return}sending=true;controls();el('confirmation').hidden=false;el('confirmation').textContent='Waiting for Bridgit to persist the request…';bridge.respond(JSON.stringify({recipe_id:RECIPE.id,action:action,text:action==='confirm'?el('draft').value:null,source_at:lastAt,requested_at:new Date().toISOString()}))}\nasync function stop(){if(busy||stopped)return;busy=true;revision++;try{if(RECIPE.mode==='watch'){if(!RECIPE.resource_id)throw Error('No watch_id is configured; no running watch was stopped.');var b=await act({method:'DELETE',path:'/v1/watches/'+encodeURIComponent(RECIPE.resource_id)});if(b.deleted!==RECIPE.resource_id)throw Error('The phone did not confirm this watch was deleted.');stopped=true;show('stopped','Watch deletion confirmed by the phone.')}else{stopped=true;show('stopped',answered?'Card stopped. This card already answered; contact your agent to stop any remaining job.':'Card stopped. Controller shutdown is requested separately.');send('stop')}}catch(e){show('error',e.message)}finally{busy=false;controls()}}\ndocument.addEventListener('bridgit:answered',function(){answered=true;sending=false;el('confirmation').hidden=false;el('confirmation').textContent='Request saved by Bridgit. Controller processing has not been confirmed.';controls()});\nel('refresh').onclick=refresh;el('pause').onclick=pause;el('stop').onclick=stop;el('answer').onclick=function(){send('confirm')};el('draft').oninput=controls;\nel('answer').hidden=true;el('title').textContent=RECIPE.title;el('intro').textContent=RECIPE.summary;el('boundary').textContent=RECIPE.boundary;el('refresh').textContent=RECIPE.refresh_label;show('empty',RECIPE.empty);\nsetInterval(function(){if(!busy&&!paused&&!stopped&&lastAt&&Date.now()-Date.parse(lastAt)>RECIPE.freshness_seconds*1000)show('stale','This reading needs a refresh')},15000);\n</script></body></html>\n",
  "no_takeover": true,
  "expires_in_seconds": 86400
}
