sync: auto-sync from HOWARD-HOME at 2026-07-17 20:12:39
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-07-17 20:12:39
This commit is contained in:
@@ -200,7 +200,20 @@ const SECTIONS = [
|
|||||||
{ id: 'gc-all', label: 'GuruConnect', repos: ['guru-connect'], filter: () => true },
|
{ id: 'gc-all', label: 'GuruConnect', repos: ['guru-connect'], filter: () => true },
|
||||||
{ id: 'ct-bugs', label: 'Harness / Skill Bugs', repos: ['claudetools'], filter: i => getType(i)==='bug' || getType(i)==='security' },
|
{ id: 'ct-bugs', label: 'Harness / Skill Bugs', repos: ['claudetools'], filter: i => getType(i)==='bug' || getType(i)==='security' },
|
||||||
{ id: 'ct-skills', label: 'Skill Work', repos: ['claudetools'], filter: i => getType(i)==='skill' },
|
{ id: 'ct-skills', label: 'Skill Work', repos: ['claudetools'], filter: i => getType(i)==='skill' },
|
||||||
{ id: 'ct-projects', label: 'Projects', repos: ['claudetools'], filter: i => getType(i)==='project' || getType(i)==='feature' },
|
{ id: 'ct-projects', label: 'Projects', repos: ['claudetools'], filter: i => (getType(i)==='project' || getType(i)==='feature') && !isClientProject(i) },
|
||||||
|
];
|
||||||
|
// Client project sections — built dynamically from labels
|
||||||
|
const CLIENT_SECTIONS = [
|
||||||
|
{ id: 'cp-all', label: 'All Client Projects', client: null },
|
||||||
|
{ id: 'cp-cascades', label: 'Cascades', client: 'client:cascades' },
|
||||||
|
{ id: 'cp-glaztech', label: 'GlazTech', client: 'client:glaztech' },
|
||||||
|
{ id: 'cp-sif', label: 'SIF / Oidak', client: 'client:sif-oidak' },
|
||||||
|
{ id: 'cp-goldstein', label: 'Goldstein', client: 'client:goldstein' },
|
||||||
|
{ id: 'cp-prairie', label: 'Prairie Schooner', client: 'client:prairie-schooner' },
|
||||||
|
{ id: 'cp-cryoweave', label: 'CryoWeave', client: 'client:cryoweave' },
|
||||||
|
{ id: 'cp-tedards', label: 'Tedards Law', client: 'client:tedards' },
|
||||||
|
{ id: 'cp-vwp', label: 'Valley Water Pumps', client: 'client:vwp' },
|
||||||
|
{ id: 'cp-acg', label: 'ACG Internal', client: 'client:acg' },
|
||||||
];
|
];
|
||||||
const ALL_REPOS = ['gururmm','guru-rmm','guru-connect','claudetools'];
|
const ALL_REPOS = ['gururmm','guru-rmm','guru-connect','claudetools'];
|
||||||
const REPO_NAMES = { 'gururmm':'GuruRMM', 'guru-rmm':'GuruRMM', 'guru-connect':'GuruConnect', 'claudetools':'ClaudeTools' };
|
const REPO_NAMES = { 'gururmm':'GuruRMM', 'guru-rmm':'GuruRMM', 'guru-connect':'GuruConnect', 'claudetools':'ClaudeTools' };
|
||||||
@@ -288,6 +301,15 @@ function getType(i) {
|
|||||||
for(const t of ['bug','feature','skill','project','security']) if(i.labels.some(l=>l.name===t)) return t;
|
for(const t of ['bug','feature','skill','project','security']) if(i.labels.some(l=>l.name===t)) return t;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function isClientProject(i) { return i.labels.some(l => l.name === 'client-project'); }
|
||||||
|
function getAssignee(i) {
|
||||||
|
const a = i.labels.find(l => l.name.startsWith('assigned:'));
|
||||||
|
return a ? a.name.split(':')[1] : null;
|
||||||
|
}
|
||||||
|
function getClient(i) {
|
||||||
|
const c = i.labels.find(l => l.name.startsWith('client:'));
|
||||||
|
return c ? c.name : null;
|
||||||
|
}
|
||||||
function getDiscovered(i) {
|
function getDiscovered(i) {
|
||||||
// Parse "**Discovered:** YYYY-MM-DD" from issue body
|
// Parse "**Discovered:** YYYY-MM-DD" from issue body
|
||||||
if (!i.body) return null;
|
if (!i.body) return null;
|
||||||
@@ -306,9 +328,19 @@ function tagStyle(l) { return `background:#${l.color}20;color:#${l.color};`; }
|
|||||||
|
|
||||||
function getFiltered() {
|
function getFiltered() {
|
||||||
let items = allIssues;
|
let items = allIssues;
|
||||||
if (currentSection !== 'all' && currentSection !== 'activity') {
|
if (currentSection.startsWith('cp-')) {
|
||||||
|
// Client project section
|
||||||
|
const cs = CLIENT_SECTIONS.find(s=>s.id===currentSection);
|
||||||
|
if (cs) {
|
||||||
|
items = items.filter(i => isClientProject(i));
|
||||||
|
if (cs.client) items = items.filter(i => i.labels.some(l => l.name === cs.client));
|
||||||
|
}
|
||||||
|
} else if (currentSection === 'all') {
|
||||||
|
// Exclude client projects from "all issues"
|
||||||
|
items = items.filter(i => !isClientProject(i));
|
||||||
|
} else if (currentSection !== 'activity') {
|
||||||
const sec = SECTIONS.find(s=>s.id===currentSection);
|
const sec = SECTIONS.find(s=>s.id===currentSection);
|
||||||
if (sec) items = items.filter(i => sec.repos.includes(i._repo) && sec.filter(i));
|
if (sec) items = items.filter(i => sec.repos.includes(i._repo) && sec.filter(i) && !isClientProject(i));
|
||||||
}
|
}
|
||||||
if (searchText) {
|
if (searchText) {
|
||||||
const q = searchText.toLowerCase();
|
const q = searchText.toLowerCase();
|
||||||
@@ -355,6 +387,27 @@ function renderSidebar() {
|
|||||||
${sec.label} <span class="cnt">${countFor(sec)}</span>
|
${sec.label} <span class="cnt">${countFor(sec)}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client projects section
|
||||||
|
const cpCount = (cs) => {
|
||||||
|
let items = allIssues.filter(i => i.state !== 'closed' && isClientProject(i));
|
||||||
|
if (cs && cs.client) items = items.filter(i => i.labels.some(l => l.name === cs.client));
|
||||||
|
return items.length;
|
||||||
|
};
|
||||||
|
const totalCp = cpCount(null);
|
||||||
|
html += `<div class="sidebar-divider"></div><div class="sidebar-section">Client Projects</div>`;
|
||||||
|
html += `<div class="sidebar-item ${currentSection==='cp-all'?'active':''}" onclick="setSection('cp-all')">
|
||||||
|
All Clients <span class="cnt">${totalCp}</span>
|
||||||
|
</div>`;
|
||||||
|
for (const cs of CLIENT_SECTIONS.filter(s=>s.client)) {
|
||||||
|
const cnt = cpCount(cs);
|
||||||
|
if (cnt > 0) {
|
||||||
|
html += `<div class="sidebar-item ${currentSection===cs.id?'active':''}" onclick="setSection('${cs.id}')">
|
||||||
|
${cs.label} <span class="cnt">${cnt}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sb.innerHTML = html;
|
sb.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,11 +460,14 @@ function renderStats(items) {
|
|||||||
|
|
||||||
function makeCard(i) {
|
function makeCard(i) {
|
||||||
const p=getPriority(i); const st=getStatus(i);
|
const p=getPriority(i); const st=getStatus(i);
|
||||||
const tags=i.labels.filter(l=>!l.name.match(/^P\d/)&&!['in-progress','blocked','dropped','fixed','verified','wontfix'].includes(l.name));
|
const tags=i.labels.filter(l=>!l.name.match(/^P\d/)&&!['in-progress','blocked','dropped','fixed','verified','wontfix','client-project'].includes(l.name)&&!l.name.startsWith('assigned:')&&!l.name.startsWith('client:'));
|
||||||
|
const assignee=getAssignee(i);
|
||||||
|
const isCp=isClientProject(i);
|
||||||
|
const clientLabel=getClient(i);
|
||||||
return `<div class="card" onclick="showDetail('${i._repo}',${i.number})">
|
return `<div class="card" onclick="showDetail('${i._repo}',${i.number})">
|
||||||
<div class="card-header"><span class="card-repo">${i._rname}</span><span class="card-id">#${i.number}</span></div>
|
<div class="card-header"><span class="card-repo">${isCp && clientLabel ? clientLabel.split(':')[1] : i._rname}</span><span class="card-id">#${i.number}</span></div>
|
||||||
<div class="card-title">${esc(i.title)}</div>
|
<div class="card-title">${esc(i.title)}</div>
|
||||||
<div class="card-labels">${tags.map(l=>`<span class="tag" style="${tagStyle(l)}">${esc(l.name)}</span>`).join('')}</div>
|
<div class="card-labels">${tags.map(l=>`<span class="tag" style="${tagStyle(l)}">${esc(l.name)}</span>`).join('')}${assignee?`<span class="tag" style="background:rgba(191,218,220,0.2);color:#bfdadc">${assignee}</span>`:''}</div>
|
||||||
<div class="card-foot">
|
<div class="card-foot">
|
||||||
<span>${p?`<span class="pdot pdot-${p}"></span>${p}`:''}</span>
|
<span>${p?`<span class="pdot pdot-${p}"></span>${p}`:''}</span>
|
||||||
<span>${timeAgo(getDiscovered(i)||i.created_at)} old · upd ${timeAgo(i.updated_at)}</span>
|
<span>${timeAgo(getDiscovered(i)||i.created_at)} old · upd ${timeAgo(i.updated_at)}</span>
|
||||||
@@ -591,6 +647,7 @@ function showNewIssue() {
|
|||||||
<option value="skill">Skill</option>
|
<option value="skill">Skill</option>
|
||||||
<option value="project">Project</option>
|
<option value="project">Project</option>
|
||||||
<option value="security">Security</option>
|
<option value="security">Security</option>
|
||||||
|
<option value="client-project">Client Project</option>
|
||||||
</select></div>
|
</select></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="frow">
|
<div class="frow">
|
||||||
@@ -609,6 +666,30 @@ function showNewIssue() {
|
|||||||
<option value="component:harness">Harness / Skills</option>
|
<option value="component:harness">Harness / Skills</option>
|
||||||
</select></div>
|
</select></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="frow" id="ni-client-row" style="display:none">
|
||||||
|
<div class="fg"><label>Client</label><select id="ni-client">
|
||||||
|
<option value="">(select client)</option>
|
||||||
|
<option value="client:cascades">Cascades of Tucson</option>
|
||||||
|
<option value="client:glaztech">GlazTech</option>
|
||||||
|
<option value="client:sif-oidak">SIF / Oidak</option>
|
||||||
|
<option value="client:goldstein">Goldstein</option>
|
||||||
|
<option value="client:prairie-schooner">Prairie Schooner</option>
|
||||||
|
<option value="client:cryoweave">CryoWeave</option>
|
||||||
|
<option value="client:tedards">Tedards Law</option>
|
||||||
|
<option value="client:vwp">Valley Water Pumps</option>
|
||||||
|
<option value="client:acg">ACG Internal</option>
|
||||||
|
<option value="client:attrebesch">Attrebesch</option>
|
||||||
|
<option value="client:birth-biologic">Birth Biologic</option>
|
||||||
|
<option value="client:desert-rv">Desert RV Center</option>
|
||||||
|
<option value="client:deere-park">Deere Park</option>
|
||||||
|
<option value="client:mineralogical">Mineralogical Record</option>
|
||||||
|
</select></div>
|
||||||
|
<div class="fg"><label>Assigned To</label><select id="ni-assign">
|
||||||
|
<option value="">(unassigned)</option>
|
||||||
|
<option value="assigned:howard">Howard</option>
|
||||||
|
<option value="assigned:mike">Mike</option>
|
||||||
|
</select></div>
|
||||||
|
</div>
|
||||||
<div class="fg"><label>Title</label><input type="text" id="ni-title" placeholder="Short description"></div>
|
<div class="fg"><label>Title</label><input type="text" id="ni-title" placeholder="Short description"></div>
|
||||||
<div class="fg"><label>Description</label><textarea id="ni-body" placeholder="What happened? Steps to reproduce? Context?"></textarea></div>
|
<div class="fg"><label>Description</label><textarea id="ni-body" placeholder="What happened? Steps to reproduce? Context?"></textarea></div>
|
||||||
<div class="factions">
|
<div class="factions">
|
||||||
@@ -619,6 +700,18 @@ function showNewIssue() {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
document.getElementById('new-overlay').classList.add('open');
|
document.getElementById('new-overlay').classList.add('open');
|
||||||
|
// Show/hide client fields based on type
|
||||||
|
const typeEl = document.getElementById('ni-type');
|
||||||
|
const clientRow = document.getElementById('ni-client-row');
|
||||||
|
const toggleClient = () => { clientRow.style.display = typeEl.value === 'client-project' ? 'flex' : 'none'; };
|
||||||
|
typeEl.onchange = toggleClient;
|
||||||
|
// Pre-select client-project if we're in a client section
|
||||||
|
if (currentSection.startsWith('cp-')) {
|
||||||
|
typeEl.value = 'client-project';
|
||||||
|
toggleClient();
|
||||||
|
const cs = CLIENT_SECTIONS.find(s=>s.id===currentSection);
|
||||||
|
if (cs && cs.client) document.getElementById('ni-client').value = cs.client;
|
||||||
|
}
|
||||||
setTimeout(()=>document.getElementById('ni-title').focus(), 100);
|
setTimeout(()=>document.getElementById('ni-title').focus(), 100);
|
||||||
}
|
}
|
||||||
function closeNew() { document.getElementById('new-overlay').classList.remove('open'); }
|
function closeNew() { document.getElementById('new-overlay').classList.remove('open'); }
|
||||||
@@ -630,11 +723,16 @@ async function submitNew() {
|
|||||||
const type=document.getElementById('ni-type').value;
|
const type=document.getElementById('ni-type').value;
|
||||||
const prio=document.getElementById('ni-prio').value;
|
const prio=document.getElementById('ni-prio').value;
|
||||||
const comp=document.getElementById('ni-comp').value;
|
const comp=document.getElementById('ni-comp').value;
|
||||||
|
const client=document.getElementById('ni-client').value;
|
||||||
|
const assign=document.getElementById('ni-assign').value;
|
||||||
const err=document.getElementById('ni-err');
|
const err=document.getElementById('ni-err');
|
||||||
if(!title) { err.textContent='Title required'; err.style.display='block'; return; }
|
if(!title) { err.textContent='Title required'; err.style.display='block'; return; }
|
||||||
|
if(type==='client-project'&&!client) { err.textContent='Select a client'; err.style.display='block'; return; }
|
||||||
const lc=labelCache[repo]||{};
|
const lc=labelCache[repo]||{};
|
||||||
const ids=[lc[type],lc[prio]].filter(Boolean);
|
const ids=[lc[type],lc[prio]].filter(Boolean);
|
||||||
if(comp&&lc[comp]) ids.push(lc[comp]);
|
if(comp&&lc[comp]) ids.push(lc[comp]);
|
||||||
|
if(client&&lc[client]) ids.push(lc[client]);
|
||||||
|
if(assign&&lc[assign]) ids.push(lc[assign]);
|
||||||
try {
|
try {
|
||||||
const r=await fetch(`${API}/repos/${OWNER}/${repo}/issues`, {method:'POST', headers:{...headers(),'Content-Type':'application/json'}, body:JSON.stringify({title,body,labels:ids})});
|
const r=await fetch(`${API}/repos/${OWNER}/${repo}/issues`, {method:'POST', headers:{...headers(),'Content-Type':'application/json'}, body:JSON.stringify({title,body,labels:ids})});
|
||||||
if(!r.ok) { const e=await r.json(); err.textContent=e.message||'Failed'; err.style.display='block'; return; }
|
if(!r.ok) { const e=await r.json(); err.textContent=e.message||'Failed'; err.style.display='block'; return; }
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ docker run -d --name acg-tracker --restart unless-stopped \
|
|||||||
- Consider adding assignee support to the dashboard (Gitea supports assignees)
|
- Consider adding assignee support to the dashboard (Gitea supports assignees)
|
||||||
- RMM Thoughts backlog (RMM_THOUGHTS.md) now has corresponding Gitea issues — future feature requests should go directly to Gitea via the tracker
|
- RMM Thoughts backlog (RMM_THOUGHTS.md) now has corresponding Gitea issues — future feature requests should go directly to Gitea via the tracker
|
||||||
|
|
||||||
|
## Update: 20:15 PT — Client project tracking added
|
||||||
|
|
||||||
|
Added client project tracking to the tracker dashboard. Created `client-project` label + 14 client-specific labels (client:cascades, client:glaztech, etc.) + `assigned:mike` / `assigned:howard` labels on the claudetools repo. Filed 10 Cascades project items from the wiki REMAINING-WORK-PLAN (domain join, M365 relicensing, break-glass accounts, PAA removal, Juan offboard, ASSISTNURSE-PC kiosk, ALIS records, Zeke MFA, KPI dashboard, voice QoS). Each tagged with client, priority, and assignee.
|
||||||
|
|
||||||
|
Updated the dashboard with a "Client Projects" sidebar section separate from All Issues. Client projects are excluded from the code bug/feature views. The sidebar shows per-client counts and only displays clients that have active projects. Cards show assignee tags. The new issue form adds Client and Assigned To dropdowns when "Client Project" type is selected, auto-selecting the current client when filing from a client section. Filed GuruRMM feature request (gururmm #74) for built-in client/site project tracking in the RMM dashboard as the long-term solution.
|
||||||
|
|
||||||
## Reference Information
|
## Reference Information
|
||||||
|
|
||||||
- Dashboard source: `projects/msp-tools/bug-tracker/dashboard.html`
|
- Dashboard source: `projects/msp-tools/bug-tracker/dashboard.html`
|
||||||
|
|||||||
Reference in New Issue
Block a user