{
  "id": "situation-counter",
  "title": "Situation counter",
  "summary": "Spot and count one camera situation, like a pot boiling over or a dog licking itself.",
  "category": "run",
  "capability_ids": [
    "v1.watches",
    "v1.camera",
    "v1.interactions"
  ],
  "support": {
    "source_api_revision": 71,
    "tested_contract_api_revision": 71,
    "minimum_recipe_api_revision": 71,
    "released": "unknown; compare public changes and installed health",
    "installed": "unknown",
    "ready": null,
    "requirements": "GET /v1/health, /v1/capabilities and installed /v1/guide must confirm every operation and grant, and the installed API revision must be at least 71 (situation in POST /v1/watches). Source support alone is insufficient."
  },
  "execution": {
    "phone": [
      "Timer → camera → free local change check → Clef yes/no on changed photos → episode tally on the phone → watch.matched event and, for alert situations, an urgent owner notice.",
      "The phone expands situation into the tuned question, the tally (track = the situation id, or \"situation\" for own words) and the alert rule; unchanged photos reuse the last answer for free."
    ],
    "controller": [
      "Optional: read GET /v1/watches/{id}/tally to archive day totals; the card and the count work with no agent.",
      "Pick the situation with the owner: a library id from GET /v1/situations or the owner's own words."
    ],
    "server": "Only Clef photo checks leave the phone (model left out, so Clef is the default for a camera watch); direct or relay transport still requires a running reachable phone."
  },
  "boundaries": [
    "A count is sampled photos judged by a model, not continuous coverage or proof: gaps, a locked camera or the thing being out of frame are unknown, not zero.",
    "It does not diagnose health or detect fire: a smoke or boiling-over count is a nudge, not a safety alarm."
  ],
  "lifecycle": {
    "start": "POST /v1/watches with the start example; retain the returned watch_id. Then render/post the card with resource_id.",
    "update": "GET /v1/watches/{id}/tally first. To change the situation or timing, archive the tally, DELETE the old watch, POST the revised body. No watch PATCH exists.",
    "pause": "POST /v1/watches/{id}/pause with {\"paused\":true}; resume with false. Confirm user_paused from the returned metadata. Expiry is unchanged.",
    "stop": "DELETE /v1/watches/{id}; confirm deleted equals the saved id and watch.ended reason deleted. Archive needed tally BEFORE deletion. Cancel the interaction separately."
  },
  "failure": [
    "Own words that look like an id (lowercase_with_underscores) but aren't in the library are refused: describe it with spaces instead.",
    "A busy or locked camera is a gap; poor light, glare and odd angles cause misses and false counts. Aim the camera and try a known example first."
  ],
  "cost": [
    "Clef photo checks from the phone's model budget: at most max_paid_per_hour (30 here, under the 50-an-hour limit some phones have) an hour; unchanged pictures reuse the last answer for free. Read GET /v1/limits and lower the cap or lengthen interval_s for long runs."
  ],
  "extensions": [
    "Send {\"situation\": {\"describe\": \"...\", \"alert\": true}} to count own words and notify the owner when one starts.",
    "Add callback_when on the situation id, or rules, to reach a controller or show other notices; min and count_only_while tune what counts."
  ],
  "examples": [
    {
      "id": "start",
      "request": {
        "method": "POST",
        "path": "/v1/watches",
        "body": {
          "situation": "pot_boiling_over",
          "data": [
            "camera"
          ],
          "camera": "rear",
          "interval_s": 20,
          "expires_in_s": 7776000,
          "max_paid_per_hour": 30,
          "local": {
            "change_check": true,
            "sensitivity": "normal",
            "max_reuse_s": 300
          }
        }
      },
      "response": {
        "status": 201,
        "body": {
          "watch_id": "wsituation01",
          "label": "Pot boiling over",
          "user_paused": false,
          "created_at": "2026-11-04T10:00:00Z",
          "expires_at": "2027-02-02T10:00:00Z",
          "interval_s": 20,
          "data": [
            "camera"
          ],
          "checks": 0,
          "matches": 0,
          "last_match_at": null,
          "readings_held": 0,
          "last_check": null,
          "usage": {
            "model": "clef",
            "answered_locally": 0,
            "paid_calls": 0,
            "paid_cost_usd": 0,
            "paid_calls_last_hour": 0,
            "max_paid_per_hour": 30,
            "skipped_over_paid_cap": 0,
            "change_check": true
          },
          "spec": {
            "interval_s": 20,
            "question": null,
            "data": [
              "camera"
            ],
            "expires_in_s": 7776000,
            "questions": {
              "pot_boiling_over": {
                "instructions": "Is a pot or pan on the stove boiling over, with liquid or foam spilling over its rim? Judge from the newest photo: how likely is it that this is happening in it now?",
                "type": "noul"
              }
            },
            "paid_calls_per_hour_at_most": 30,
            "label": "Pot boiling over",
            "min_readings": 1,
            "max_paid_per_hour": 30,
            "local": {
              "max_reuse_s": 300,
              "learn_noise": true,
              "sensitivity": "normal",
              "change_check": true
            },
            "callback_url": null,
            "cooldown_s": 600,
            "repeat": true,
            "context": null,
            "model": "clef",
            "window": 1,
            "callback_when": [],
            "camera": "rear",
            "tally": {
              "min": 0.7,
              "gap_s": 180,
              "kind": "noul",
              "min_episode_readings": 1,
              "rules": [
                {
                  "cooldown_s": 300,
                  "id": "alert",
                  "do": {
                    "path": "/v1/interactions",
                    "method": "POST",
                    "body": {
                      "kind": "notice",
                      "text": "Seen on the camera just now.",
                      "title": "Pot boiling over",
                      "priority": "urgent"
                    }
                  },
                  "when": {
                    "of": "pot_boiling_over",
                    "within_s": 120,
                    "at_least": 1
                  }
                }
              ],
              "track": "pot_boiling_over",
              "seen": null,
              "labels": null,
              "keep_days": 30
            },
            "situation": "pot_boiling_over"
          },
          "tally_today": null,
          "rules_fired": 0,
          "callback_secret": null,
          "first_check_in_s": 0,
          "delivery": "watch.matched and watch.ended go to your registered callback if you have one, and to GET /v1/events (both need the interactions permission); watch.rule (a rule fired) goes the same way"
        }
      },
      "evidence": "Synthetic contract fixture, not a device observation."
    },
    {
      "id": "own-words",
      "request": {
        "method": "POST",
        "path": "/v1/watches",
        "body": {
          "situation": {
            "describe": "the cat is sleeping on the armchair",
            "alert": false
          },
          "data": [
            "camera"
          ],
          "camera": "rear",
          "interval_s": 60,
          "expires_in_s": 2592000,
          "max_paid_per_hour": 20,
          "local": {
            "change_check": true,
            "sensitivity": "normal",
            "max_reuse_s": 900
          }
        }
      },
      "response": {
        "status": 201,
        "body": {
          "watch_id": "wsituation02",
          "label": "The cat is sleeping on the armchair",
          "user_paused": false,
          "created_at": "2026-11-04T10:00:00Z",
          "expires_at": "2026-12-04T10:00:00Z",
          "interval_s": 60,
          "data": [
            "camera"
          ],
          "checks": 0,
          "matches": 0,
          "last_match_at": null,
          "readings_held": 0,
          "last_check": null,
          "usage": {
            "model": "clef",
            "answered_locally": 0,
            "paid_calls": 0,
            "paid_cost_usd": 0,
            "paid_calls_last_hour": 0,
            "max_paid_per_hour": 20,
            "skipped_over_paid_cap": 0,
            "change_check": true
          },
          "spec": {
            "interval_s": 60,
            "question": null,
            "data": [
              "camera"
            ],
            "expires_in_s": 2592000,
            "questions": {
              "situation": {
                "instructions": "Does the photo show this: the cat is sleeping on the armchair? Judge from the newest photo: how likely is it that this is happening in it now?",
                "type": "noul"
              }
            },
            "paid_calls_per_hour_at_most": 20,
            "label": "The cat is sleeping on the armchair",
            "min_readings": 1,
            "max_paid_per_hour": 20,
            "local": {
              "max_reuse_s": 900,
              "learn_noise": true,
              "sensitivity": "normal",
              "change_check": true
            },
            "callback_url": null,
            "cooldown_s": 600,
            "repeat": true,
            "context": null,
            "model": "clef",
            "window": 1,
            "callback_when": [],
            "camera": "rear",
            "tally": {
              "min": 0.7,
              "gap_s": 180,
              "kind": "noul",
              "min_episode_readings": 1,
              "rules": [],
              "track": "situation",
              "seen": null,
              "labels": null,
              "keep_days": 30
            },
            "situation": "situation"
          },
          "tally_today": null,
          "rules_fired": 0,
          "callback_secret": null,
          "first_check_in_s": 0,
          "delivery": "watch.matched and watch.ended go to your registered callback if you have one, and to GET /v1/events (both need the interactions permission); watch.rule (a rule fired) goes the same way"
        }
      },
      "evidence": "Synthetic contract fixture, not a device observation: own words count under the track id \"situation\"."
    },
    {
      "id": "stop",
      "request": {
        "method": "DELETE",
        "path": "/v1/watches/wsituation01"
      },
      "response": {
        "status": 200,
        "body": {
          "deleted": "wsituation01"
        }
      },
      "evidence": "Synthetic contract fixture, not a device observation."
    }
  ],
  "card": {
    "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."
  },
  "evidence": [
    "app/src/main/java/cc/bridgit/app/watch/Situations.kt",
    "app/src/main/java/cc/bridgit/app/watch/WatchRules.kt",
    "app/src/main/java/cc/bridgit/app/watch/WatchTally.kt",
    "app/src/main/java/cc/bridgit/app/watch/Watches.kt",
    "app/src/main/java/cc/bridgit/app/interact/CardActions.kt"
  ],
  "start": {
    "fits": "Spot and count a common picture situation on the camera: a pot boiling over, smoke, water on the floor, the dog licking or scratching itself, the dog on the couch, the cat on the counter, sunlight in the room, lights left on, or one described in the owner's words.",
    "ideas": [
      "situation",
      "situations",
      "boil",
      "boiling",
      "pot",
      "stove",
      "smoke",
      "smoky",
      "lick",
      "licking",
      "licks",
      "itself",
      "scratch",
      "scratching",
      "itch",
      "couch",
      "sofa",
      "counter",
      "sunlight",
      "sunny",
      "sun",
      "lights",
      "puddle",
      "spill",
      "leak",
      "often",
      "spot",
      "spots",
      "episodes"
    ],
    "owner_turns_on": [
      "Watches",
      "Camera",
      "Interactions"
    ],
    "you_decide": {
      "question": "Which situation should the camera count: a library id (GET /v1/situations) or the owner's own words?",
      "default": "pot_boiling_over with the back camera every 20 seconds (the start example); own words go in situation as a sentence with spaces."
    },
    "first_calls": [
      "GET /v1/capabilities",
      "GET /v1/limits",
      "GET /v1/situations",
      "POST /v1/watches with examples[start] (situation set to the owner's pick); save watch_id",
      "POST /v1/interactions with card_request from recipe_url, resource_id set to watch_id"
    ],
    "adapt": "Set situation to the library id or the owner's words; leave question, questions and tally out (the phone fills them in). Alert situations (pot_boiling_over, smoke_in_room, water_on_floor) also send the owner an urgent notice when an episode starts; {\"id\": ..., \"alert\": false} turns that off. Set max_paid_per_hour from GET /v1/limits and use the library's interval_s for slow situations (sunlight, lights: 300).",
    "done_when": [
      "POST /v1/watches returned a watch_id",
      "GET /v1/watches/{id}/tally shows the first checks",
      "The card is on the phone and Refresh shows today's count",
      "The owner knows which situation is counted, whether it alerts, and how to Pause or Stop"
    ],
    "examples": [
      "start",
      "own-words"
    ]
  },
  "url": "https://bridgit.cc/recipes/situation-counter/",
  "card_url": "https://bridgit.cc/recipes/situation-counter/card.html",
  "card_request": {
    "method": "POST",
    "path": "/v1/interactions",
    "body": {
      "kind": "html",
      "title": "Situation counter",
      "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 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\":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||{};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",
      "no_takeover": true,
      "expires_in_seconds": 86400
    }
  },
  "operations": [
    {
      "method": "POST",
      "path": "/v1/watches",
      "documentation": "https://bridgit.cc/learn/operations/post-v1-watches.json"
    },
    {
      "method": "DELETE",
      "path": "/v1/watches/{id}",
      "documentation": "https://bridgit.cc/learn/operations/delete-v1-watches-id.json"
    },
    {
      "method": "POST",
      "path": "/v1/interactions",
      "documentation": "https://bridgit.cc/learn/operations/post-v1-interactions.json"
    },
    {
      "method": "GET",
      "path": "/v1/watches/{id}/tally",
      "documentation": "https://bridgit.cc/learn/operations/get-v1-watches-id-tally.json"
    },
    {
      "method": "POST",
      "path": "/v1/watches/{id}/pause",
      "documentation": "https://bridgit.cc/learn/operations/post-v1-watches-id-pause.json"
    }
  ],
  "package": {
    "url": "https://bridgit.cc/recipes/situation-counter/applet.json",
    "version": "1.0.0",
    "sha256": "42e91db55cd0216fcb7d02e1404e783e327e8ac4dfe41f61580778da325cce5a",
    "import_url": "https://bridgit.cc/install/?src=%2Frecipes%2Fsituation-counter%2Fapplet.json&sha256=42e91db55cd0216fcb7d02e1404e783e327e8ac4dfe41f61580778da325cce5a",
    "app_link": "bridgit://install?src=%2Frecipes%2Fsituation-counter%2Fapplet.json&sha256=42e91db55cd0216fcb7d02e1404e783e327e8ac4dfe41f61580778da325cce5a",
    "install_request": {
      "method": "POST",
      "path": "/v1/applets/install",
      "body": {
        "url": "https://bridgit.cc/recipes/situation-counter/applet.json"
      }
    }
  }
}
