agents-relay 2.0.177 → 2.0.178
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dashboard-data.js +47 -0
- package/dist/dashboard-html.js +38 -28
- package/dist/dashboard-style.js +4 -3
- package/dist/dashboard.js +10 -5
- package/package.json +1 -1
package/dist/dashboard-data.js
CHANGED
|
@@ -4,6 +4,19 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { eventsBusHistory } from './events-bus-runtime.js';
|
|
5
5
|
const repositoryPattern = /^([A-Za-z0-9](?:[A-Za-z0-9-]{0,36}[A-Za-z0-9])?)\/([A-Za-z0-9._-]{1,100})$/;
|
|
6
6
|
const workflowDirectory = fileURLToPath(new URL('../workflow/', import.meta.url));
|
|
7
|
+
export const DASHBOARD_JOB_PAGE_SIZE = 20;
|
|
8
|
+
export const DEFAULT_DASHBOARD_JOB_STATUSES = ['DRAFT', 'ACTIVE', 'COMPLETED'];
|
|
9
|
+
function compareDashboardJobs(a, b) {
|
|
10
|
+
const terminal = (job) => ['COMPLETED', 'CANCELLED'].includes(job.state) || ['MERGED', 'CLOSED'].includes(job.githubState ?? '');
|
|
11
|
+
const group = (job) => terminal(job) ? 3 : job.draft || job.state === 'DRAFT' ? 2 : 0;
|
|
12
|
+
const groupDelta = group(a) - group(b);
|
|
13
|
+
if (groupDelta)
|
|
14
|
+
return groupDelta;
|
|
15
|
+
const mergePendingDelta = Number(b.runtimeStatus === 'MERGE_PENDING') - Number(a.runtimeStatus === 'MERGE_PENDING');
|
|
16
|
+
if (mergePendingDelta)
|
|
17
|
+
return mergePendingDelta;
|
|
18
|
+
return String(b.updatedAt ?? '').localeCompare(String(a.updatedAt ?? ''));
|
|
19
|
+
}
|
|
7
20
|
export async function dashboardWorkflowGraphs() {
|
|
8
21
|
const names = (await readdir(workflowDirectory)).filter(name => name.endsWith('.mmd')).sort();
|
|
9
22
|
return Promise.all(names.map(async (name) => {
|
|
@@ -43,3 +56,37 @@ export async function eventHistory(jobId, taskId, limit) {
|
|
|
43
56
|
return [];
|
|
44
57
|
}
|
|
45
58
|
}
|
|
59
|
+
export function searchDashboardJobs(jobs, query) {
|
|
60
|
+
const requestedStatuses = [...new Set([...query.getAll('status'), ...query.getAll('statuses')].flatMap(value => value.split(',')).map(value => value.trim().toUpperCase()).filter(Boolean))];
|
|
61
|
+
const statuses = requestedStatuses.length > 0 ? requestedStatuses : [...DEFAULT_DASHBOARD_JOB_STATUSES];
|
|
62
|
+
const repositories = [...new Set([...query.getAll('repo'), ...query.getAll('repositories'), ...query.getAll('repos')].map(value => value.trim()).filter(Boolean))];
|
|
63
|
+
const search = query.get('q')?.trim().toLowerCase() ?? '';
|
|
64
|
+
const parsedPage = Number(query.get('page'));
|
|
65
|
+
const page = Number.isInteger(parsedPage) && parsedPage > 0 ? parsedPage : 1;
|
|
66
|
+
const parsedPageSize = Number(query.get('pageSize'));
|
|
67
|
+
const pageSize = Number.isInteger(parsedPageSize) && parsedPageSize > 0 ? Math.min(parsedPageSize, DASHBOARD_JOB_PAGE_SIZE) : DASHBOARD_JOB_PAGE_SIZE;
|
|
68
|
+
const matchesStatus = (job) => statuses.some(status => status === 'DRAFT'
|
|
69
|
+
? Boolean(job.draft) || job.state === 'DRAFT'
|
|
70
|
+
: job.state === status);
|
|
71
|
+
const matchesRepository = (job) => repositories.length === 0 || repositories.includes(job.repository);
|
|
72
|
+
const normalizedPr = search.replace(/^pr\s*#?/, '').replace(/^#/, '');
|
|
73
|
+
const matchesSearch = (job) => {
|
|
74
|
+
if (!search)
|
|
75
|
+
return true;
|
|
76
|
+
return String(job.title || job.id || '').toLowerCase().includes(search)
|
|
77
|
+
|| String(job.id || '').toLowerCase().includes(search)
|
|
78
|
+
|| Boolean(normalizedPr) && String(job.prNumber || '').includes(normalizedPr);
|
|
79
|
+
};
|
|
80
|
+
const matching = jobs.filter(job => matchesStatus(job) && matchesRepository(job) && matchesSearch(job)).sort(compareDashboardJobs);
|
|
81
|
+
const total = matching.length;
|
|
82
|
+
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
83
|
+
const safePage = Math.min(page, totalPages);
|
|
84
|
+
return {
|
|
85
|
+
jobs: matching.slice((safePage - 1) * pageSize, safePage * pageSize),
|
|
86
|
+
repositories: [...new Set(jobs.map(job => job.repository).filter(Boolean))].sort(),
|
|
87
|
+
total,
|
|
88
|
+
page: safePage,
|
|
89
|
+
pageSize,
|
|
90
|
+
totalPages,
|
|
91
|
+
};
|
|
92
|
+
}
|
package/dist/dashboard-html.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dashboardStyles } from './dashboard-style.js';
|
|
2
|
+
import { DASHBOARD_JOB_PAGE_SIZE, DEFAULT_DASHBOARD_JOB_STATUSES } from './dashboard-data.js';
|
|
2
3
|
import { createRequire } from 'node:module';
|
|
3
4
|
import { execFileSync } from 'node:child_process';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -41,7 +42,7 @@ ${dashboardStyles}
|
|
|
41
42
|
</header>
|
|
42
43
|
<main>
|
|
43
44
|
<section id="jobs-view" role="tabpanel" aria-labelledby="jobs-tab" class="panel jobs-panel">
|
|
44
|
-
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><input id="job-search" class="job-search" type="search" placeholder="Search title or PR #" aria-label="Search jobs by title or PR number"><select id="
|
|
45
|
+
<div class="panel-head"><h2>Jobs</h2><div class="panel-actions"><div class="job-filters"><input id="job-search" class="job-search" type="search" placeholder="Search title or PR #" aria-label="Search jobs by title or PR number"><select id="status-filter" class="job-filter" multiple aria-label="Filter jobs by status (choose at least one; empty uses DRAFT, ACTIVE, and COMPLETED)"><option value="DRAFT">DRAFT</option><option value="ACTIVE">ACTIVE</option><option value="COMPLETED">COMPLETED</option><option value="CANCELLED">CANCELLED</option></select><select id="repo-filter" class="job-filter" multiple aria-label="Filter jobs by repository (empty shows all repositories)"></select><button id="jobs-reset" class="refresh-button" type="button">Reset</button></div><div class="jobs-pagination"><button id="jobs-prev" class="refresh-button" type="button">Previous</button><span id="jobs-page" class="subtle">Page 1</span><button id="jobs-next" class="refresh-button" type="button">Next</button></div><button id="jobs-refresh" class="refresh-button" type="button">Refresh</button></div></div>
|
|
45
46
|
<div class="panel-body" aria-live="polite"><ul id="job-list" class="job-list"><li class="skeleton" aria-hidden="true"></li></ul></div>
|
|
46
47
|
</section>
|
|
47
48
|
<div class="workspace" id="jobs-workspace">
|
|
@@ -141,10 +142,13 @@ const shortcutsView=document.querySelector('#shortcuts-view');
|
|
|
141
142
|
const jobsWorkspace=document.querySelector('#jobs-workspace');
|
|
142
143
|
const jobList=document.querySelector('#job-list');
|
|
143
144
|
const jobsRefresh=document.querySelector('#jobs-refresh');
|
|
145
|
+
const jobsReset=document.querySelector('#jobs-reset');
|
|
144
146
|
const jobSearch=document.querySelector('#job-search');
|
|
147
|
+
const jobsPrev=document.querySelector('#jobs-prev');
|
|
148
|
+
const jobsNext=document.querySelector('#jobs-next');
|
|
149
|
+
const jobsPageLabel=document.querySelector('#jobs-page');
|
|
145
150
|
const repoFilter=document.querySelector('#repo-filter');
|
|
146
|
-
const
|
|
147
|
-
const quickDraft=document.querySelector('#quick-draft');
|
|
151
|
+
const statusFilter=document.querySelector('#status-filter');
|
|
148
152
|
const selectedJobRefresh=document.querySelector('#selected-job-refresh');
|
|
149
153
|
const mobileJobsBack=document.querySelector('#mobile-jobs-back');
|
|
150
154
|
const mobileJobs=window.matchMedia('(max-width:650px)');
|
|
@@ -176,9 +180,12 @@ const runtimeSummary=document.querySelector('#runtime-summary');
|
|
|
176
180
|
let jobs=[];
|
|
177
181
|
let selectedJob=sessionStorage.getItem('agents-relay:selected-job');
|
|
178
182
|
let selectedTaskId=sessionStorage.getItem('agents-relay:selected-task');
|
|
179
|
-
let
|
|
180
|
-
let
|
|
181
|
-
let
|
|
183
|
+
let selectedRepos=storedValues('agents-relay:repo-filter');
|
|
184
|
+
let selectedStatuses=sessionStorage.getItem('agents-relay:status-filter')===null?[...${JSON.stringify(DEFAULT_DASHBOARD_JOB_STATUSES)}]:storedValues('agents-relay:status-filter');
|
|
185
|
+
let jobsPage=1;
|
|
186
|
+
let jobsTotal=0;
|
|
187
|
+
let jobsTotalPages=1;
|
|
188
|
+
let jobRepositories=[];
|
|
182
189
|
let searchQuery=sessionStorage.getItem('agents-relay:job-search')||'';
|
|
183
190
|
let workflowGraphs=[];
|
|
184
191
|
let selectedGraphId='entry';
|
|
@@ -188,6 +195,7 @@ const collapsedTasks=new Set();
|
|
|
188
195
|
const expandedTasks=new Set();
|
|
189
196
|
const displayedEventIds=new Set();
|
|
190
197
|
function value(input){return input===null||input===undefined||input===''?'—':String(input);}
|
|
198
|
+
function storedValues(key){const stored=sessionStorage.getItem(key);if(!stored)return [];try{const parsed=JSON.parse(stored);if(Array.isArray(parsed))return parsed.filter(value=>typeof value==='string'&&value);if(typeof parsed==='string'&&parsed)return [parsed];}catch{return [stored];}return [];}
|
|
191
199
|
function text(input){return document.createTextNode(value(input));}
|
|
192
200
|
function element(name,className,child){const node=document.createElement(name);if(className)node.className=className;if(child)node.append(child);return node;}
|
|
193
201
|
function yamlScalar(input){if(input===null)return 'null';if(typeof input==='boolean'||typeof input==='number')return String(input);return JSON.stringify(String(input));}
|
|
@@ -214,25 +222,22 @@ function clearEvents(){displayedEventIds.clear();eventFeed.replaceChildren(eleme
|
|
|
214
222
|
async function loadEventHistory(){const job=selected();if(!job){clearEvents();return;}const params=new URLSearchParams({job:job.id,limit:String(MAX_EVENTS)});if(eventTaskId)params.set('task',eventTaskId);try{const history=await loadJSON('/api/events?'+params.toString());clearEvents();for(const event of Array.isArray(history)?history.slice().reverse():[])addEvent(event);}catch{clearEvents();}}
|
|
215
223
|
function copySelectedJobIdentity(job){if(!job||!navigator.clipboard?.writeText)return;void navigator.clipboard.writeText(repoName(job.repository)+'/'+value(job.id)).catch(()=>{});}
|
|
216
224
|
function selectJob(job){selectedJob=jobSelectionKey(job);selectedTaskId=null;eventTaskId=null;rememberSelection();clearEvents();location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';copySelectedJobIdentity(job);renderAll();void loadEventHistory();}
|
|
217
|
-
function matchesQuickFilter(job){
|
|
225
|
+
function matchesQuickFilter(job){const statuses=selectedStatuses.length?selectedStatuses:${JSON.stringify(DEFAULT_DASHBOARD_JOB_STATUSES)};return statuses.some(status=>status==='DRAFT'?job.draft||job.state==='DRAFT':job.state===status);}
|
|
218
226
|
function matchesSearch(job){const query=searchQuery.trim().toLowerCase();if(!query)return true;const pr=String(job.prNumber||'');const normalized=query.replace(/^pr\s*#?/,'').replace(/^#/,'');return String(job.title||job.id||'').toLowerCase().includes(query)||String(job.id||'').toLowerCase().includes(query)||shortManagedId(job.id,job.repository,'j').toLowerCase().includes(query)||pr.includes(normalized);}
|
|
219
|
-
function filteredJobs(){return jobs.filter(job=>(!
|
|
220
|
-
function setQuickMode(mode,status=''){quickMode=mode;selectedRuntimeStatus=mode==='status'?status:'';sessionStorage.setItem('agents-relay:quick-filter',quickMode);if(selectedRuntimeStatus)sessionStorage.setItem('agents-relay:runtime-status-filter',selectedRuntimeStatus);else sessionStorage.removeItem('agents-relay:runtime-status-filter');syncFilters();renderJobList();}
|
|
221
|
-
function toggleQuickMode(mode){setQuickMode(quickMode===mode?'jobs':mode);}
|
|
227
|
+
function filteredJobs(){return jobs.filter(job=>(!selectedRepos.length||selectedRepos.includes(job.repository))&&matchesQuickFilter(job)&&matchesSearch(job));}
|
|
222
228
|
function syncFilters(){
|
|
223
|
-
const repos=[...new Set(jobs.map(job=>job.repository).filter(Boolean))].sort();
|
|
224
|
-
const
|
|
225
|
-
repoFilter.replaceChildren(
|
|
226
|
-
|
|
227
|
-
repoFilter.value
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
selectedRuntimeStatus=statuses.includes(previousStatus)?previousStatus:'';
|
|
232
|
-
if(quickMode==='status'&&!selectedRuntimeStatus)quickMode='jobs';
|
|
233
|
-
quickStatus.value=quickMode==='status'?selectedRuntimeStatus:'';
|
|
234
|
-
quickDraft.setAttribute('aria-pressed',String(quickMode==='draft'));
|
|
229
|
+
const repos=jobRepositories.length?jobRepositories:[...new Set(jobs.map(job=>job.repository).filter(Boolean))].sort();
|
|
230
|
+
const validRepos=new Set(repos);
|
|
231
|
+
repoFilter.replaceChildren(...repos.map(repo=>new Option(repo,repo)));
|
|
232
|
+
selectedRepos=selectedRepos.filter(repo=>validRepos.has(repo));
|
|
233
|
+
for(const option of repoFilter.options)option.selected=selectedRepos.includes(option.value);
|
|
234
|
+
const validStatuses=new Set([...statusFilter.options].map(option=>option.value));
|
|
235
|
+
selectedStatuses=selectedStatuses.filter(status=>validStatuses.has(status));
|
|
236
|
+
for(const option of statusFilter.options)option.selected=selectedStatuses.includes(option.value);
|
|
235
237
|
jobSearch.value=searchQuery;
|
|
238
|
+
jobsPageLabel.textContent='Page '+jobsPage+' of '+jobsTotalPages+(jobsTotal?' · '+jobsTotal+' jobs':'');
|
|
239
|
+
jobsPrev.disabled=jobsPage<=1;
|
|
240
|
+
jobsNext.disabled=jobsPage>=jobsTotalPages;
|
|
236
241
|
}
|
|
237
242
|
function repoName(repository){if(typeof repository!=='string'||!repository)return 'unknown-repo';const parts=repository.split('/').filter(Boolean);return parts.at(-1)||repository;}
|
|
238
243
|
const CHAT_SHORTCUTS_KEY='agents-relay:chatgpt-shortcuts';
|
|
@@ -247,7 +252,7 @@ function jobChatRef(job){const pr=Number(job?.prNumber||0);return repoName(job?.
|
|
|
247
252
|
function chatShortcutUrl(prompt){return 'https://chatgpt.com/?prompt='+encodeURIComponent(prompt);}
|
|
248
253
|
const CHAT_SHORTCUT_TARGET='agents-relay-shortcut';
|
|
249
254
|
function chatShortcutField(job){const box=element('div','detail-group');box.append(element('p','detail-label',text('ChatGPT shortcut')));const select=document.createElement('select');select.className='job-filter job-chat-shortcut-select';select.setAttribute('aria-label','ChatGPT shortcut');select.append(new Option('',''));for(const item of parseChatShortcuts(chatShortcutText()))select.append(new Option(item.name,item.prompt));select.value='';select.addEventListener('change',()=>{if(!select.value)return;const prompt=select.value.replaceAll('%1',jobChatRef(job));window.open(chatShortcutUrl(prompt),CHAT_SHORTCUT_TARGET,'noopener');select.value='';});box.append(select);return box;}
|
|
250
|
-
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected
|
|
255
|
+
function renderJobList(){jobList.replaceChildren();const visible=filteredJobs();if(!jobs.length){jobList.append(element('li','empty',text('No durable jobs discovered.')));return;}if(!visible.length){jobList.append(element('li','empty',text('No jobs match the selected filters.')));return;}for(const job of visible){const row=element('div','job-row'+(isCompleteUnmerged(job)?' complete-unmerged':''));row.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));row.addEventListener('click',()=>selectJob(job));const content=element('div','job-content');const select=document.createElement('button');select.type='button';select.className='job-button';select.setAttribute('aria-current',String(jobSelectionKey(job)===selectedJob));select.title=job.title||job.id;select.addEventListener('click',event=>{event.stopPropagation();selectJob(job);});select.append(element('span','job-title',text(repoName(job.repository)+'/'+value(job.id)+(Number(job.prNumber||0)>0?'/PR#'+value(job.prNumber):'')+': '+value(job.title||job.id))));const states=element('span','job-status-line');states.append(element('span','badge '+value(job.state),text(value(job.state))),element('span','badge '+value(job.runtimeStatus||'IDLE'),text(value(job.runtimeStatus||'IDLE'))));if(job.publicationFailure)states.append(element('span','badge FAILED',text('PUBLISH FAILED: '+value(job.publicationFailure.stage))));else if(job.publication?.version)states.append(element('span','badge SUCCEEDED',text('v'+job.publication.version)));else if(Number(job.prNumber||0)>0&&job.githubState==='MERGED')states.append(element('span','badge MERGED',text('PR Merged')));else if(job.state==='COMPLETED'&&job.githubState==='CLOSED')states.append(element('span','badge SUCCEEDED',text('PR Archived')));if(job.state==='ACTIVE'&&job.reviewEnabled!==false)states.append(element('span','badge flag-review',text('REVIEW REQUIRED')));if(job.localMerge?.status==='failed')states.append(element('span','badge FAILED',text('MERGE FAILED: '+value(job.localMerge.error||'Unknown error'))));else if(isCompleteUnmerged(job))states.append(element('span','badge MERGE_PENDING',text('MERGE PENDING')));const primary=element('div','job-primary');primary.append(states);const lastLine=element('div','job-last-line');lastLine.append(element('span','subtle',text(compactCounts(counts(job.tasks)))),element('span','subtle',text(relativeTime(job.updatedAt))));content.append(select,primary,lastLine);row.append(content);jobList.append(element('li','',row));}}
|
|
251
256
|
function taskTagDisplay(job,task){const display=task.tag&&job?.tag?.values?.[task.tag];return typeof display==='string'&&display?display:null;}
|
|
252
257
|
function taskVerdict(job,task){if(task.state!=="SUCCEEDED")return null;const data=task.result?.data||{};if(task.agentName==="reviewer"){const verdict=data.verdict||((job?.review?.taskId===task.id)?job.review.outcome:null);return ({approved:"APPROVED",pass:"APPROVED","changes-required":"CHANGES REQUIRED",changes_required:"CHANGES REQUIRED"})[verdict]||null;}if(task.agentName==="troubleshooter"){const original=job?.tasks?.find(t=>t.id===task.parentTaskId);const resolution=original?.resolution;if(resolution?.state!=="resolved"||resolution.owner!=="troubleshooter")return null;return ({exact_original_retry:"RETRY VERIFIED",same_job_dependency:"DEPENDENCY REGISTERED",blocker_job_dependency:"BLOCKER REGISTERED",cancel_supersede:"SUPERSEDED"})[resolution.path]||null;}return null;}function taskSubtreeComplete(task,children){return ['SUCCEEDED','CANCELLED'].includes(task.state)&&(children.get(task.id)||[]).every(child=>taskSubtreeComplete(child,children));}
|
|
253
258
|
function taskStateLabel(task){const resolution=task?.resolution?.state;return task?.state==='FAILED'&&typeof resolution==='string'&&resolution.trim()?task.state+' / '+resolution:task.state;}
|
|
@@ -279,7 +284,10 @@ function renderRateLimitStatus(status){
|
|
|
279
284
|
function runtimePill(label,value){const pill=element('span','runtime-stat');pill.append(document.createTextNode(label+' '),element('strong','',text(value)));return pill;}
|
|
280
285
|
async function drawRuntimeSummary(){try{const summary=await loadJSON('/api/runtime-summary');runtimeSummary.replaceChildren(runtimePill('Active Jobs',summary.activeJobs||0),runtimePill('Running Tasks',summary.runningTasks||0),runtimePill('ChatGPT',summary.workers?.chatgpt||0),runtimePill('Codex',summary.workers?.codex||0));for(const item of Array.isArray(summary.models)?summary.models:[])runtimeSummary.append(runtimePill(item.model,item.count));}catch{runtimeSummary.replaceChildren();}}
|
|
281
286
|
async function refreshStatus(){try{renderRateLimitStatus(await loadJSON('/api/status'));}catch{renderRateLimitStatus(null);}}
|
|
282
|
-
|
|
287
|
+
function rememberJobFilters(){sessionStorage.setItem('agents-relay:repo-filter',JSON.stringify(selectedRepos));sessionStorage.setItem('agents-relay:status-filter',JSON.stringify(selectedStatuses));sessionStorage.setItem('agents-relay:job-search',searchQuery);}
|
|
288
|
+
function jobsQuery(force=false){const params=new URLSearchParams();if(searchQuery.trim())params.set('q',searchQuery.trim());for(const status of selectedStatuses)params.append('status',status);for(const repository of selectedRepos)params.append('repo',repository);params.set('page',String(jobsPage));params.set('pageSize',String(${DASHBOARD_JOB_PAGE_SIZE}));if(force)params.set('refresh','1');return params.toString();}
|
|
289
|
+
let jobsRequestSequence=0;
|
|
290
|
+
async function refresh(force=false){const requestSequence=++jobsRequestSequence;jobsRefresh.disabled=true;jobsRefresh.textContent='Refreshing…';jobsPrev.disabled=true;jobsNext.disabled=true;try{const params=jobsQuery(force);const discovered=await loadJSON('/api/jobs'+(params?'?'+params:''));if(requestSequence!==jobsRequestSequence)return;const result=Array.isArray(discovered)?{jobs:discovered,repositories:[...new Set(discovered.map(job=>job.repository).filter(Boolean))].sort(),total:discovered.length,page:1,totalPages:1}:discovered;jobs=dedupeJobs(Array.isArray(result.jobs)?result.jobs:[]);jobRepositories=result.repositories||[];jobsTotal=result.total||0;jobsPage=result.page||1;jobsTotalPages=result.totalPages||1;void drawRuntimeSummary();jobs.sort(compareJobs);if(selectedJob&&!jobForSelection(selectedJob)){selectedJob=null;selectedTaskId=null;}if(!selectedJob){const match=location.hash.match(/^#\\/jobs\\/(.+)$/);const fromHash=match?decodeURIComponent(match[1]):null;const fromRoute=jobForSelection(fromHash);selectedJob=jobSelectionKey(fromRoute||jobs[0]);}rememberSelection();rememberJobFilters();renderAll();void loadEventHistory();connection.textContent='';connection.setAttribute('aria-label','Live durable state connected');connection.className='status-pill ok';}catch(error){if(requestSequence!==jobsRequestSequence)return;connection.textContent='';connection.setAttribute('aria-label','Durable state unavailable');connection.className='status-pill bad';jobList.replaceChildren(element('li','error-state',text('Could not load jobs: '+error.message)));}finally{if(requestSequence===jobsRequestSequence){jobsRefresh.disabled=false;jobsRefresh.textContent='Refresh';syncFilters();}}}
|
|
283
291
|
function renderEventScope(){const task=eventTaskId&&selected()?.tasks?.find(candidate=>candidate.id===eventTaskId);eventsAllTasks.disabled=!task;eventsAllTasks.setAttribute('aria-pressed',String(!task));eventsScope.textContent=task?'Task '+task.id+' · newest first · 60 max':'Job-wide · newest first · 60 max';}
|
|
284
292
|
function eventLabel(type){return ({'task.process.launched':'Process launched','task.started':'Worker started','task.process.exited':'Sync process exited','task.process.async_exited':'Async process exited','task.completed':'Worker completed','task.failed':'Worker failed','task.blocked':'Worker blocked','review.started':'Review started','review.approved':'Review approved','review.changes_required':'Review changes required','review.blocked':'Review blocked'})[type]||type||'Event';}
|
|
285
293
|
function eventSeverity(event,level){const type=String(event?.type||'').toLowerCase(),eventLevel=String(event?.level||level||'').toLowerCase(),outcome=String(event?.outcome||event?.status||'').toLowerCase();if(eventLevel==='error'||outcome==='failed'||type.endsWith('.failed')||type==='task.failed')return 'error';if(eventLevel==='warn'||outcome==='blocked'||type.endsWith('.blocked')||type==='task.blocked'||type==='review.blocked')return 'warn';return 'normal';}
|
|
@@ -337,11 +345,13 @@ async function saveQa(){qaSave.disabled=true;qaStatus.textContent='Saving…';tr
|
|
|
337
345
|
function showView(view){const isJobs=view==='jobs';const isUsage=view==='usage';const isGraphs=view==='graphs';const isRules=view==='rules';const isShortcuts=view==='shortcuts';const isQa=view==='qa';qaView.hidden=!isQa;qaTab.setAttribute('aria-selected',String(isQa));jobsView.hidden=!isJobs;jobsWorkspace.hidden=!isJobs;usageView.hidden=!isUsage;graphsView.hidden=!isGraphs;rulesView.hidden=!isRules;shortcutsView.hidden=!isShortcuts;jobsTab.setAttribute('aria-selected',String(isJobs));usageTab.setAttribute('aria-selected',String(isUsage));graphsTab.setAttribute('aria-selected',String(isGraphs));rulesTab.setAttribute('aria-selected',String(isRules));shortcutsTab.setAttribute('aria-selected',String(isShortcuts));if(isJobs)void refresh();else if(isUsage)void drawUsage();else if(isGraphs)void drawGraphs();else if(isRules)void drawRules();else if(isQa)void drawQa();else void drawShortcuts();}
|
|
338
346
|
jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';showView('jobs');});
|
|
339
347
|
jobsRefresh.addEventListener('click',()=>void refresh(true));
|
|
348
|
+
jobsReset.addEventListener('click',()=>{searchQuery='';selectedStatuses=[...${JSON.stringify(DEFAULT_DASHBOARD_JOB_STATUSES)}];selectedRepos=[];jobsPage=1;void refresh(false);});
|
|
349
|
+
jobsPrev.addEventListener('click',()=>{if(jobsPage>1){jobsPage-=1;void refresh(false);}});
|
|
350
|
+
jobsNext.addEventListener('click',()=>{if(jobsPage<jobsTotalPages){jobsPage+=1;void refresh(false);}});
|
|
340
351
|
eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
|
|
341
|
-
repoFilter.addEventListener('change',()=>{
|
|
342
|
-
jobSearch.addEventListener('input',()=>{searchQuery=jobSearch.value;
|
|
343
|
-
|
|
344
|
-
quickStatus.addEventListener('change',()=>setQuickMode(quickStatus.value?'status':'jobs',quickStatus.value));
|
|
352
|
+
repoFilter.addEventListener('change',()=>{selectedRepos=[...repoFilter.selectedOptions].map(option=>option.value);jobsPage=1;void refresh(false);});
|
|
353
|
+
jobSearch.addEventListener('input',()=>{searchQuery=jobSearch.value;jobsPage=1;void refresh(false);});
|
|
354
|
+
statusFilter.addEventListener('change',()=>{selectedStatuses=[...statusFilter.selectedOptions].map(option=>option.value);jobsPage=1;void refresh(false);});
|
|
345
355
|
selectedJobRefresh.addEventListener('click',()=>void refreshSelected());
|
|
346
356
|
mobileJobsBack.addEventListener('click',()=>{location.hash='#/jobs';syncMobileJobsView();window.scrollTo({top:0,behavior:'instant'});});
|
|
347
357
|
mobileJobs.addEventListener('change',syncMobileJobsView);
|
package/dist/dashboard-style.js
CHANGED
|
@@ -24,6 +24,8 @@ export const dashboardStyles = ` <style>
|
|
|
24
24
|
.panel-actions { display:flex; align-items:center; gap:.45rem; }
|
|
25
25
|
.job-filters { display:flex; gap:.35rem; align-items:center; flex-wrap:wrap; }
|
|
26
26
|
.job-filter { max-width:145px; border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--text); padding:.27rem .4rem; font:inherit; font-size:.72rem; }
|
|
27
|
+
.job-filter[multiple] { height:5.6rem; }
|
|
28
|
+
.jobs-pagination { display:flex; gap:.3rem; align-items:center; }
|
|
27
29
|
.job-search { width:95px; max-width:14vw; border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--text); padding:.27rem .45rem; font:inherit; font-size:.72rem; }
|
|
28
30
|
.quick-filter { border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--muted); padding:.27rem .42rem; font:inherit; font-size:.7rem; font-weight:750; cursor:pointer; }
|
|
29
31
|
.quick-filter[aria-pressed="true"] { border-color:var(--accent); background:var(--accent2); color:var(--accent); }
|
|
@@ -147,9 +149,8 @@ export const dashboardStyles = ` <style>
|
|
|
147
149
|
#jobs-view .panel-actions { width:100%; flex-wrap:wrap; }
|
|
148
150
|
.runtime-summary { display:none; }
|
|
149
151
|
#jobs-view .job-filters { width:auto; }
|
|
150
|
-
#jobs-view .job-search
|
|
151
|
-
#jobs-view #
|
|
152
|
-
#jobs-view #quick-draft { display:none; }
|
|
152
|
+
#jobs-view .job-search { display:none; }
|
|
153
|
+
#jobs-view #status-filter { display:block; max-width:145px; }
|
|
153
154
|
#jobs-view .panel-body { max-height:none; }
|
|
154
155
|
#jobs-workspace { gap:.65rem; }
|
|
155
156
|
#jobs-workspace .panel-body { max-height:none; }
|
package/dist/dashboard.js
CHANGED
|
@@ -7,8 +7,8 @@ import { deriveJobRuntimeStatus } from './runtime-status.js';
|
|
|
7
7
|
import { DeliveryDeduper, parseGitHubWebhook, verifyGitHubWebhook } from './github-webhook.js';
|
|
8
8
|
import { loadExecutionRouterRules, saveExecutionRouterRules } from './routing-policy.js';
|
|
9
9
|
import { summarizeRuntime } from './runtime-summary.js';
|
|
10
|
-
import { dashboardWorkflowGraphs, effectiveGitHubUsage, eventHistory } from './dashboard-data.js';
|
|
11
|
-
export { dashboardWorkflowGraphs, githubPrUrl } from './dashboard-data.js';
|
|
10
|
+
import { dashboardWorkflowGraphs, effectiveGitHubUsage, eventHistory, searchDashboardJobs } from './dashboard-data.js';
|
|
11
|
+
export { dashboardWorkflowGraphs, githubPrUrl, searchDashboardJobs } from './dashboard-data.js';
|
|
12
12
|
const SSE_HEARTBEAT_MS = 15000;
|
|
13
13
|
function isTaskId(candidate) {
|
|
14
14
|
return candidate.length > 0;
|
|
@@ -34,6 +34,8 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
34
34
|
return jobs;
|
|
35
35
|
}
|
|
36
36
|
catch (error) {
|
|
37
|
+
if (forceRefresh)
|
|
38
|
+
throw error;
|
|
37
39
|
if (lastKnownJobs)
|
|
38
40
|
return lastKnownJobs;
|
|
39
41
|
if (jobId) {
|
|
@@ -204,12 +206,15 @@ export function serveDashboard(store, port = 8787, bus, refreshMs = 300000, jobI
|
|
|
204
206
|
if (new URL(req.url ?? '/', 'http://127.0.0.1').pathname === '/api/jobs') {
|
|
205
207
|
try {
|
|
206
208
|
await subscribeDiscovered();
|
|
207
|
-
const
|
|
209
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
210
|
+
const forceRefresh = url.searchParams.get('refresh') === '1';
|
|
208
211
|
const jobs = await discoveredJobs(forceRefresh);
|
|
212
|
+
const usesSearchContract = ['q', 'status', 'statuses', 'repo', 'repositories', 'repos', 'page', 'pageSize'].some(name => url.searchParams.has(name));
|
|
213
|
+
const body = usesSearchContract ? searchDashboardJobs(jobs, url.searchParams) : jobs;
|
|
209
214
|
if (!res.headersSent)
|
|
210
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
215
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
211
216
|
if (!res.writableEnded)
|
|
212
|
-
res.end(JSON.stringify(
|
|
217
|
+
res.end(JSON.stringify(body));
|
|
213
218
|
}
|
|
214
219
|
catch (error) {
|
|
215
220
|
if (!res.headersSent)
|