Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions dashboard/analysis_handoff.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
(() => {
'use strict';
const $ = s => document.querySelector(s);
const esc = v => String(v ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
let period = 'week', start = '', end = '', preview = null, generation = 0;
const dateInput = d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
function selection() {
if (period === 'current' || period === 'all') return {scope: period};
const now = new Date(), a = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const b = new Date(a); b.setDate(b.getDate()+1);
if (period === 'week') a.setDate(a.getDate()-6);
if (period === 'custom') {
if (!start || !end) throw new Error('Choose both dates.');
const aa = new Date(start+'T00:00:00'), bb = new Date(end+'T00:00:00');
bb.setDate(bb.getDate()+1);
if (!Number.isFinite(+aa) || !Number.isFinite(+bb) || aa >= bb) throw new Error('End date must be on or after start.');
return {scope:'range',since:aa.toISOString(),until:bb.toISOString()};
}
return {scope:'range',since:a.toISOString(),until:b.toISOString()};
}
window.owgEvidenceSelection = selection;
async function api(path, options) {
await window.__owgAuthReady;
const r = await fetch(path,{cache:'no-store',...options});
const d = await r.json().catch(()=>({}));
if (!r.ok) throw new Error(d.detail || 'Could not prepare evidence.');
return d;
}
async function download(fmt, detailed, redacted=true, chosen=selection()) {
const d = await api('/v1/export-ticket',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({format:fmt,...chosen,include_raw:!!detailed,redact_names:!!redacted})});
const a = document.createElement('a');a.href=d.url;a.download='';a.hidden=true;
document.body.appendChild(a);a.click();a.remove();
}
function syncControls() {
document.querySelectorAll('.owg-period').forEach(node => {
node.querySelector('select').value=period;
node.querySelector('[data-custom]').hidden=period!=='custom';
node.querySelector('[data-start]').value=start;node.querySelector('[data-end]').value=end;
});
}
async function refresh() {
const seq=++generation; preview=null;
document.querySelectorAll('[data-period-summary]').forEach(x=>x.textContent='Checking selected evidence…');
try {
const d=await api('/v1/analysis-preview?'+new URLSearchParams(selection()));
if(seq!==generation)return;preview=d;
const text=d.canonical_rows
? `${d.canonical_rows} recorded events · observed ${new Date(d.first_observed_at).toLocaleString()} – ${new Date(d.last_observed_at).toLocaleString()}`
: d.retained_rows ? 'No activity in this selection. Saved work is available; choose another period.' : 'No saved evidence yet. Work normally after enabling recording.';
document.querySelectorAll('[data-period-summary]').forEach(x=>x.textContent=text);
} catch(e) {
if(seq!==generation)return;
document.querySelectorAll('[data-period-summary]').forEach(x=>x.textContent=e.message);
}
}
async function prepare() {
await refresh();
if(!preview){window.toast?.('Could not prepare selected evidence. Check the period and dashboard connection.');return;}
const d=preview, request=d.request_text;
const chosen={scope:d.window.scope,...(d.window.scope==='range'?{since:d.window.since,until:d.window.until}:{})};
const access=d.local_ai_history_access || {};
const localRead=!d.local_ai_enabled ? 'Local AI access is off.' : access.mode==='off' ? 'Local AI can read the current run only; saved-history access is off.' : `Local saved-history permission: ${access.mode}${access.expires_at?' until '+new Date(access.expires_at).toLocaleString():''}.`;
window.openModal?.('Prepare for AI','Discover opportunities from recorded work',`
<p>No workflow description or repeated pattern is required. Your AI will interpret the evidence; this panel does not run a model.</p>
<div class="note"><strong>What the AI will receive</strong>
<p>${esc(d.canonical_rows)} recorded events · ${esc(d.event_types.focus_span||0)} focus spans · ${esc(d.event_types.capture_gap||0)} unobserved-gap records.</p>
<p>Selected: ${esc(d.window.since||'all available history')} to ${esc(d.window.until||'the latest retained evidence')} (end exclusive, UTC). Observed: ${esc(d.first_observed_at||'none')} to ${esc(d.last_observed_at||'none')}.</p>
<p>Sources: ${esc(Object.entries(d.sources).map(([k,v])=>`${k}: ${v}`).join(', ')||'none')}. Visible work text is not included in this activity package. Typed values and clipboard contents are not included.</p>
<p>${esc(localRead)} Hosted AI availability must be checked separately: it only sees previously shared evidence. Preparing a file does not change those permissions.</p>
</div>
<details><summary>Preview redacted context examples</summary><ul>${d.redacted_examples.map(x=>`<li>${esc(x.app)} · ${esc(x.window_title||'Untitled activity')}</li>`).join('')||'<li>No focus spans in this selection.</li>'}</ul><p>Filtering is best effort; inspect the downloaded package before sharing it.</p></details>
<label style="display:flex;gap:8px;margin:12px 0"><input id="analysisDetailed" type="checkbox">Include detailed chronological evidence (more events and labels for deeper investigation)</label>
<p>Downloaded evidence is redacted. Gaps and foreground time are not measured lost work or savings.</p>
<div class="modal-actions"><button type="button" id="analysisDownload" ${d.canonical_rows?'':'disabled'}>Download AI evidence package</button><button type="button" class="secondary" id="analysisCopy">Copy request for my AI</button></div>
<details style="margin-top:12px"><summary>Read the request</summary><pre style="white-space:pre-wrap" id="analysisRequest"></pre></details>
<div class="muted" id="analysisActionStatus" role="status">Nothing has been sent to an AI. Downloads work with any capable AI, without an OWG cloud account.</div>`);
$('#analysisRequest').textContent=request;
$('#analysisCopy').onclick=async()=>{
try{await navigator.clipboard.writeText(request);$('#analysisActionStatus').textContent='Request copied. Paste it into your AI; no model was invoked.';}
catch(_){$('#analysisActionStatus').textContent='Clipboard unavailable. Open “Read the request” and copy the text.';}
};
$('#analysisDownload').onclick=async()=>{
try{await download('csvzip',$('#analysisDetailed').checked,true,chosen);$('#analysisActionStatus').textContent='Download started. Inspect the file, then attach it to your AI. No data was uploaded.';}
catch(e){$('#analysisActionStatus').textContent=e.message;}
};
}
function install() {
const style=document.createElement('style');style.textContent='.owg-period{display:flex;align-items:center;flex-wrap:wrap;gap:10px;margin-bottom:14px;padding:12px 14px;border:1px solid var(--line);border-radius:12px;background:#fff}.owg-period select,.owg-period input{font:inherit;padding:7px;border:1px solid var(--line);border-radius:7px}.owg-period [data-period-summary]{flex-basis:100%;font-size:12px;color:var(--muted)}.owg-period [data-custom]{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.owg-period [hidden]{display:none!important}';document.head.appendChild(style);
const today=new Date();start=dateInput(new Date(today.getFullYear(),today.getMonth(),today.getDate()-6));end=dateInput(today);
for(const id of ['overview','evidence','connect','export']) {
const panel=$('#panel-'+id);if(!panel)continue;
const bar=document.createElement('div');bar.className='owg-period';
bar.innerHTML=`<strong>Work period</strong><select aria-label="Work period"><option value="today">Today</option><option value="week">Last 7 days</option><option value="custom">Custom dates</option><option value="current">This run</option><option value="all">All retained work</option></select><span data-custom hidden><label>From <input type="date" data-start></label><label>Through <input type="date" data-end></label></span><span class="muted">${esc(Intl.DateTimeFormat().resolvedOptions().timeZone||'local time')}</span><button type="button" class="secondary">Prepare for AI</button><span data-period-summary role="status"></span>`;
// Scope applies to handoff/exports; existing current-run dashboards retain their labels.
const note=document.createElement('small');note.className='muted';note.textContent='Select the period to view in Activity or prepare for AI. Activity uses this period; current-run charts keep their labeled scope.';bar.appendChild(note);
panel.insertBefore(bar,panel.firstChild);
bar.querySelector('select').onchange=e=>{period=e.target.value;syncControls();refresh();document.dispatchEvent(new Event('owg:period-change'));};
bar.querySelectorAll('input').forEach(x=>x.onchange=()=>{start=bar.querySelector('[data-start]').value;end=bar.querySelector('[data-end]').value;syncControls();refresh();document.dispatchEvent(new Event('owg:period-change'));});
bar.querySelector('button').onclick=prepare;
}
syncControls();refresh();document.dispatchEvent(new Event('owg:period-change'));
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',install);else install();
})();
12 changes: 12 additions & 0 deletions dashboard/basic_mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,18 @@
title.textContent = 'Recording is paused'; sub.textContent = 'Nothing is recorded until you resume.'; dot.className = 'hero-dot paused';
} else if (state === 'stopped') {
title.textContent = 'Recording is stopped'; sub.textContent = 'Your data is kept on this computer.'; dot.className = 'hero-dot off';
} else if (capture.missing_permissions?.length) {
title.textContent = 'Recorder needs permission';
sub.textContent = 'OpenWorkGraph is waiting for the required OS permissions. Saved work remains available.';
dot.className = 'hero-dot paused';
} else if (capture.away) {
title.textContent = 'Recorder is away';
sub.textContent = 'The computer is idle or locked. Saved work remains available.';
dot.className = 'hero-dot paused';
} else if (capture.collector_alive === false) {
title.textContent = 'Waiting for the recorder';
sub.textContent = 'No collector heartbeat has arrived for this run. Saved work remains available.';
dot.className = 'hero-dot paused';
} else {
title.textContent = engaged >= 60 ? `${fmtDuration(engaged)} of work recorded so far` : 'Recording your work';
const parts = [];
Expand Down
3 changes: 2 additions & 1 deletion dashboard/evidence_paging.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
}

function endpoint(cursor) {
const params=new URLSearchParams({scope:'current',limit:String(PAGE_SIZE)});
const params=new URLSearchParams({...(window.owgEvidenceSelection?.()||{scope:'current'}),limit:String(PAGE_SIZE)});
const q=currentQuery();
if (q) params.set('q',q);
if (selectedSurface) params.set('surface',selectedSurface);
Expand Down Expand Up @@ -168,6 +168,7 @@
document.querySelectorAll('[data-evidence-view]').forEach(button=>button.addEventListener('click',()=>{
if (button.dataset.evidenceView==='all') schedulePageLoad(0);
}));
document.addEventListener('owg:period-change',()=>{resetPages();loadPage();});
ensureControls();
if (window.__owgLastSummary) pagedRenderEvidence(window.__owgLastSummary);
}
Expand Down
2 changes: 1 addition & 1 deletion dashboard/gateway_panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@
const runs=(p.runs||[]).map(run=>`<tr><td>${e(String(run.started_at||'').replace('T',' ').slice(0,16))}–${e(String(run.ended_at||'').slice(11,16))}</td><td>${(run.surfaces||[]).map(e).join(' → ')}</td><td>${fmtDuration(run.engaged_seconds)}</td><td>${e(String((run.boundary||{}).end_reason||'').replace(/_/g,' '))}</td></tr>`).join('');
const data=encodeURIComponent(JSON.stringify({name:p.name,signature:p.signature,event_ids:(p.runs||[]).flatMap(r=>r.event_ids||[])}));
return `<div class="pattern" id="rich-pattern-${i}"><div class="pattern-head"><div><strong>${e(safeName)}</strong> <span class="badge">Needs review · ${e(p.confidence||'low')}</span><div style="margin-top:7px">${steps}</div></div><div><strong>×${Number(p.observed_count||p.runs?.length||0)}</strong><div class="muted">median ${fmtDuration(p.median_engaged_seconds)} · total ${fmtDuration(p.total_engaged_seconds)}</div></div></div><div class="pattern-actions"><button class="secondary" data-show-runs="${i}">Show the ${Number(p.runs?.length||p.observed_count||0)} runs</button><button class="secondary" data-automation="${i}">Prepare automation context</button><button class="secondary" data-pattern-export="${data}">Export evidence IDs</button></div><div class="pattern-runs"><div class="table-wrap"><table><thead><tr><th>When</th><th>Steps</th><th>Engaged time</th><th>How it ended</th></tr></thead><tbody>${runs||'<tr><td colspan="4">Execution windows are unavailable for this pattern.</td></tr>'}</tbody></table></div></div></div>`;
}).join('') || (typeof window.emptyState==='function' ? window.emptyState('No repeated workflow yet','When you do the same sequence of steps across tools at least twice, it appears here with how long each run took.') : '<div class="muted">No repeated workflow yet.</div>');
}).join('') || (typeof window.emptyState==='function' ? window.emptyState('No repeated pattern found in this run','You can still prepare the recorded chronology for AI analysis.') : '<div class="muted">No repeated pattern found in this run. You can still prepare the recorded chronology for AI analysis.</div>');
host.querySelectorAll('[data-show-runs]').forEach(btn=>btn.onclick=()=>document.querySelector(`#rich-pattern-${btn.dataset.showRuns}`)?.classList.toggle('expanded'));
host.querySelectorAll('[data-automation]').forEach(btn=>btn.onclick=()=>{if(typeof window.activateTab==='function')window.activateTab('connect');if(typeof window.toast==='function')window.toast('Connect your AI, then ask it to inspect this repeated workflow through OpenWorkGraph MCP.');});
host.querySelectorAll('[data-pattern-export]').forEach(btn=>btn.onclick=()=>{
Expand Down
Loading
Loading