0
TOOLS#55cfd5ad
Field Tracker
@Owner·deposited 3w ago·updated 3w ago·93 views
TOOLS#55cfd5ad
Field Tracker
OW
@Owner
93Views
0Comments
0Forks
0Saves
SHARE · REMIX
Field Tracker — a HTML tools widget by @Owner.
CONTROLS
trackerprojectstasksfieldnotes
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: "Field Tracker" by @Owner]
Source: https://itjustvibes.com/Owner/field-tracker-m0eu
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, viewport-fit=cover">
<title>Field Tracker</title>
<style>
:root{
--bg:#0e1116; --panel:#161b22; --panel2:#1c232d; --line:#262d38;
--txt:#e6edf3; --dim:#9aa7b4; --faint:#6b7785;
--accent:#4c9aff; --green:#3fb950; --amber:#d29922; --red:#f85149;
--r:14px;
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{margin:0;background:var(--bg);color:var(--txt);
font:15px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
#app{max-width:760px;margin:0 auto;padding:14px 14px 80px}
h1{font-size:18px;margin:0;font-weight:650;letter-spacing:.2px}
.sub{color:var(--faint);font-size:12px;margin-top:2px}
.row{display:flex;align-items:center;gap:8px}
.between{justify-content:space-between}
.head{margin-bottom:12px}
.iconbtn{background:var(--panel);border:1px solid var(--line);color:var(--dim);
border-radius:10px;padding:7px 10px;font-size:13px;cursor:pointer}
.iconbtn:active{transform:scale(.97)}
.search{width:100%;background:var(--panel);border:1px solid var(--line);
color:var(--txt);border-radius:12px;padding:11px 12px;font-size:15px;outline:none}
.search:focus{border-color:var(--accent)}
.chips{display:flex;gap:7px;flex-wrap:wrap;margin:11px 0 4px}
.chip{background:var(--panel);border:1px solid var(--line);color:var(--dim);
border-radius:999px;padding:5px 11px;font-size:12.5px;cursor:pointer;white-space:nowrap}
.chip.on{background:var(--accent);border-color:var(--accent);color:#fff}
.seg{display:flex;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden}
.seg button{flex:1;background:transparent;border:0;color:var(--dim);padding:8px 6px;font-size:13px;cursor:pointer}
.seg button.on{background:var(--panel2);color:var(--txt)}
.grp{margin-top:18px}
.grp-h{font-size:12px;letter-spacing:.4px;text-transform:uppercase;color:var(--faint);
margin:0 2px 8px;display:flex;justify-content:space-between}
.item{display:flex;gap:11px;padding:11px 12px;background:var(--panel);
border:1px solid var(--line);border-radius:var(--r);margin-bottom:8px;align-items:flex-start}
.box{flex:none;width:21px;height:21px;border-radius:6px;border:1.8px solid var(--faint);
margin-top:1px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:.12s}
.box:active{transform:scale(.9)}
.box.done{background:var(--green);border-color:var(--green)}
.box.done:after{content:"";width:5px;height:9px;border:solid #0e1116;border-width:0 2.2px 2.2px 0;
transform:rotate(45deg);margin-top:-2px}
.box.wait{border-color:var(--amber);border-style:dashed}
.it-body{flex:1;min-width:0}
.it-text{font-size:14.5px}
.it-text.done{color:var(--faint);text-decoration:line-through}
.meta{margin-top:4px;display:flex;gap:6px;flex-wrap:wrap}
.tag{font-size:11px;color:var(--dim);background:var(--panel2);border:1px solid var(--line);
border-radius:6px;padding:1px 7px}
.tag.p{color:#a8c7fa}.tag.per{color:#f0b8d8}.tag.loc{color:#8fd3c0}.tag.due{color:#e3b341}
.tag.waitlbl{color:var(--amber)}
.note{background:var(--panel);border:1px solid var(--line);border-radius:var(--r);
padding:11px 12px;margin-bottom:8px;cursor:pointer}
.note h4{margin:0;font-size:14px;font-weight:600}
.note .body{color:var(--dim);font-size:13.5px;margin-top:6px;white-space:pre-wrap;display:none}
.note.open .body{display:block}
.empty{text-align:center;color:var(--faint);padding:34px 16px;font-size:14px}
.empty b{color:var(--dim)}
.addbar{display:flex;gap:8px;margin-top:6px}
.addbar input{flex:1}
.hint{margin-top:22px;background:var(--panel);border:1px dashed var(--line);
border-radius:var(--r);padding:12px 14px;color:var(--dim);font-size:13px}
.hint b{color:var(--txt)}
.hint code{background:var(--panel2);padding:1px 6px;border-radius:6px;color:#a8c7fa;font-size:12.5px}
.diag{margin-top:14px;border:1px solid var(--amber);background:rgba(210,153,34,.08);
color:#e9c46a;border-radius:12px;padding:11px 13px;font-size:12.5px;display:none}
.diag.show{display:block}
.save-flash{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);
background:var(--panel2);border:1px solid var(--line);color:var(--txt);
padding:8px 14px;border-radius:999px;font-size:13px;opacity:0;transition:.2s;pointer-events:none}
.save-flash.show{opacity:1}
.tabbar{display:flex;gap:7px;margin:14px 0 2px}
.tabbar .chip{flex:1;text-align:center}
</style>
</head>
<body>
<div id="app">
<div class="head">
<div class="row between">
<div>
<h1>Field Tracker</h1>
<div class="sub" id="updated">loading…</div>
</div>
<button class="iconbtn" id="refresh">↻ Sync</button>
</div>
</div>
<input class="search" id="search" placeholder="Search anyone, anywhere, anything…">
<div class="tabbar">
<div class="chip on" data-tab="tasks" id="tab-tasks">Tasks</div>
<div class="chip" data-tab="notes" id="tab-notes">Notes & knowledge</div>
</div>
<!-- TASKS VIEW -->
<div id="view-tasks">
<div class="seg" id="statusseg" style="margin-top:10px">
<button data-st="active" class="on">Active</button>
<button data-st="open">To do</button>
<button data-st="waiting">Backburner</button>
<button data-st="done">Done</button>
<button data-st="all">All</button>
</div>
<div class="chips" id="projchips"></div>
<div class="addbar">
<input class="search" id="addtext" placeholder="Quick add a task…">
<button class="iconbtn" id="addbtn" style="padding:0 14px">Add</button>
</div>
<div id="tasklist"></div>
</div>
<!-- NOTES VIEW -->
<div id="view-notes" style="display:none">
<div id="notelist" style="margin-top:12px"></div>
</div>
<div class="hint">
<b>The fast way to update this:</b> talk to Claude (voice works great while driving).
Try <code>"I took the riser photo at Riverside"</code> or
<code>"Dave agreed the survey for the 14th — backburner it and remind me to call the QS next week"</code>.
Claude writes straight to this tracker; hit <b>↻ Sync</b> to see it.
</div>
<div class="diag" id="diag"></div>
</div>
<div class="save-flash" id="flash">saved</div>
<script>
(function(){
"use strict";
/* ====== Vibes SDK storage ======
Agent-readable state lives in the vibes.ai.* namespace.
Short key 'tracker' is stored internally as ai/tracker. */
var KEY="tracker";
var STORE=null; // {get,set}
var AGENT_OK=false; // true when using vibes.ai (Claude can read/write)
var SDK_OK=false;
var saving=false;
function makeStore(){
if(typeof vibes==="undefined" || !vibes) return null;
if(vibes.ai && typeof vibes.ai.getState==="function" && typeof vibes.ai.setState==="function"){
AGENT_OK=true;
return {
get:function(){return vibes.ai.getState(KEY);},
set:function(v){return vibes.ai.setState(KEY,v);}
};
}
if(typeof vibes.load==="function" && typeof vibes.save==="function"){
AGENT_OK=false; // saved per-user but NOT visible to Claude
return {
get:function(){return vibes.load(KEY);},
set:function(v){return vibes.save(KEY,v);}
};
}
return null;
}
function coerce(v){
if(v==null) return null;
if(typeof v==="string"){ try{ return JSON.parse(v); }catch(e){ return v; } }
return v;
}
async function readState(){
if(!STORE) return null;
try{ return coerce(await STORE.get()); }
catch(e){ console.warn("load failed:",e&&e.message); return null; }
}
async function writeState(obj){
obj.updated=new Date().toISOString();
if(!STORE){ flash("can't save here — tell Claude"); return false; }
saving=true;
try{ await STORE.set(obj); flash(AGENT_OK?"saved":"saved (local only)"); return true; }
catch(e){ console.warn("save failed:",e&&e.message); flash("save failed"); return false; }
finally{ saving=false; }
}
/* ---------- model ---------- */
var DATA={version:1,updated:null,projects:[],items:[],notes:[]};
function uid(p){return (p||"x")+Math.random().toString(36).slice(2,8)}
/* ---------- ui state ---------- */
var tab="tasks", statusFilter="active", projFilter=null, q="";
var $=function(id){return document.getElementById(id)};
function flash(t){var f=$("flash");if(!f)return;f.textContent=t;f.classList.add("show");
clearTimeout(f._t);f._t=setTimeout(function(){f.classList.remove("show")},1400)}
function esc(s){return (s==null?"":String(s)).replace(/[&<>"]/g,function(c){
return {"&":"&","<":"<",">":">",'"':"""}[c]})}
function matches(it){
if(!q) return true;
var hay=[it.text,it.project,it.person,it.location,(it.tags||[]).join(" "),it.note].join(" ").toLowerCase();
return hay.indexOf(q)>-1;
}
function noteMatches(n){
if(!q) return true;
var hay=[n.title,n.project,n.person,n.location,n.body,(n.tags||[]).join(" ")].join(" ").toLowerCase();
return hay.indexOf(q)>-1;
}
function fmtDue(d){
if(!d) return null;
try{ var dt=new Date(d); if(isNaN(dt)) return d;
return dt.toLocaleDateString(undefined,{month:"short",day:"numeric"}); }catch(e){return d}
}
function renderProjChips(){
var c=$("projchips"); var ps=DATA.projects||[];
var h='<div class="chip '+(projFilter===null?"on":"")+'" data-p="">All projects</div>';
ps.forEach(function(p){ h+='<div class="chip '+(projFilter===p?"on":"")+'" data-p="'+esc(p)+'">'+esc(p)+'</div>'; });
c.innerHTML=h;
Array.prototype.forEach.call(c.querySelectorAll(".chip"),function(el){
el.onclick=function(){ var p=el.getAttribute("data-p"); projFilter=p?p:null; render(); };
});
}
function statusOf(it){return it.status||"open"}
function passStatus(it){
var s=statusOf(it);
if(statusFilter==="all") return true;
if(statusFilter==="active") return s==="open"||s==="waiting";
return s===statusFilter;
}
function itemHTML(it){
var s=statusOf(it), done=s==="done", wait=s==="waiting";
var meta="";
if(wait) meta+='<span class="tag waitlbl">⏸ backburner</span>';
if(it.person) meta+='<span class="tag per">@ '+esc(it.person)+'</span>';
if(it.location) meta+='<span class="tag loc">⌖ '+esc(it.location)+'</span>';
var due=fmtDue(it.due); if(due) meta+='<span class="tag due">⏷ '+esc(due)+'</span>';
(it.tags||[]).forEach(function(t){meta+='<span class="tag">#'+esc(t)+'</span>'});
return '<div class="item" data-id="'+esc(it.id)+'">'+
'<div class="box '+(done?"done":wait?"wait":"")+'" data-act="toggle" data-id="'+esc(it.id)+'"></div>'+
'<div class="it-body">'+
'<div class="it-text '+(done?"done":"")+'">'+esc(it.text)+'</div>'+
(meta?'<div class="meta">'+meta+'</div>':'')+
'</div></div>';
}
function render(){
var u=$("updated");
if(u) u.textContent = DATA.updated
? "updated "+new Date(DATA.updated).toLocaleString(undefined,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})
+ (AGENT_OK?"":" · not synced to Claude")
: (SDK_OK?"no data yet — add a task or ask Claude":"offline view");
$("tab-tasks").classList.toggle("on",tab==="tasks");
$("tab-notes").classList.toggle("on",tab==="notes");
$("view-tasks").style.display = tab==="tasks"?"":"none";
$("view-notes").style.display = tab==="notes"?"":"none";
if(tab==="tasks"){
renderProjChips();
Array.prototype.forEach.call($("statusseg").children,function(b){
b.classList.toggle("on", b.getAttribute("data-st")===statusFilter);
});
var items=(DATA.items||[]).filter(function(it){
return passStatus(it) && (projFilter===null||it.project===projFilter) && matches(it);
});
var groups={}, order=[];
items.forEach(function(it){
var g=it.project||"Other / internal";
if(!groups[g]){groups[g]=[];order.push(g)}
groups[g].push(it);
});
var h="";
if(!items.length){
h='<div class="empty"><b>Nothing here.</b><br>'+
(SDK_OK?'Add a task above, or tell Claude what came up.':'Open this on itjustvibes.com so it can load your data.')+'</div>';
} else {
order.forEach(function(g){
h+='<div class="grp"><div class="grp-h"><span>'+esc(g)+'</span><span>'+groups[g].length+'</span></div>';
groups[g].forEach(function(it){ h+=itemHTML(it); });
h+='</div>';
});
}
$("tasklist").innerHTML=h;
Array.prototype.forEach.call($("tasklist").querySelectorAll('[data-act="toggle"]'),function(el){
el.onclick=function(){ toggle(el.getAttribute("data-id")); };
});
} else {
var notes=(DATA.notes||[]).filter(noteMatches);
var nh="";
if(!notes.length){
nh='<div class="empty"><b>No notes yet.</b><br>Tell Claude things like<br>"save the contract terms we agreed with Acme".</div>';
} else {
notes.forEach(function(n){
var m=[];
if(n.project)m.push(esc(n.project));
if(n.person)m.push("@"+esc(n.person));
if(n.location)m.push("⌖"+esc(n.location));
if(n.date)m.push(esc(n.date));
nh+='<div class="note" data-nid="'+esc(n.id)+'">'+
'<h4>'+esc(n.title||"Untitled")+'</h4>'+
(m.length?'<div class="meta"><span class="tag">'+m.join('</span><span class="tag">')+'</span></div>':'')+
'<div class="body">'+esc(n.body||"")+'</div></div>';
});
}
$("notelist").innerHTML=nh;
Array.prototype.forEach.call($("notelist").querySelectorAll(".note"),function(el){
el.onclick=function(){ el.classList.toggle("open"); };
});
}
}
function toggle(id){
var it=(DATA.items||[]).find(function(x){return x.id===id});
if(!it) return;
it.status = (statusOf(it)==="done") ? "open" : "done";
if(it.status==="done") it.doneAt=new Date().toISOString();
render();
writeState(DATA);
}
function quickAdd(){
var v=$("addtext").value.trim();
if(!v) return;
var item={id:uid("i"),text:v,status:"open",
project: projFilter || (DATA.projects&&DATA.projects[0]) || "",
person:"",location:"",due:null,tags:[],note:"",created:new Date().toISOString()};
DATA.items=DATA.items||[]; DATA.items.unshift(item);
$("addtext").value="";
render();
writeState(DATA);
}
async function load(){
var s=await readState();
if(s && typeof s==="object"){
DATA={version:s.version||1,updated:s.updated||null,
projects:s.projects||[],items:s.items||[],notes:s.notes||[]};
}
render();
}
function showDiag(msg){
var d=$("diag"); if(!d) return;
d.textContent="⚠ "+msg; d.classList.add("show");
}
function wireUI(){
$("search").addEventListener("input",function(e){q=e.target.value.trim().toLowerCase();render()});
$("refresh").onclick=function(){ $("refresh").textContent="↻ …";
Promise.resolve(STORE?load():null).then(function(){ $("refresh").textContent="↻ Sync"; }); };
$("addbtn").onclick=quickAdd;
$("addtext").addEventListener("keydown",function(e){if(e.key==="Enter")quickAdd()});
Array.prototype.forEach.call($("statusseg").children,function(b){
b.onclick=function(){statusFilter=b.getAttribute("data-st");render()};
});
$("tab-tasks").onclick=function(){tab="tasks";render()};
$("tab-notes").onclick=function(){tab="notes";render()};
document.addEventListener("visibilitychange",function(){if(!document.hidden&&!saving&&STORE)load();});
setInterval(function(){if(!document.hidden&&!saving&&STORE)load();},20000);
}
function start(){
wireUI();
STORE=makeStore();
SDK_OK=!!STORE;
if(SDK_OK){
load();
if(!AGENT_OK) showDiag("Saving on your device, but the agent channel (vibes.ai) wasn't available, so Claude can't read this yet. Tell Claude if this persists.");
} else {
render();
showDiag("Storage SDK not found — this is the offline preview. Open the widget on itjustvibes.com to load and save your tracker.");
}
}
function ready(fn){
if(document.readyState==="loading") document.addEventListener("DOMContentLoaded",fn);
else fn();
}
if(typeof vibes!=="undefined" && vibes && typeof vibes.onReady==="function"){
vibes.onReady(function(){ ready(start); });
} else {
ready(start); // local/offline preview
}
})();
</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/field-tracker-m0eu */