agents-relay 2.0.177 → 2.0.179
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 +59 -28
- package/dist/dashboard-style.js +30 -9
- 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"><
|
|
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"><div class="filter-dropdown"><button id="status-filter" class="filter-trigger" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="status-filter-menu"><span class="filter-label">Status</span><span id="status-filter-summary" class="filter-value">Default</span></button><div id="status-filter-menu" class="filter-menu" role="group" aria-label="Status options" hidden><label class="filter-option"><input type="checkbox" name="status" value="DRAFT"><span>DRAFT</span></label><label class="filter-option"><input type="checkbox" name="status" value="ACTIVE"><span>ACTIVE</span></label><label class="filter-option"><input type="checkbox" name="status" value="COMPLETED"><span>COMPLETED</span></label><label class="filter-option"><input type="checkbox" name="status" value="CANCELLED"><span>CANCELLED</span></label></div></div><div class="filter-dropdown"><button id="repo-filter" class="filter-trigger" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="repo-filter-menu"><span class="filter-label">Repositories</span><span id="repo-filter-summary" class="filter-value">All</span></button><div id="repo-filter-menu" class="filter-menu" role="group" aria-label="Repository options" hidden></div></div></div><div class="jobs-actions"><button id="jobs-reset" class="refresh-button" type="button">Reset</button><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></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,17 @@ 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
|
|
151
|
+
const repoFilterMenu=document.querySelector('#repo-filter-menu');
|
|
152
|
+
const repoFilterSummary=document.querySelector('#repo-filter-summary');
|
|
153
|
+
const statusFilter=document.querySelector('#status-filter');
|
|
154
|
+
const statusFilterMenu=document.querySelector('#status-filter-menu');
|
|
155
|
+
const statusFilterSummary=document.querySelector('#status-filter-summary');
|
|
148
156
|
const selectedJobRefresh=document.querySelector('#selected-job-refresh');
|
|
149
157
|
const mobileJobsBack=document.querySelector('#mobile-jobs-back');
|
|
150
158
|
const mobileJobs=window.matchMedia('(max-width:650px)');
|
|
@@ -176,9 +184,12 @@ const runtimeSummary=document.querySelector('#runtime-summary');
|
|
|
176
184
|
let jobs=[];
|
|
177
185
|
let selectedJob=sessionStorage.getItem('agents-relay:selected-job');
|
|
178
186
|
let selectedTaskId=sessionStorage.getItem('agents-relay:selected-task');
|
|
179
|
-
let
|
|
180
|
-
let
|
|
181
|
-
let
|
|
187
|
+
let selectedRepos=storedValues('agents-relay:repo-filter');
|
|
188
|
+
let selectedStatuses=sessionStorage.getItem('agents-relay:status-filter')===null?[...${JSON.stringify(DEFAULT_DASHBOARD_JOB_STATUSES)}]:storedValues('agents-relay:status-filter');
|
|
189
|
+
let jobsPage=1;
|
|
190
|
+
let jobsTotal=0;
|
|
191
|
+
let jobsTotalPages=1;
|
|
192
|
+
let jobRepositories=[];
|
|
182
193
|
let searchQuery=sessionStorage.getItem('agents-relay:job-search')||'';
|
|
183
194
|
let workflowGraphs=[];
|
|
184
195
|
let selectedGraphId='entry';
|
|
@@ -188,6 +199,7 @@ const collapsedTasks=new Set();
|
|
|
188
199
|
const expandedTasks=new Set();
|
|
189
200
|
const displayedEventIds=new Set();
|
|
190
201
|
function value(input){return input===null||input===undefined||input===''?'—':String(input);}
|
|
202
|
+
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
203
|
function text(input){return document.createTextNode(value(input));}
|
|
192
204
|
function element(name,className,child){const node=document.createElement(name);if(className)node.className=className;if(child)node.append(child);return node;}
|
|
193
205
|
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 +226,35 @@ function clearEvents(){displayedEventIds.clear();eventFeed.replaceChildren(eleme
|
|
|
214
226
|
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
227
|
function copySelectedJobIdentity(job){if(!job||!navigator.clipboard?.writeText)return;void navigator.clipboard.writeText(repoName(job.repository)+'/'+value(job.id)).catch(()=>{});}
|
|
216
228
|
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){
|
|
229
|
+
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
230
|
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
|
|
221
|
-
function
|
|
231
|
+
function filteredJobs(){return jobs.filter(job=>(!selectedRepos.length||selectedRepos.includes(job.repository))&&matchesQuickFilter(job)&&matchesSearch(job));}
|
|
232
|
+
function selectedNames(inputs,selected){const selectedSet=new Set(selected);return [...inputs].filter(input=>selectedSet.has(input.value)).map(input=>input.value);}
|
|
233
|
+
function syncFilterSummary(trigger,summary,inputs,selected,emptyLabel){const names=selectedNames(inputs,selected);summary.textContent=names.length===0?emptyLabel:names.length<=2?names.join(', '):names.length+' selected';trigger.title=names.length===0?emptyLabel:names.join(', ');}
|
|
234
|
+
function closeFilterMenus(exceptMenu){for(const [trigger,menu] of [[statusFilter,statusFilterMenu],[repoFilter,repoFilterMenu]]){if(menu===exceptMenu)continue;menu.hidden=true;trigger.setAttribute('aria-expanded','false');}}
|
|
235
|
+
function toggleFilterMenu(trigger,menu){const open=menu.hidden;closeFilterMenus(open?menu:null);menu.hidden=!open;trigger.setAttribute('aria-expanded',String(open));}
|
|
222
236
|
function syncFilters(){
|
|
223
|
-
const repos=[...new Set(jobs.map(job=>job.repository).filter(Boolean))].sort();
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
237
|
+
const repos=jobRepositories.length?jobRepositories:[...new Set(jobs.map(job=>job.repository).filter(Boolean))].sort();
|
|
238
|
+
const validRepos=new Set(repos);
|
|
239
|
+
const existingInputs=[...repoFilterMenu.querySelectorAll('input')];
|
|
240
|
+
// Preserve checkbox nodes while their options are unchanged so keyboard focus survives selection.
|
|
241
|
+
if(existingInputs.length!==repos.length||existingInputs.some((input,index)=>input.value!==repos[index])){
|
|
242
|
+
const focusedRepo=repoFilterMenu.contains(document.activeElement)?document.activeElement.value:null;
|
|
243
|
+
repoFilterMenu.replaceChildren(...repos.map(repo=>{const label=document.createElement('label');label.className='filter-option';const input=document.createElement('input');input.type='checkbox';input.name='repo';input.value=repo;label.append(input,element('span','',text(repo)));return label;}));
|
|
244
|
+
if(focusedRepo)for(const input of repoFilterMenu.querySelectorAll('input'))if(input.value===focusedRepo){input.focus();break;}
|
|
245
|
+
}
|
|
246
|
+
selectedRepos=selectedRepos.filter(repo=>validRepos.has(repo));
|
|
247
|
+
for(const input of repoFilterMenu.querySelectorAll('input'))input.checked=selectedRepos.includes(input.value);
|
|
248
|
+
const statusInputs=[...statusFilterMenu.querySelectorAll('input')];
|
|
249
|
+
const validStatuses=new Set(statusInputs.map(input=>input.value));
|
|
250
|
+
selectedStatuses=selectedStatuses.filter(status=>validStatuses.has(status));
|
|
251
|
+
for(const input of statusInputs)input.checked=selectedStatuses.includes(input.value);
|
|
252
|
+
syncFilterSummary(statusFilter,statusFilterSummary,statusInputs,selectedStatuses,'Default');
|
|
253
|
+
syncFilterSummary(repoFilter,repoFilterSummary,[...repoFilterMenu.querySelectorAll('input')],selectedRepos,'All');
|
|
235
254
|
jobSearch.value=searchQuery;
|
|
255
|
+
jobsPageLabel.textContent='Page '+jobsPage+' of '+jobsTotalPages+(jobsTotal?' · '+jobsTotal+' jobs':'');
|
|
256
|
+
jobsPrev.disabled=jobsPage<=1;
|
|
257
|
+
jobsNext.disabled=jobsPage>=jobsTotalPages;
|
|
236
258
|
}
|
|
237
259
|
function repoName(repository){if(typeof repository!=='string'||!repository)return 'unknown-repo';const parts=repository.split('/').filter(Boolean);return parts.at(-1)||repository;}
|
|
238
260
|
const CHAT_SHORTCUTS_KEY='agents-relay:chatgpt-shortcuts';
|
|
@@ -247,7 +269,7 @@ function jobChatRef(job){const pr=Number(job?.prNumber||0);return repoName(job?.
|
|
|
247
269
|
function chatShortcutUrl(prompt){return 'https://chatgpt.com/?prompt='+encodeURIComponent(prompt);}
|
|
248
270
|
const CHAT_SHORTCUT_TARGET='agents-relay-shortcut';
|
|
249
271
|
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
|
|
272
|
+
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
273
|
function taskTagDisplay(job,task){const display=task.tag&&job?.tag?.values?.[task.tag];return typeof display==='string'&&display?display:null;}
|
|
252
274
|
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
275
|
function taskStateLabel(task){const resolution=task?.resolution?.state;return task?.state==='FAILED'&&typeof resolution==='string'&&resolution.trim()?task.state+' / '+resolution:task.state;}
|
|
@@ -279,7 +301,10 @@ function renderRateLimitStatus(status){
|
|
|
279
301
|
function runtimePill(label,value){const pill=element('span','runtime-stat');pill.append(document.createTextNode(label+' '),element('strong','',text(value)));return pill;}
|
|
280
302
|
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
303
|
async function refreshStatus(){try{renderRateLimitStatus(await loadJSON('/api/status'));}catch{renderRateLimitStatus(null);}}
|
|
282
|
-
|
|
304
|
+
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);}
|
|
305
|
+
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();}
|
|
306
|
+
let jobsRequestSequence=0;
|
|
307
|
+
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
308
|
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
309
|
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
310
|
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 +362,17 @@ async function saveQa(){qaSave.disabled=true;qaStatus.textContent='Saving…';tr
|
|
|
337
362
|
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
363
|
jobsTab.addEventListener('click',()=>{location.hash=selectedJob?'#/jobs/'+encodeURIComponent(selectedJob):'#/jobs';showView('jobs');});
|
|
339
364
|
jobsRefresh.addEventListener('click',()=>void refresh(true));
|
|
365
|
+
jobsReset.addEventListener('click',()=>{searchQuery='';selectedStatuses=[...${JSON.stringify(DEFAULT_DASHBOARD_JOB_STATUSES)}];selectedRepos=[];jobsPage=1;closeFilterMenus();void refresh(false);});
|
|
366
|
+
jobsPrev.addEventListener('click',()=>{if(jobsPage>1){jobsPage-=1;void refresh(false);}});
|
|
367
|
+
jobsNext.addEventListener('click',()=>{if(jobsPage<jobsTotalPages){jobsPage+=1;void refresh(false);}});
|
|
340
368
|
eventsAllTasks.addEventListener('click',()=>{eventTaskId=null;clearEvents();renderEventScope();void loadEventHistory();});
|
|
341
|
-
repoFilter.addEventListener('
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
369
|
+
repoFilter.addEventListener('click',()=>toggleFilterMenu(repoFilter,repoFilterMenu));
|
|
370
|
+
repoFilterMenu.addEventListener('change',()=>{selectedRepos=[...repoFilterMenu.querySelectorAll('input:checked')].map(input=>input.value);jobsPage=1;syncFilters();void refresh(false);});
|
|
371
|
+
jobSearch.addEventListener('input',()=>{searchQuery=jobSearch.value;jobsPage=1;void refresh(false);});
|
|
372
|
+
statusFilter.addEventListener('click',()=>toggleFilterMenu(statusFilter,statusFilterMenu));
|
|
373
|
+
statusFilterMenu.addEventListener('change',()=>{selectedStatuses=[...statusFilterMenu.querySelectorAll('input:checked')].map(input=>input.value);jobsPage=1;syncFilters();void refresh(false);});
|
|
374
|
+
document.addEventListener('click',event=>{const target=event.target instanceof Node?event.target:null;const inStatus=statusFilter.contains(target)||statusFilterMenu.contains(target);const inRepo=repoFilter.contains(target)||repoFilterMenu.contains(target);if(!inStatus&&!inRepo)closeFilterMenus(null);});
|
|
375
|
+
document.addEventListener('keydown',event=>{if(event.key!=='Escape')return;const active=document.activeElement;if(statusFilterMenu.hidden===false&&(statusFilterMenu.contains(active)||active===statusFilter)){statusFilterMenu.hidden=true;statusFilter.setAttribute('aria-expanded','false');statusFilter.focus();}if(repoFilterMenu.hidden===false&&(repoFilterMenu.contains(active)||active===repoFilter)){repoFilterMenu.hidden=true;repoFilter.setAttribute('aria-expanded','false');repoFilter.focus();}});
|
|
345
376
|
selectedJobRefresh.addEventListener('click',()=>void refreshSelected());
|
|
346
377
|
mobileJobsBack.addEventListener('click',()=>{location.hash='#/jobs';syncMobileJobsView();window.scrollTo({top:0,behavior:'instant'});});
|
|
347
378
|
mobileJobs.addEventListener('change',syncMobileJobsView);
|
package/dist/dashboard-style.js
CHANGED
|
@@ -21,10 +21,25 @@ export const dashboardStyles = ` <style>
|
|
|
21
21
|
.panel { min-width:0; background:var(--surface); border:1px solid var(--border); border-radius:.55rem; box-shadow:var(--shadow); }
|
|
22
22
|
.panel-head { display:flex; align-items:center; justify-content:space-between; gap:.6rem; padding:.65rem .75rem; border-bottom:1px solid var(--border); }
|
|
23
23
|
.panel-head h2,.panel-head h3 { margin:0; color:var(--muted); font-size:.75rem; letter-spacing:.07em; text-transform:uppercase; min-width:0; }\n #workspace-heading { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n #selected-job-title { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
24
|
-
.panel-actions { display:flex; align-items:center; gap:.
|
|
25
|
-
.
|
|
26
|
-
.job-
|
|
27
|
-
.
|
|
24
|
+
.panel-actions { display:flex; align-items:center; gap:.55rem; flex-wrap:wrap; min-width:0; }
|
|
25
|
+
#jobs-view .panel-head { display:grid; grid-template-columns:minmax(0,1fr); align-items:start; gap:.45rem; }
|
|
26
|
+
.job-filters,.jobs-actions { display:flex; align-items:center; gap:.4rem; flex-wrap:wrap; min-width:0; }
|
|
27
|
+
.jobs-actions { justify-content:flex-end; }
|
|
28
|
+
.filter-trigger,.refresh-button { min-height:2rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface); color:var(--muted); padding:.3rem .55rem; font:inherit; font-size:.72rem; font-weight:700; cursor:pointer; transition:border-color .15s ease,background .15s ease; }
|
|
29
|
+
.filter-trigger { display:inline-flex; align-items:center; gap:.4rem; max-width:17rem; }
|
|
30
|
+
.filter-trigger:hover,.refresh-button:hover { background:#ffffff10; color:var(--text); }
|
|
31
|
+
.filter-trigger:focus-visible,.refresh-button:focus-visible,.filter-option input:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
|
32
|
+
.filter-trigger[aria-expanded="true"] { border-color:var(--accent); color:var(--text); }
|
|
33
|
+
.filter-label { flex:0 0 auto; }
|
|
34
|
+
.filter-value { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--text); font-weight:600; }
|
|
35
|
+
.filter-dropdown { position:relative; min-width:0; }
|
|
36
|
+
.filter-menu { position:absolute; top:calc(100% + .3rem); left:0; z-index:30; width:max-content; min-width:11rem; max-width:calc(100vw - 1.5rem); max-height:17rem; overflow:auto; border:1px solid var(--border); border-radius:.55rem; background:var(--surface); box-shadow:0 12px 34px #00000026; padding:.3rem; }
|
|
37
|
+
.filter-option { display:grid; grid-template-columns:auto minmax(0,1fr); align-items:center; gap:.45rem; padding:.4rem .5rem; border-radius:.4rem; font-size:.74rem; font-weight:600; color:var(--text); cursor:pointer; }
|
|
38
|
+
.filter-option:hover { background:#ffffff10; }
|
|
39
|
+
.filter-option input { width:.95rem; height:.95rem; margin:0; accent-color:var(--accent); }
|
|
40
|
+
.jobs-pagination { display:flex; gap:.3rem; align-items:center; white-space:nowrap; }
|
|
41
|
+
.job-search { flex:1 1 10rem; width:12rem; min-width:0; max-width:20rem; border:1px solid var(--border); border-radius:.45rem; background:var(--surface); color:var(--text); padding:.3rem .5rem; font:inherit; font-size:.72rem; }
|
|
42
|
+
.job-search:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }
|
|
28
43
|
.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
44
|
.quick-filter[aria-pressed="true"] { border-color:var(--accent); background:var(--accent2); color:var(--accent); }
|
|
30
45
|
.refresh-button { border:1px solid var(--border); border-radius:.35rem; background:var(--surface); color:var(--muted); padding:.28rem .55rem; font:inherit; font-size:.72rem; font-weight:700; cursor:pointer; }
|
|
@@ -144,12 +159,18 @@ export const dashboardStyles = ` <style>
|
|
|
144
159
|
body[data-mobile-jobs-view="detail"] #jobs-view { display:none; }
|
|
145
160
|
.mobile-only { display:inline-flex; }
|
|
146
161
|
#jobs-view .panel-head { display:grid; align-items:start; }
|
|
147
|
-
#jobs-view .panel-actions { width:100%;
|
|
162
|
+
#jobs-view .panel-actions { display:grid; grid-template-columns:1fr; width:100%; gap:.5rem; }
|
|
148
163
|
.runtime-summary { display:none; }
|
|
149
|
-
#jobs-view .job-filters { width:
|
|
150
|
-
#jobs-view .job-search
|
|
151
|
-
#jobs-view
|
|
152
|
-
#jobs-view
|
|
164
|
+
#jobs-view .job-filters { width:100%; display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.4rem; }
|
|
165
|
+
#jobs-view .job-search { grid-column:1 / -1; width:100%; max-width:none; }
|
|
166
|
+
#jobs-view .filter-trigger { width:100%; max-width:none; min-height:2.5rem; }
|
|
167
|
+
#jobs-view .filter-trigger { display:grid; align-content:center; justify-items:start; gap:.1rem; }
|
|
168
|
+
#jobs-view .filter-label { font-size:.6rem; letter-spacing:.04em; text-transform:uppercase; color:var(--muted); }
|
|
169
|
+
#jobs-view .filter-value { overflow:visible; text-overflow:clip; white-space:normal; font-size:.72rem; line-height:1.2; }
|
|
170
|
+
#jobs-view .jobs-actions { width:100%; display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; }
|
|
171
|
+
#jobs-view .refresh-button { min-height:2.5rem; }
|
|
172
|
+
#jobs-view .jobs-pagination { min-width:0; justify-content:center; }
|
|
173
|
+
#jobs-view #jobs-page { overflow:hidden; text-overflow:ellipsis; }
|
|
153
174
|
#jobs-view .panel-body { max-height:none; }
|
|
154
175
|
#jobs-workspace { gap:.65rem; }
|
|
155
176
|
#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)
|