0
TOOLS#51d457c2
Site Log — a field tracker you talk to
@Owner·deposited 3w ago·updated 3w ago·163 views
TOOLS#51d457c2
Site Log — a field tracker you talk to
OW
@Owner
163Views
0Comments
0Forks
0Saves
SHARE · REMIX
Site Log — a field tracker you talk to — a HTML tools widget by @Owner.
CONTROLS
trackerproductivityfield-workclaudevoice
No comments yet. Be the first!
✦ Remix with AI
SDK in this widget
vibes.onReadyvibes.savevibes.load
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "Site Log — a field tracker you talk to" by @Owner]
Source: https://itjustvibes.com/Owner/site-log-a-field-tracker-you-talk-to
Type: HTML
--- SOURCE CODE ---
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover">
<title>Site Log</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>:root{--bg:#0e1116;--surface:#161b22;--surface2:#1d242d;--line:#2a323c;--text:#e6edf3;--dim:#8b97a6;--faint:#5b6673;--hi:#ffb22e;--hi-dim:#7a5616;--info:#2dd4bf;--open:#ffb22e;--done:#3fb950;--waiting:#58a6ff;--backburner:#7c8794;--danger:#f85149;--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--disp:"Space Grotesk",system-ui,sans-serif;--body:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;--r:10px}*{box-sizing:border-box}html,body{margin:0;padding:0}body{background:var(--bg);color:var(--text);font-family:var(--body);font-size:15px;line-height:1.45;-webkit-font-smoothing:antialiased;padding-bottom:env(safe-area-inset-bottom)}button{font-family:inherit;cursor:pointer;color:inherit;background:none;border:none}input,textarea,select{font-family:inherit;font-size:15px}::placeholder{color:var(--faint)}.wrap{max-width:760px;margin:0 auto;padding:0 14px 90px}.strip{position:sticky;top:0;z-index:30;background:var(--bg);display:flex;align-items:center;gap:10px;padding:14px 0 10px;border-bottom:1px solid var(--line);box-shadow:0 6px 18px -16px #000}.brand{font-family:var(--disp);font-weight:700;letter-spacing:.14em;font-size:13px;text-transform:uppercase}.brand b{color:var(--hi)}.sync{margin-left:auto;display:flex;align-items:center;gap:7px;font-size:11px;color:var(--dim)}.dot{width:8px;height:8px;border-radius:50%;background:var(--faint)}.dot.on{background:var(--done);box-shadow:0 0 0 3px #3fb95022}.sync .saving{color:var(--hi)}.capwrap{padding:14px 0 6px}.cap{display:flex;align-items:stretch;gap:0;border:1px solid var(--line);border-radius:var(--r);background:var(--surface);overflow:hidden;transition:border-color .15s}.cap:focus-within{border-color:var(--hi)}.cap .lead{display:flex;align-items:center;padding:0 0 0 14px;color:var(--hi);font-family:var(--mono);font-size:13px;font-weight:600;user-select:none}.cap input{flex:1;background:none;border:none;outline:none;color:var(--text);padding:14px 12px}.cap .go{background:var(--hi);color:#1a1205;font-weight:700;padding:0 16px;font-family:var(--disp);letter-spacing:.03em;transition:filter .12s}.cap .go:hover{filter:brightness(1.08)}.cap .go:active{filter:brightness(.94)}.hint{font-size:11.5px;color:var(--faint);padding:7px 4px 2px;line-height:1.5}.hint code{font-family:var(--mono);color:var(--dim);background:var(--surface2);padding:1px 5px;border-radius:5px}.tabs{display:flex;gap:6px;margin:14px 0 4px}.tab{font-family:var(--disp);font-weight:600;letter-spacing:.06em;font-size:12.5px;text-transform:uppercase;padding:8px 14px;border-radius:8px;color:var(--dim);border:1px solid transparent}.tab.active{color:var(--text);background:var(--surface2);border-color:var(--line)}.tab .n{font-family:var(--mono);font-size:11px;color:var(--faint);margin-left:6px}.tab.active .n{color:var(--hi)}.tab.book.active .n{color:var(--info)}.spacer{flex:1}.util{display:flex;gap:4px;align-items:center}.iconbtn{color:var(--dim);padding:7px 9px;border-radius:8px;font-size:12px;border:1px solid var(--line)}.iconbtn:hover{color:var(--text);border-color:var(--faint)}.filters{display:flex;flex-wrap:wrap;gap:6px;padding:10px 0 4px}.pillrow{display:flex;flex-wrap:wrap;gap:6px;align-items:center;width:100%}.pgroup{font-family:var(--mono);font-size:10px;color:var(--faint);text-transform:uppercase;letter-spacing:.1em;margin-right:2px;align-self:center}.pill{font-size:12.5px;padding:5px 11px;border-radius:999px;border:1px solid var(--line);color:var(--dim);background:var(--surface);white-space:nowrap;transition:.12s}.pill:hover{border-color:var(--faint);color:var(--text)}.pill.on{background:var(--hi);color:#1a1205;border-color:var(--hi);font-weight:600}.pill.who.on{background:var(--info);color:#04201c;border-color:var(--info)}.search{flex:1;min-width:140px}.search input{width:100%;background:var(--surface);border:1px solid var(--line);border-radius:999px;color:var(--text);padding:7px 13px;outline:none}.search input:focus{border-color:var(--faint)}.prep{margin:12px 0 2px;border:1px solid var(--hi-dim);background:linear-gradient(180deg,#241b09,#1a1408);border-radius:var(--r);padding:12px 14px;font-size:13.5px}.prep b{color:var(--hi)}.prep .clr{float:right;color:var(--dim);font-size:12px;border:1px solid var(--line);border-radius:7px;padding:3px 9px}.prep .clr:hover{color:var(--text)}.group{margin-top:18px}.ghead{display:flex;align-items:center;gap:8px;font-family:var(--disp);font-weight:600;font-size:13px;color:var(--dim);margin:0 2px 8px}.ghead .gline{flex:1;height:1px;background:var(--line)}.ghead .gn{font-family:var(--mono);font-size:11px;color:var(--faint)}.item{position:relative;display:flex;gap:0;background:var(--surface);border:1px solid var(--line);border-radius:var(--r);margin-bottom:8px;overflow:hidden}.item .spine{width:5px;flex:none;background:var(--open)}.item.s-done .spine{background:var(--done)}.item.s-waiting .spine{background:var(--waiting)}.item.s-backburner .spine{background:var(--backburner)}.item .body{flex:1;padding:11px 12px 11px 13px;min-width:0}.item .top{display:flex;align-items:flex-start;gap:10px}.check{flex:none;width:22px;height:22px;border-radius:6px;border:1.5px solid var(--faint);margin-top:1px;display:flex;align-items:center;justify-content:center;color:transparent;transition:.12s}.check:hover{border-color:var(--done)}.item.s-done .check{background:var(--done);border-color:var(--done);color:#072b12}.item.s-done .txt{color:var(--dim);text-decoration:line-through;text-decoration-color:var(--faint)}.txt{flex:1;min-width:0;word-break:break-word}.typ{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--hi);margin-right:7px}.tags{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}.tg{font-size:11px;color:var(--dim);background:var(--surface2);border:1px solid var(--line);border-radius:6px;padding:2px 7px;font-family:var(--mono)}.tg.where:before{content:"@ ";color:var(--faint)}.tg.who{color:var(--info)}.tg.who:before{content:"+ ";color:var(--faint)}.tg.proj:before{content:"# ";color:var(--faint)}.tg.due{color:var(--text);border-color:var(--hi-dim)}.tg.due.over{color:var(--danger);border-color:var(--danger)}.tg.st{text-transform:capitalize}.rowbtns{display:flex;gap:2px;margin-left:6px;flex:none}.mini{color:var(--faint);padding:5px 7px;border-radius:6px;font-size:14px;line-height:1}.mini:hover{color:var(--text);background:var(--surface2)}.note-line{margin-top:7px;font-size:12.5px;color:var(--dim);white-space:pre-wrap}.bk{background:var(--surface);border:1px solid var(--line);border-left:3px solid var(--info);border-radius:var(--r);padding:12px 14px;margin-bottom:8px}.bk h4{margin:0;font-family:var(--disp);font-size:15px;font-weight:600}.bk .meta{display:flex;flex-wrap:wrap;gap:5px;margin:8px 0 0}.bk .body-txt{margin-top:9px;font-size:13.5px;color:var(--dim);white-space:pre-wrap;display:none}.bk.open .body-txt{display:block}.bk .more{font-size:11.5px;color:var(--info);font-family:var(--mono);margin-top:8px;cursor:pointer}.empty{text-align:center;padding:44px 18px;color:var(--dim)}.empty .big{font-family:var(--disp);font-size:16px;color:var(--text);margin-bottom:6px}.empty .tape{height:14px;margin:0 auto 20px;max-width:220px;border-radius:3px;background:repeating-linear-gradient(-45deg,#ffb22e 0 14px,#1a1408 14px 28px);opacity:.5}.scrim{position:fixed;inset:0;background:#000a;z-index:50;display:none;align-items:flex-end;justify-content:center}.scrim.show{display:flex}.sheet{background:var(--surface);border:1px solid var(--line);border-bottom:none;border-radius:14px 14px 0 0;width:100%;max-width:620px;max-height:88vh;overflow:auto;padding:18px 18px calc(22px + env(safe-area-inset-bottom))}@media(min-width:620px){.scrim{align-items:center}.sheet{border-radius:14px;border-bottom:1px solid var(--line);max-height:84vh}}.sheet h3{margin:0 0 2px;font-family:var(--disp);font-size:17px;display:flex;align-items:center;gap:8px}.sheet h3 .x{margin-left:auto;color:var(--dim);font-size:22px;line-height:1;padding:2px 6px}.sheet p.sub{color:var(--dim);font-size:13px;margin:0 0 14px}.field{margin-bottom:12px}.field label{display:block;font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:var(--faint);font-family:var(--mono);margin-bottom:5px}.field input,.field textarea,.field select{width:100%;background:var(--surface2);border:1px solid var(--line);border-radius:8px;color:var(--text);padding:10px 11px;outline:none}.field input:focus,.field textarea:focus,.field select:focus{border-color:var(--hi)}.field textarea{resize:vertical;min-height:64px;line-height:1.5}.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}.seg{display:flex;gap:5px;flex-wrap:wrap}.seg button{font-size:12px;padding:7px 12px;border:1px solid var(--line);border-radius:8px;color:var(--dim);font-family:var(--mono)}.seg button.on{background:var(--hi);color:#1a1205;border-color:var(--hi);font-weight:600}.actions{display:flex;gap:8px;margin-top:6px}.btn{flex:1;text-align:center;padding:12px;border-radius:9px;font-family:var(--disp);font-weight:600;letter-spacing:.02em}.btn.primary{background:var(--hi);color:#1a1205}.btn.primary:hover{filter:brightness(1.07)}.btn.ghost{background:var(--surface2);color:var(--text);border:1px solid var(--line)}.btn.del{background:none;color:var(--danger);border:1px solid #f8514944;flex:none;padding:12px 14px}.cc h4{font-family:var(--disp);margin:16px 0 6px;font-size:13.5px;color:var(--hi)}.cc h4.t2{color:var(--info)}.cc p{font-size:13px;color:var(--dim);margin:6px 0}.cc pre{background:var(--bg);border:1px solid var(--line);border-radius:9px;padding:11px 12px;overflow:auto;font-family:var(--mono);font-size:11.5px;color:var(--text);white-space:pre-wrap;line-height:1.55}.cc .copy{font-size:11px;font-family:var(--mono);color:var(--hi);border:1px solid var(--hi-dim);border-radius:6px;padding:4px 9px;margin-top:6px}.cc .copy:hover{background:#ffb22e14}.cc ol{margin:6px 0;padding-left:18px;font-size:13px;color:var(--dim)}.cc ol li{margin:5px 0}.cc code{font-family:var(--mono);background:var(--surface2);padding:1px 5px;border-radius:5px;color:var(--text);font-size:12px}.toast{position:fixed;left:50%;bottom:26px;transform:translate(-50%) translateY(20px);background:var(--surface2);border:1px solid var(--line);color:var(--text);padding:10px 16px;border-radius:10px;font-size:13px;z-index:80;opacity:0;pointer-events:none;transition:.22s;box-shadow:0 12px 30px -10px #000}.toast.show{opacity:1;transform:translate(-50%) translateY(0)}@media(prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}:focus-visible{outline:2px solid var(--hi);outline-offset:2px;border-radius:6px}
</style>
</head>
<body>
<div class="wrap">
<div class="strip">
<div class="brand">SITE <b>LOG</b></div>
<div class="sync"><span class="dot" id="dot"></span><span id="syncTxt">loading…</span></div>
</div>
<div class="capwrap">
<div class="cap">
<span class="lead">></span>
<input id="cap" autocomplete="off" placeholder="Log it — e.g. photo of junction box @SiteB #ProjA +John due fri">
<button class="go" id="capGo">Log</button>
</div>
<div class="hint">
<code>@place</code> location · <code>#project</code> · <code>+person</code> · <code>due fri</code> / <code>due 12/07</code> · start with <code>?</code> for a question, <code>info</code>/<code>chase</code> to type it
</div>
</div>
<div class="tabs">
<button class="tab active" id="tabLog" data-view="log">Log <span class="n" id="nLog">0</span></button>
<button class="tab book" id="tabBook" data-view="book">Book <span class="n" id="nBook">0</span></button>
<span class="spacer"></span>
<div class="util">
<button class="iconbtn" id="btnConnect">⚡ Claude</button>
<button class="iconbtn" id="btnData">⋯</button>
</div>
</div>
<div id="filters" class="filters"></div>
<div id="prep"></div>
<div id="list"></div>
</div>
<div class="scrim" id="editScrim">
<div class="sheet" id="editSheet"></div>
</div>
<div class="scrim" id="ccScrim">
<div class="sheet cc" id="ccSheet"></div>
</div>
<div class="scrim" id="dataScrim">
<div class="sheet" id="dataSheet"></div>
</div>
<div class="toast" id="toast"></div>
<script>
"use strict";const KEY="tracker",TYPES=["action","photo","chase","info","question"],STATUSES=["open","waiting","backburner","done"];let DB={v:1,updated:null,projects:[],items:[],book:[]},view="log",F={project:null,who:null,where:null,status:null,q:""},saveT=null,dirty=!1,lastSeen=null;const $=(e,t=document)=>t.querySelector(e),$$=(e,t=document)=>[...t.querySelectorAll(e)],el=(e,t,o)=>{const s=document.createElement(e);return t&&(s.className=t),o!=null&&(s.innerHTML=o),s},esc=e=>(e==null?"":String(e)).replace(/[&<>"]/g,t=>({"&":"&","<":"<",">":">",'"':"""})[t]),uid=e=>e+Math.random().toString(36).slice(2,8)+Date.now().toString(36).slice(-3),nowISO=()=>new Date().toISOString();function toast(e){const t=$("#toast");t.textContent=e,t.classList.add("show"),clearTimeout(t._z),t._z=setTimeout(()=>t.classList.remove("show"),1800)}async function loadDB(){try{const e=await vibes.load(KEY);e&&typeof e=="object"&&(DB=Object.assign({v:1,projects:[],items:[],book:[]},e)),DB.items||(DB.items=[]),DB.book||(DB.book=[]),DB.projects||(DB.projects=[]),lastSeen=DB.updated,setSync("synced")}catch(e){setSync("offline"),console.error(e)}}function queueSave(){dirty=!0,setSync("saving"),clearTimeout(saveT),saveT=setTimeout(commit,500)}async function commit(){if(dirty){dirty=!1,DB.updated=nowISO();try{await vibes.save(KEY,DB),lastSeen=DB.updated,setSync("synced")}catch(e){dirty=!0,setSync("offline"),toast("Save failed \u2014 will retry"),console.error(e),clearTimeout(saveT),saveT=setTimeout(commit,2500)}}}function setSync(e){const t=$("#dot"),o=$("#syncTxt");t.classList.toggle("on",e==="synced"),e==="saving"?(o.textContent="saving\u2026",o.className="saving"):e==="offline"?(o.textContent="offline",o.className=""):(o.textContent=DB.updated?"synced "+rel(DB.updated):"synced",o.className="")}function parseDue(e){if(!e)return null;const t=e.trim().toLowerCase(),o=new Date;o.setHours(0,0,0,0);const s=["sun","mon","tue","wed","thu","fri","sat"];if(t==="today")return iso(o);if(t==="tomorrow"||t==="tmrw")return o.setDate(o.getDate()+1),iso(o);if(t==="next week"||t==="nextweek")return o.setDate(o.getDate()+7),iso(o);const i=s.findIndex(a=>t.startsWith(a));if(i>=0){let a=(i-o.getDay()+7)%7;return a===0&&(a=7),o.setDate(o.getDate()+a),iso(o)}let n=t.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);if(n)return iso(new Date(+n[1],+n[2]-1,+n[3]));if(n=t.match(/^(\d{1,2})[\/.](\d{1,2})(?:[\/.](\d{2,4}))?$/),n){let a=n[3]?+n[3]:o.getFullYear();return a<100&&(a+=2e3),iso(new Date(a,+n[2]-1,+n[1]))}return null}const iso=e=>isNaN(e)?null:e.toISOString().slice(0,10);function rel(e){const t=new Date(e),o=new Date,s=o-t,i=Math.round(s/6e4);if(i<1)return"just now";if(i<60)return i+"m ago";const n=Math.round(i/60);if(n<24)return n+"h ago";const a=Math.round(n/24);return a<7?a+"d ago":t.toLocaleDateString()}function dueLabel(e){if(!e)return null;const t=new Date(e+"T00:00:00"),o=new Date;o.setHours(0,0,0,0);const s=Math.round((t-o)/864e5);let i;return s<0?i="overdue "+-s+"d":s===0?i="due today":s===1?i="due tmrw":s<7?i="due "+t.toLocaleDateString(void 0,{weekday:"short"}):i="due "+t.toLocaleDateString(void 0,{day:"numeric",month:"short"}),{label:i,over:s<0}}function parseCapture(e){let t=" "+e.trim()+" ",o=null,s=null,i=null,n=null,a="action",l=t.match(/\sdue:?\s+([a-z0-9\/.\-]+(?:\s+week)?)/i);if(l){const c=parseDue(l[1]);c&&(n=c,t=t.replace(l[0]," "))}t=t.replace(/\s\+("?)([^"@#]+?)\1(?=\s)/,(c,p,d)=>(o=d.trim()," ")),t=t.replace(/\s@("?)([^"+#]+?)\1(?=\s)/,(c,p,d)=>(s=d.trim()," ")),t=t.replace(/\s#("?)([^"+@]+?)\1(?=\s)/,(c,p,d)=>(i=d.trim()," "));let r=t.trim();if(/^\?/.test(r))a="question",r=r.replace(/^\?\s*/,"");else{const c=r.match(/^(photo|action|chase|info|question)\s*[:\-]\s*/i);c&&(a=c[1].toLowerCase(),r=r.slice(c[0].length))}return a==="action"&&(/\b(photo|picture|pic|snap)\b/i.test(r)?a="photo":/\b(chase|chasing|follow[ -]?up)\b/i.test(r)&&(a="chase")),{text:r.trim(),who:o,where:s,project:i,due:n,type:a}}function addItem(e){const t={id:uid("i_"),text:e.text||"(untitled)",type:e.type||"action",status:"open",project:e.project||null,who:e.who||null,where:e.where||null,due:e.due||null,notes:e.notes||"",tags:e.tags||[],created:nowISO(),updated:nowISO()};return DB.items.unshift(t),t.project&&!DB.projects.includes(t.project)&&DB.projects.push(t.project),queueSave(),t}function cycleStatus(e){e.status=e.status==="done"?"open":"done",e.updated=nowISO(),queueSave()}function setStatus(e,t){e.status=t,e.updated=nowISO(),queueSave()}function delItem(e){DB.items=DB.items.filter(t=>t.id!==e),queueSave()}function delBook(e){DB.book=DB.book.filter(t=>t.id!==e),queueSave()}function distinct(e){const t=new Set;return DB.items.forEach(o=>{o[e]&&t.add(o[e])}),[...t].sort((o,s)=>o.localeCompare(s))}function render(){$("#nLog").textContent=DB.items.filter(e=>e.status!=="done").length,$("#nBook").textContent=DB.book.length,$("#tabLog").classList.toggle("active",view==="log"),$("#tabBook").classList.toggle("active",view==="book"),renderFilters(),view==="log"?renderLog():renderBook(),setSync(dirty?"saving":(DB.updated,"synced"))}function renderFilters(){const e=$("#filters");if(e.innerHTML="",view==="book"){e.style.display="none";return}e.style.display="";const t=el("div","pillrow");t.appendChild(el("span","pgroup","show")),[["open","Open"],["waiting","Waiting"],["backburner","Parked"],["done","Done"]].forEach(([l,r])=>{const c=el("button","pill"+(F.status===l?" on":""),esc(r));c.onclick=()=>{F.status=F.status===l?null:l,render()},t.appendChild(c)});const o=el("div","search"),s=el("input");s.placeholder="search\u2026",s.value=F.q,s.oninput=()=>{F.q=s.value,renderLog()},o.appendChild(s),t.appendChild(o),e.appendChild(t);const i=distinct("who"),n=distinct("where"),a=[...new Set([...DB.projects,...distinct("project")])].sort();if(n.length){const l=el("div","pillrow");l.appendChild(el("span","pgroup","where")),n.forEach(r=>{const c=el("button","pill"+(F.where===r?" on":""),esc(r));c.onclick=()=>{F.where=F.where===r?null:r,render()},l.appendChild(c)}),e.appendChild(l)}if(i.length){const l=el("div","pillrow");l.appendChild(el("span","pgroup","who")),i.forEach(r=>{const c=el("button","pill who"+(F.who===r?" on":""),esc(r));c.onclick=()=>{F.who=F.who===r?null:r,render()},l.appendChild(c)}),e.appendChild(l)}if(a.length){const l=el("div","pillrow");l.appendChild(el("span","pgroup","proj")),a.forEach(r=>{const c=el("button","pill"+(F.project===r?" on":""),esc(r));c.onclick=()=>{F.project=F.project===r?null:r,render()},l.appendChild(c)}),e.appendChild(l)}}function matchItem(e){if(F.status&&e.status!==F.status||!F.status&&e.status==="done"||F.where&&e.where!==F.where||F.who&&e.who!==F.who||F.project&&e.project!==F.project)return!1;if(F.q){const t=F.q.toLowerCase();if(!((e.text||"")+" "+(e.notes||"")+" "+(e.who||"")+" "+(e.where||"")+" "+(e.project||"")).toLowerCase().includes(t))return!1}return!0}function renderPrep(e){const t=$("#prep");if(!(F.where||F.who)){t.innerHTML="";return}const o=[];F.who&&o.push("<b>"+esc(F.who)+"</b>"),F.where&&o.push("@ <b>"+esc(F.where)+"</b>");const s=e.filter(n=>n.status!=="done").length,i=DB.book.filter(n=>F.who&&n.who===F.who||F.where&&n.where===F.where||(n.tags||[]).some(a=>a===F.who||a===F.where)).length;t.innerHTML='<button class="clr" id="clrPrep">clear</button>Covering '+o.join(" ")+" \u2014 "+s+" open"+(i?" \xB7 "+i+" in Book":"")+".",$("#clrPrep").onclick=()=>{F.who=null,F.where=null,render()}}function renderLog(){const e=$("#list");e.innerHTML="";let t=DB.items.filter(matchItem);if(renderPrep(DB.items.filter(n=>(!F.where||n.where===F.where)&&(!F.who||n.who===F.who)&&(!F.project||n.project===F.project))),!t.length){e.appendChild(emptyState());return}const o=F.project?"where":"project",s={};t.forEach(n=>{const a=n[o]||"\u2014";(s[a]=s[a]||[]).push(n)}),Object.keys(s).sort((n,a)=>(n==="\u2014")-(a==="\u2014")||n.localeCompare(a)).forEach(n=>{const a=el("div","group");a.appendChild(el("div","ghead",esc(n==="\u2014"?o==="project"?"No project":"No location":n)+'<span class="gline"></span><span class="gn">'+s[n].length+"</span>")),s[n].sort((l,r)=>dueRank(l)-dueRank(r)),s[n].forEach(l=>a.appendChild(itemEl(l))),e.appendChild(a)})}function dueRank(e){return e.status==="backburner"?9e15:e.due?new Date(e.due).getTime():8e15}function itemEl(e){const t=el("div","item s-"+e.status);t.appendChild(el("div","spine"));const o=el("div","body"),s=el("div","top"),i=el("button","check","\u2713");i.setAttribute("aria-label","toggle done"),i.onclick=()=>{cycleStatus(e),render()};const n=el("div","txt");n.innerHTML='<span class="typ">'+esc(e.type)+"</span>"+esc(e.text);const a=el("div","tags");e.where&&a.appendChild(el("span","tg where",esc(e.where))),e.who&&a.appendChild(el("span","tg who",esc(e.who))),e.project&&a.appendChild(el("span","tg proj",esc(e.project))),(e.tags||[]).forEach(p=>a.appendChild(el("span","tg",esc(p))));const l=dueLabel(e.due);l&&e.status!=="done"&&a.appendChild(el("span","tg due"+(l.over?" over":""),esc(l.label))),e.status==="waiting"&&a.appendChild(el("span","tg st","waiting")),e.status==="backburner"&&a.appendChild(el("span","tg st","parked")),a.children.length&&n.appendChild(a),e.notes&&n.appendChild(el("div","note-line",esc(e.notes)));const r=el("div","rowbtns"),c=el("button","mini","\u270E");return c.setAttribute("aria-label","edit"),c.onclick=()=>openEdit(e),r.appendChild(c),s.appendChild(i),s.appendChild(n),s.appendChild(r),o.appendChild(s),t.appendChild(o),t}function emptyState(){const e=el("div","empty"),t=F.where?" for @"+esc(F.where):F.who?" with +"+esc(F.who):F.project?" on #"+esc(F.project):"";return e.innerHTML='<div class="tape"></div><div class="big">Nothing'+(F.status?" "+esc(F.status):" open")+t+"</div><div>Capture something in the bar above, or say it to Claude on your phone.</div>",e}function renderBook(){const e=$("#list");e.innerHTML="";const t=el("div"),o=el("button","iconbtn","+ New note");o.style.margin="8px 0 4px",o.onclick=()=>openBook(null);const s=el("div","search");s.style.margin="4px 0 8px";const i=el("input");i.placeholder="search the book \u2014 meetings, contracts\u2026",i.value=F.q,i.oninput=()=>{F.q=i.value,renderBook()},s.appendChild(i),t.appendChild(o),t.appendChild(s),e.appendChild(t);let n=DB.book.slice();if(F.q){const a=F.q.toLowerCase();n=n.filter(l=>((l.title||"")+" "+(l.body||"")+" "+(l.tags||[]).join(" ")+" "+(l.who||"")+" "+(l.where||"")).toLowerCase().includes(a))}if(n.sort((a,l)=>(l.updated||"").localeCompare(a.updated||"")),!n.length){const a=el("div","empty");a.innerHTML='<div class="big">The book is empty</div><div>Park meeting notes, contract clauses, equipment details here \u2014 then ask Claude to read them back later.</div>',e.appendChild(a);return}n.forEach(a=>e.appendChild(bookEl(a)))}function bookEl(e){const t=el("div","bk");t.appendChild(el("h4",null,esc(e.title||"(untitled)")));const o=el("div","meta");if(e.where&&o.appendChild(el("span","tg where",esc(e.where))),e.who&&o.appendChild(el("span","tg who",esc(e.who))),e.project&&o.appendChild(el("span","tg proj",esc(e.project))),(e.tags||[]).forEach(n=>o.appendChild(el("span","tg",esc(n)))),o.children.length&&t.appendChild(o),e.body){t.appendChild(el("div","body-txt",esc(e.body)));const n=el("div","more","\u25B8 show details");n.onclick=()=>{const a=t.classList.toggle("open");n.textContent=a?"\u25BE hide":"\u25B8 show details"},t.appendChild(n)}const s=el("div");s.style.marginTop="8px";const i=el("button","mini","\u270E edit");return i.style.fontSize="12px",i.onclick=()=>openBook(e),s.appendChild(i),t.appendChild(s),t}function openSheet(e){$(e).classList.add("show")}function closeSheets(){$$(".scrim").forEach(e=>e.classList.remove("show"))}function openEdit(e){const t=$("#editSheet"),o=(i,n,a)=>'<div class="seg" data-seg="'+i+'">'+n.map(l=>'<button data-v="'+l+'" class="'+(l===a?"on":"")+'">'+l+"</button>").join("")+"</div>";t.innerHTML='<h3>Edit item<button class="x" data-close>\xD7</button></h3><div class="field"><label>What</label><textarea id="e_text">'+esc(e.text)+'</textarea></div><div class="field"><label>Type</label>'+o("type",TYPES,e.type)+'</div><div class="field"><label>Status</label>'+o("status",STATUSES,e.status)+'</div><div class="grid2"><div class="field"><label>Where</label><input id="e_where" value="'+esc(e.where||"")+'"></div><div class="field"><label>Who</label><input id="e_who" value="'+esc(e.who||"")+'"></div><div class="field"><label>Project</label><input id="e_project" value="'+esc(e.project||"")+'"></div><div class="field"><label>Due (e.g. fri, 12/07)</label><input id="e_due" value="'+esc(e.due||"")+'"></div></div><div class="field"><label>Tags (comma sep)</label><input id="e_tags" value="'+esc((e.tags||[]).join(", "))+'"></div><div class="field"><label>Notes</label><textarea id="e_notes">'+esc(e.notes||"")+'</textarea></div><div class="actions"><button class="btn del" id="e_del">Delete</button><button class="btn ghost" data-close>Cancel</button><button class="btn primary" id="e_save">Save</button></div>';let s={type:e.type,status:e.status};$$(".seg",t).forEach(i=>i.querySelectorAll("button").forEach(n=>n.onclick=()=>{s[i.dataset.seg]=n.dataset.v,i.querySelectorAll("button").forEach(a=>a.classList.remove("on")),n.classList.add("on")})),$("#e_save",t).onclick=()=>{e.text=$("#e_text",t).value.trim()||e.text,e.type=s.type,e.status=s.status,e.where=$("#e_where",t).value.trim()||null,e.who=$("#e_who",t).value.trim()||null,e.project=$("#e_project",t).value.trim()||null;const i=$("#e_due",t).value.trim();e.due=i?parseDue(i)||(/^\d{4}-\d\d-\d\d$/.test(i)?i:e.due):null,e.notes=$("#e_notes",t).value.trim(),e.updated=nowISO(),e.tags=$("#e_tags",t).value.split(",").map(n=>n.trim()).filter(Boolean),e.project&&!DB.projects.includes(e.project)&&DB.projects.push(e.project),queueSave(),closeSheets(),render()},$("#e_del",t).onclick=()=>{confirm("Delete this item?")&&(delItem(e.id),closeSheets(),render())},openSheet("#editScrim")}function openBook(e){const t=!e;e=e||{id:uid("n_"),title:"",tags:[],who:"",where:"",project:"",body:""};const o=$("#editSheet");o.innerHTML="<h3>"+(t?"New note":"Edit note")+'<button class="x" data-close>\xD7</button></h3><p class="sub">Meeting notes, contract clauses, equipment details \u2014 anything to recall later.</p><div class="field"><label>Title</label><input id="b_title" value="'+esc(e.title||"")+'" placeholder="e.g. ProjA \u2014 handover meeting 24 Jun"></div><div class="grid2"><div class="field"><label>Where</label><input id="b_where" value="'+esc(e.where||"")+'"></div><div class="field"><label>Who</label><input id="b_who" value="'+esc(e.who||"")+'"></div><div class="field"><label>Project</label><input id="b_project" value="'+esc(e.project||"")+'"></div><div class="field"><label>Tags (comma sep)</label><input id="b_tags" value="'+esc((e.tags||[]).join(", "))+'"></div></div><div class="field"><label>Details</label><textarea id="b_body" style="min-height:140px">'+esc(e.body||"")+'</textarea></div><div class="actions">'+(t?"":'<button class="btn del" id="b_del">Delete</button>')+'<button class="btn ghost" data-close>Cancel</button><button class="btn primary" id="b_save">Save</button></div>',$("#b_save",o).onclick=()=>{e.title=$("#b_title",o).value.trim()||"(untitled)",e.where=$("#b_where",o).value.trim()||null,e.who=$("#b_who",o).value.trim()||null,e.project=$("#b_project",o).value.trim()||null,e.tags=$("#b_tags",o).value.split(",").map(s=>s.trim()).filter(Boolean),e.body=$("#b_body",o).value.trim(),e.updated=nowISO(),t&&(e.created=nowISO(),DB.book.unshift(e)),queueSave(),closeSheets(),render()},t||($("#b_del",o).onclick=()=>{confirm("Delete this note?")&&(delBook(e.id),closeSheets(),render())}),openSheet("#editScrim")}function openConnect(){const e=$("#ccSheet"),t=`Read my Site Log. Use the it-just-vibes bridge tool vibes_state_get on this widget,
key "ai/tracker". It is JSON: { items:[...], book:[...] }. I'm heading to a site \u2014
tell me everything open for a given place or person.`,o=`Update my Site Log via the it-just-vibes bridge. First vibes_state_get key "ai/tracker"
to get the JSON, then make my change, then vibes_state_set the whole object back.
Rules: items[] entries have {id,text,type,status,project,who,where,due,notes,tags,
created,updated}. type=action|photo|chase|info|question, status=open|waiting|
backburner|done, due is YYYY-MM-DD. Set new id like "i_"+random, set updated=now.
Long-form meeting/contract notes go in book[] as {id,title,tags,who,where,
project,body,created,updated}. Don't drop existing entries.`;e.innerHTML=`<h3>\u26A1 Drive this from Claude<button class="x" data-close>\xD7</button></h3><p class="sub">Two ways, depending on what your account has switched on. Both read & write the same live state.</p><h4>A \xB7 The bridge (talk to it, hands-free)</h4><p>Open this widget's <code>Manage \u2192 Agent</code> tab, turn on agent access, set it to <b>Read-write</b>, and make sure the allowed keys include <code>ai/tracker</code> (the agent namespace maps to the widget's own data). Then in the Claude app (phone or laptop), with the It Just Vibes connector on, just talk:</p><p>\u201C<i>I'm driving to Site B to see John \u2014 what's open?</i>\u201D \u2026 \u201C<i>Photo done. Add: chase the steel cert next Tuesday.</i>\u201D</p><p>Paste these once so Claude knows the shape:</p><pre id="cc_read">`+esc(t)+'</pre><button class="copy" data-copy="cc_read">copy read prompt</button><pre id="cc_write">'+esc(o)+`</pre><button class="copy" data-copy="cc_write">copy write prompt</button><h4 class="t2">B \xB7 Copy-paste (works anywhere, today)</h4><p>No bridge yet? Use the <code>\u22EF</code> menu to <b>Copy state</b>, paste it into Claude, ask for changes, then <b>Paste state</b> back in. Same loop, fully manual.</p><ol><li>Export the JSON (\u22EF \u2192 Copy state)</li><li>In Claude: \u201Chere's my tracker, mark the Site B photo done and add a chase for John\u201D + paste</li><li>Copy Claude's JSON back \u2192 \u22EF \u2192 Paste state</li></ol>`,$$(".copy",e).forEach(s=>s.onclick=()=>{const i=$("#"+s.dataset.copy).textContent;navigator.clipboard?.writeText(i),toast("Copied")}),openSheet("#ccScrim")}function openData(){const e=$("#dataSheet");e.innerHTML=`<h3>Data<button class="x" data-close>\xD7</button></h3><p class="sub">Your whole Site Log is one JSON object. Move it in and out freely.</p><div class="actions" style="margin-bottom:12px"><button class="btn ghost" id="d_copy">Copy state</button><button class="btn ghost" id="d_csv">Export CSV</button></div><div class="field"><label>Paste state to replace everything</label><textarea id="d_paste" placeholder='{ "items":[...], "book":[...] }' style="min-height:120px;font-family:var(--mono);font-size:12px"></textarea></div><div class="actions"><button class="btn ghost" data-close>Close</button><button class="btn primary" id="d_load">Paste state</button></div>`,$("#d_copy",e).onclick=()=>{navigator.clipboard?.writeText(JSON.stringify(DB,null,2)),toast("State copied")},$("#d_csv",e).onclick=()=>{const t=[["status","type","text","where","who","project","due","notes"]];DB.items.forEach(o=>t.push([o.status,o.type,o.text,o.where||"",o.who||"",o.project||"",o.due||"",o.notes||""]));try{vibes.exportData(t,{filename:"site-log.csv"})}catch{toast("Export unavailable here")}},$("#d_load",e).onclick=()=>{try{const t=JSON.parse($("#d_paste",e).value);if(!t||typeof t!="object")throw 0;DB=Object.assign({v:1,projects:[],items:[],book:[]},t),DB.items||(DB.items=[]),DB.book||(DB.book=[]),DB.projects||(DB.projects=[]),dirty=!0,commit(),closeSheets(),render(),toast("State replaced")}catch{toast("That isn't valid JSON")}},openSheet("#dataScrim")}function capture(){const e=$("#cap"),t=e.value.trim();t&&(addItem(parseCapture(t)),e.value="",render())}function bind(){$("#capGo").onclick=capture,$("#cap").addEventListener("keydown",e=>{e.key==="Enter"&&capture()}),$("#tabLog").onclick=()=>{view="log",F.q="",render()},$("#tabBook").onclick=()=>{view="book",F.q="",render()},$("#btnConnect").onclick=openConnect,$("#btnData").onclick=openData,document.addEventListener("click",e=>{(e.target.matches("[data-close]")||e.target.classList.contains("scrim"))&&closeSheets()}),document.addEventListener("keydown",e=>{e.key==="Escape"&&closeSheets()}),setInterval(pollExternal,15e3)}async function pollExternal(){if(!dirty&&!$$(".scrim.show").length)try{const e=await vibes.load(KEY);e&&e.updated&&e.updated!==lastSeen?(DB=Object.assign({v:1,projects:[],items:[],book:[]},e),DB.items||(DB.items=[]),DB.book||(DB.book=[]),DB.projects||(DB.projects=[]),lastSeen=DB.updated,render(),toast("Updated by Claude")):dirty||setSync("synced")}catch{}}vibes.onReady(async()=>{bind(),await loadDB(),render()});
</script>
</body>
</html>
```
[REQUESTED CHANGES]
(no specific request — apply your best judgment)
--- HOW TO RESPOND (READ FIRST) ---
Before writing any code, follow this exact process:
1. **ANALYZE** the widget source code provided above and identify:
a. Which Vibes SDK features it already uses (vibes.save, vibes.load, vibes.shared.join, etc.)
b. Which SDK features would genuinely benefit THIS specific widget — tailored to what it does, not a dump of everything available.
2. **PRESENT A NUMBERED LIST** covering:
- SDK features currently active in this widget
- New SDK features that would concretely improve this widget (be specific: why this widget, what it enables)
3. **WAIT** — do not write any code yet. Reply with your analysis and numbered list, then stop and ask the user which numbered items they want.
4. **IMPLEMENT ONLY** the items the user confirms, plus any explicit change they requested. Do not add unrequested features.
**IMPORTANT — Shared state room names:**
If you add vibes.shared.join(), do NOT use a hardcoded string literal as the room name (e.g. vibes.shared.join("lobby")) unless the user explicitly wants ALL viewers to share one single global state. A hardcoded room name means every person who visits this widget reads and writes the same shared state — it is a global room. For per-user or per-session isolation, derive the room name from a variable (e.g. a user ID, session token, or random value). When in doubt, use vibes.save/vibes.load for per-user persistence instead.
--- VIBES SDK CONTEXT ---
## Vibes SDK Reference
You are building an HTML widget for It Just Vibes (itjustvibes.com). The Vibes SDK is auto-injected — do NOT add a script tag. Just use `window.vibes` (or just `vibes`).
### Setup
Wrap your startup code in `vibes.onReady`:
```js
vibes.onReady(async () => {
const saved = await vibes.load("myKey");
// your widget logic here
});
```
### State (Per-User Persistence)
Every user gets their own isolated state per widget. All methods return Promises.
| Method | Description |
|--------|-------------|
| `await vibes.save(key, value)` | Save JSON-serializable data |
| `await vibes.load(key)` | Load saved data (returns `null` if not found) |
| `await vibes.delete(key)` | Delete a saved key |
| `await vibes.listKeys()` | Get array of all saved key names |
**Key rules:**
- Keys are strings, max 64 characters, alphanumeric + dashes/underscores
- Values must be JSON-serializable (objects, arrays, strings, numbers, booleans)
- Max 100KB per value, 500KB per widget per user, 5MB per user total
- Max 100 keys per widget per user
### Fetch Proxy
Use direct `fetch()` for APIs with permissive CORS headers (`Access-Control-Allow-Origin: *`). Use `vibes.fetch` for APIs without CORS headers (the proxy handles cross-origin requests):
```js
const resp = await vibes.fetch("https://api.example.com/data", {
method: "GET", // GET, POST, PUT, DELETE
headers: {}, // optional headers
body: null, // optional body (string)
timeout: null // optional timeout in ms
});
const data = await resp.json(); // or resp.text()
console.log(resp.status, resp.ok);
```
### Multiplayer (Shared State)
Real-time shared state across all users viewing the same widget.
```js
// Join a room (call once at startup)
await vibes.shared.join("lobby", { persistent: true });
// Set shared state (broadcasts to all users)
await vibes.shared.set("score", { player1: 10, player2: 7 });
// Read shared state (synchronous, returns last known value)
const score = vibes.shared.get("score");
// Listen for changes to a specific key
vibes.shared.onChange("score", (newValue) => {
console.log("Score updated:", newValue);
});
// Listen for any shared state change
vibes.shared.onAny((key, value) => {
console.log(key, "changed to", value);
});
// Get number of connected users
const count = await vibes.shared.getUserCount();
// Leave room
vibes.shared.leave();
// Clear all shared state for this room
await vibes.shared.clear();
```
**Shared state options:**
- `{ persistent: true }` — state survives page reloads (stored server-side)
- Default room name is "__default__" if omitted
### Agent-Accessible State (vibes.ai.*)
State written with the standard `vibes.save` / `vibes.load` methods is private to each user and is **NOT readable by AI agents**. To share state with an AI agent (via the MCP connector), use the `vibes.ai` sub-namespace:
| Method | Description |
|--------|-------------|
| `await vibes.ai.setState(key, value)` | Write agent-readable state. Key is stored internally as `ai/<key>`. |
| `await vibes.ai.getState(key)` | Read agent-readable state. Returns `null` if key absent. |
| `await vibes.ai.listKeys()` | List all agent-readable keys (without the `ai/` prefix). |
**Important rules:**
- Regular `vibes.save()` / `vibes.load()` is **NOT agent-accessible** — use `vibes.ai.*` for state you want agents to read.
- The widget **owner** must enable agent access in **Manage → Agent tab** before any agent can read or write `vibes.ai.*` state.
- Keys are auto-prefixed to `ai/` internally; you supply just the short key (e.g. `'context'`).
```js
vibes.onReady(async () => {
// Write state an AI agent can later read
await vibes.ai.setState('context', { currentLevel: 3, score: 1500 });
// Read it back (same auto-prefix applies)
const ctx = await vibes.ai.getState('context');
// List all agent-accessible keys for this widget
const keys = await vibes.ai.listKeys(); // e.g. ['context']
});
```
### Rules
1. **Do NOT use localStorage, sessionStorage, or window.storage** — they are blocked or undefined in the sandbox. Use `vibes.save`/`vibes.load` instead.
2. **Do NOT add a script tag to import the SDK** — it is auto-injected.
3. **Wrap startup code in `vibes.onReady()`** — the SDK may not be ready immediately.
4. **Await all SDK calls** — every method (except `vibes.shared.get`) returns a Promise.
5. **External JS libraries are allowed and encouraged** — load them via a `<script src>` tag from a reputable CDN (jsDelivr, unpkg, or cdnjs) with a pinned version, or inline the library source. The only exception: do NOT add a script tag for the Vibes SDK itself — it is auto-injected (see Rule 2).
6. **Keep total code under 100KB** — that is the default widget size limit (your account limit may be higher).
### Rate Limits
| Operation | Limit |
|-----------|-------|
| Writes (save + delete combined) | 30/min |
| Reads (load) | 60/min |
| List keys | 30/min |
| Fetch proxy | Rate limited per widget |
### Error Handling
All SDK methods can reject. Wrap in try/catch:
```js
try {
await vibes.save("key", value);
} catch (err) {
console.error("Save failed:", err.message);
}
```
Fetch proxy errors include `err.code`: `"RATE_LIMITED"`, `"BLOCKED"`, `"FETCH_ERROR"`.
### Data Export
Export rows of data as CSV or Excel directly from your widget.
```js
// Register dataset for the platform's "Export data" button
// AND optionally trigger an immediate download
vibes.exportData(rows, { filename: 'results.csv' })
// rows: Array of arrays (each inner array is one row; first row = headers)
// options.filename: sets the download filename and format (csv or xlsx)
// options.directDownload: false to only register without downloading (default: true)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `filename` | string | `'data.csv'` | Download filename; extension determines format (`.csv` or `.xlsx`) |
| `directDownload` | boolean | `true` | Trigger immediate browser download in addition to registering the dataset |
**Notes:**
- First call registers the dataset so the widget chrome shows an "Export data" button
- Use `.csv` extension for CSV, `.xlsx` for Excel
- CSV injection safety is automatic (formula-starting cells prefixed with `'`)
- Data stays in the browser — no server round-trip
--- FORK & RESUBMIT INSTRUCTION ---
Return the complete, self-contained HTML file with all changes applied.
It should be ready to paste into itjustvibes.com/submit to create a new fork.
Include this comment at the top of the output:
/* Forked from: https://itjustvibes.com/Owner/site-log-a-field-tracker-you-talk-to */