diff --git a/plugins/activity/frontend/src/index.js b/plugins/activity/frontend/src/index.js index a720189..f50871c 100644 --- a/plugins/activity/frontend/src/index.js +++ b/plugins/activity/frontend/src/index.js @@ -8,6 +8,7 @@ var PLUGIN_ID = 'verstak.activity'; var MAX_EVENTS = 250; + var RAW_DATA_NAME = 'activity-events'; var MAX_CANDIDATES = 12; var LEGACY_KEY = 'events'; var GLOBAL_KEY = 'events:global'; @@ -30,7 +31,8 @@ 'browser.capture.selection', 'browser.capture.link', 'browser.capture.file', - 'browser.capture.converted' + 'browser.capture.converted', + 'browser.activity.batch' ]; var EVENT_LABELS = { 'workspace.selected': 'Workspace selected', @@ -44,7 +46,8 @@ 'browser.capture.selection': 'Selection captured', 'browser.capture.link': 'Link captured', 'browser.capture.file': 'File captured', - 'browser.capture.converted': 'Capture converted' + 'browser.capture.converted': 'Capture converted', + 'browser.activity.domain': 'Browser domain activity' }; var LOW_VALUE_EVENT_TYPES = { 'workspace.selected': true, @@ -347,6 +350,15 @@ return sortEvents(scopedEvents.concat(globalEvents, legacyEvents)); } + function eventsFromRecords(records, workspaceRoot) { + var normalized = normalizeStoredEvents(records, RAW_DATA_NAME); + var workspace = cleanWorkspace(workspaceRoot); + if (!workspace) return sortEvents(normalized); + return sortEvents(normalized.filter(function (item) { + return item.workspaceRootPath === workspace; + })); + } + function candidateStorageKey(workspaceRoot) { return CANDIDATE_PREFIX + encodeKey(workspaceRoot); } @@ -436,7 +448,7 @@ return candidateWorkspace(activity) !== scope.workspaceRoot; }); updateCandidates(); - persist().then(render); + clearWorkspaceRaw(scope.workspaceRoot).then(persist).then(render); } }); toolbar.appendChild(titleEl); @@ -503,7 +515,12 @@ events = []; return Promise.resolve(); } - return api.settings.read().then(function (settings) { + var clearRaw = api.storage && api.storage.data && typeof api.storage.data.writeNDJSON === 'function' + ? api.storage.data.writeNDJSON(RAW_DATA_NAME, []) + : Promise.resolve(); + return clearRaw.then(function () { + return api.settings.read(); + }).then(function (settings) { var keys = globalEventKeys(settings || {}).concat(Object.keys(settings || {}).filter(function (key) { return key.indexOf(CANDIDATE_PREFIX) === 0 || key.indexOf(DISMISSAL_PREFIX) === 0; })); @@ -525,6 +542,19 @@ }); } + function clearWorkspaceRaw(workspaceRoot) { + if (!api || !api.storage || !api.storage.data || typeof api.storage.data.readNDJSON !== 'function' || typeof api.storage.data.writeNDJSON !== 'function') { + return Promise.resolve(); + } + return api.storage.data.readNDJSON(RAW_DATA_NAME).then(function (records) { + var workspace = cleanWorkspace(workspaceRoot); + var kept = (Array.isArray(records) ? records : []).filter(function (record) { + return cleanWorkspace(record && (record.workspaceRootPath || workspaceFromPayload(record.payload || {}))) !== workspace; + }); + return api.storage.data.writeNDJSON(RAW_DATA_NAME, kept); + }); + } + function renderList() { listEl.innerHTML = ''; if (events.length === 0) { @@ -634,10 +664,17 @@ function loadStored() { if (!api || !api.settings || typeof api.settings.read !== 'function') return Promise.resolve(); - return api.settings.read().then(function (settings) { + var readRaw = api.storage && api.storage.data && typeof api.storage.data.readNDJSON === 'function' + ? api.storage.data.readNDJSON(RAW_DATA_NAME) + : Promise.resolve([]); + return Promise.all([api.settings.read(), readRaw]).then(function (results) { + var settings = results[0] || {}; + var rawRecords = Array.isArray(results[1]) ? results[1] : []; settings = settings || {}; - candidateSourceEvents = eventsFromSettings(settings, ''); - events = eventsFromSettings(settings, scope.mode === 'workspace' ? scope.workspaceRoot : ''); + candidateSourceEvents = rawRecords.length ? eventsFromRecords(rawRecords, '') : eventsFromSettings(settings, ''); + events = rawRecords.length + ? eventsFromRecords(rawRecords, scope.mode === 'workspace' ? scope.workspaceRoot : '') + : eventsFromSettings(settings, scope.mode === 'workspace' ? scope.workspaceRoot : ''); dismissedByWorkspace = dismissedCandidatesFromSettings(settings); updateCandidates(); return persistCandidateCaches(); @@ -652,9 +689,14 @@ if (!api || !api.settings || typeof api.settings.read !== 'function') { return Promise.resolve({ candidates: visibleCandidates(candidateSourceEvents, workspace || (scope.mode === 'workspace' ? scope.workspaceRoot : ''), dismissedByWorkspace) }); } - return api.settings.read().then(function (settings) { - settings = settings || {}; - return { candidates: visibleCandidates(eventsFromSettings(settings, ''), workspace, dismissedCandidatesFromSettings(settings)) }; + var readRaw = api.storage && api.storage.data && typeof api.storage.data.readNDJSON === 'function' + ? api.storage.data.readNDJSON(RAW_DATA_NAME) + : Promise.resolve([]); + return Promise.all([api.settings.read(), readRaw]).then(function (results) { + var settings = results[0] || {}; + var rawRecords = Array.isArray(results[1]) ? results[1] : []; + var source = rawRecords.length ? eventsFromRecords(rawRecords, '') : eventsFromSettings(settings, ''); + return { candidates: visibleCandidates(source, workspace, dismissedCandidatesFromSettings(settings)) }; }).catch(function () { return { candidates: [] }; }); diff --git a/scripts/smoke-activity-plugin.js b/scripts/smoke-activity-plugin.js index 6d22ec4..44559ae 100644 --- a/scripts/smoke-activity-plugin.js +++ b/scripts/smoke-activity-plugin.js @@ -116,8 +116,9 @@ function loadComponent(document) { return component; } -function makeApi(initialSettings = {}) { +function makeApi(initialSettings = {}, initialData = {}) { const settings = { ...initialSettings }; + const data = { ...initialData }; const handlers = {}; const commandHandlers = new Map(); const unsubscribed = []; @@ -132,6 +133,14 @@ function makeApi(initialSettings = {}) { return { ...settings }; }, }, + storage: { + data: { + readNDJSON: async (name) => Array.isArray(data[name]) ? data[name].slice() : [], + writeNDJSON: async (name, records) => { + data[name] = Array.isArray(records) ? records.slice() : []; + }, + }, + }, events: { subscribe: async (name, handler) => { handlers[name] = handler; @@ -150,11 +159,14 @@ function makeApi(initialSettings = {}) { storedEvents(key = 'events') { return settings[key] || []; }, + storedData(name) { + return Array.isArray(data[name]) ? data[name] : []; + }, }; } async function flush() { - for (let i = 0; i < 10; i += 1) await Promise.resolve(); + for (let i = 0; i < 20; i += 1) await Promise.resolve(); } async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, workspaceRootPath: 'Project' }, document = makeDocument()) { @@ -353,7 +365,7 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w if (api.storedEvents('work-session-candidates:workspace:Project').length !== 0) throw new Error('clear action did not remove cached candidates'); component.unmount && component.unmount(container); - if (api.unsubscribed.length !== 33) throw new Error(`expected 33 unsubscribers, got ${api.unsubscribed.length}`); + if (api.unsubscribed.length !== 36) throw new Error(`expected 36 unsubscribers, got ${api.unsubscribed.length}`); const persistedApi = makeApi({ 'events:workspace:Project': [{ @@ -449,6 +461,28 @@ async function mountWithApi(api, props = { workspaceNode: { name: 'Project' }, w } component.unmount && component.unmount(sessionView.container); + const rawApi = makeApi({}, { + 'activity-events': [{ + activityId: 'browser-domain:batch-1:0', + type: 'browser.activity.domain', + title: 'example.com', + summary: '5 min browser activity', + occurredAt: '2026-07-12T10:05:00Z', + sourcePluginId: 'verstak-browser-extension', + sourceBatchId: 'batch-1', + hostname: 'example.com', + durationSeconds: 300, + payload: { hostname: 'example.com', durationSeconds: 300 }, + }], + }); + const rawView = await mountWithApi(rawApi, {}); + if (!rawView.container.textContent.includes('example.com')) throw new Error('append-only browser activity was not rendered'); + const rawClear = walk(rawView.container, (node) => node.getAttribute && node.getAttribute('data-activity-action') === 'clear'); + rawClear.click(); + await flush(); + if (rawApi.storedData('activity-events').length !== 0) throw new Error('clear activity did not replace append-only data'); + component.unmount && component.unmount(rawView.container); + console.log('activity plugin smoke passed'); })().catch((err) => { console.error(err);