{
  "format": "bridgit-template/1",
  "id": "situation-counter",
  "version": "1.0.0",
  "kind": "applet",
  "name": "Situation counter",
  "summary": "Spot and count one camera situation, like a pot boiling over or a dog licking itself.",
  "runtime": {
    "adapter": "android.package.v1",
    "applet_key": "situation-counter",
    "status": "installable_package",
    "supports_dynamic_execution": false
  },
  "config_schema": {
    "type": "object",
    "properties": {
      "situation": {
        "type": "string",
        "minLength": 1,
        "maxLength": 300
      },
      "camera": {
        "type": "string",
        "enum": [
          "rear",
          "front"
        ]
      },
      "interval_s": {
        "type": "integer",
        "minimum": 10,
        "maximum": 3600
      }
    },
    "required": [
      "situation",
      "camera",
      "interval_s"
    ],
    "additionalProperties": false
  },
  "defaults": {
    "situation": "pot_boiling_over",
    "camera": "rear",
    "interval_s": 20
  },
  "observation_schema": {
    "type": "object",
    "properties": {},
    "required": [],
    "additionalProperties": false
  },
  "metrics": [],
  "processing": {
    "watches": [
      {
        "id": "main",
        "request": {
          "situation": {
            "$input": "situation"
          },
          "data": [
            "camera"
          ],
          "camera": {
            "$input": "camera"
          },
          "interval_s": {
            "$input": "interval_s"
          },
          "expires_in_s": 7776000,
          "max_paid_per_hour": 30,
          "local": {
            "change_check": true,
            "sensitivity": "normal",
            "max_reuse_s": 300
          }
        }
      }
    ]
  },
  "permissions": {
    "required": [
      "camera",
      "interactions",
      "watches"
    ],
    "optional": [],
    "remote": [],
    "automatic_grants": false
  },
  "lifecycle": {
    "states": [
      "awaiting_owner",
      "active",
      "paused",
      "removed"
    ],
    "start": "The owner approves the package once on the phone; the phone creates every part.",
    "stop": "Pause stops every part and keeps counts; Remove deletes every part and its permissions.",
    "delete": "Remove deletes everything the applet created on the phone."
  },
  "migration": {
    "strategy": "explicit_owner_opt_in",
    "from": null,
    "rules": [
      "A revision that adds a permission waits for the owner; otherwise the phone swaps it in and keeps unchanged parts."
    ],
    "rollback": "Install the earlier revision as an update."
  },
  "ui": {
    "inputs": {
      "situation": {
        "label": "What to count",
        "help": "A library id: dog_licking_itself, dog_scratching, dog_on_couch, cat_on_counter, pot_boiling_over, smoke_in_room, water_on_floor, sunlight_in_room, lights_on, person_in_view. Or describe it in your own words, with spaces (\"the cat is on the kitchen counter\")."
      },
      "camera": {
        "label": "Camera",
        "help": "The phone camera that faces what to watch."
      },
      "interval_s": {
        "label": "Seconds between checks",
        "help": "10 to 3600. Unchanged pictures reuse the last answer for free; at most 30 paid checks an hour."
      }
    },
    "card": {
      "asset": "card.html",
      "title": "Situation counter",
      "data": {
        "mode": "watch",
        "refresh_label": "Read today’s count",
        "read_suffix": "/tally",
        "count_heading": "Today’s count",
        "freshness_seconds": 120,
        "empty": "No checks yet today. Not seen is not the same as didn’t happen.",
        "owner_intro": "Counts how often the camera sees one situation today.",
        "owner_note": "Sampled photos judged by a model. Gaps and things out of frame are unknown, and it is not a safety alarm.",
        "id": "situation-counter",
        "title": "Situation counter",
        "summary": "Counts how often the camera sees one situation today.",
        "boundary": "Sampled photos judged by a model. Gaps and things out of frame are unknown, and it is not a safety alarm.",
        "resource_id": {
          "$watch": "main"
        }
      }
    }
  },
  "data_policy": {
    "backend_default": "device_only",
    "device_only": {
      "server_registration": false,
      "uploads": false,
      "remote_models": false,
      "derived_telemetry": false,
      "queue": false
    },
    "scope": [
      "device_id",
      "applet_instance_id"
    ],
    "sharing": {
      "role": "none"
    },
    "retention": {
      "local": "Counts and checks stay on the phone until the applet is removed."
    },
    "enable": "Owner approval on the phone.",
    "disable": "Pause or Remove on the phone.",
    "execution": "Runs on the phone with no agent. Only explicit model calls named in the watch (model clef/jev) or weather requests leave the phone."
  },
  "assets": {
    "card.html": {
      "media_type": "text/html",
      "text": "<!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*/{}/*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||{};if(RECIPE.count_heading){var seen=Object.keys(labels).reduce(function(n,k){return n+(Number(labels[k].episodes)||0)},0);html='<h2>'+esc(RECIPE.count_heading)+'</h2><div class=\"tiles\">'+tile('Times seen today',today.checks===undefined?'—':seen)+tile('Readings',today.checks===undefined?'—':today.checks)+'</div>'}else\nhtml='<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",
      "sha256": "ef5c5a9c1e3f6d867fb330d7b7b46caf35a9aac1843a006d48236c5270a56466"
    }
  }
}
