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:
2026-07-17 20:13:06 -07:00
parent 742bc5f81b
commit aa12284fbf
2 changed files with 110 additions and 6 deletions

View File

@@ -200,7 +200,20 @@ const SECTIONS = [
{ 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-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 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;
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) {
// Parse "**Discovered:** YYYY-MM-DD" from issue body
if (!i.body) return null;
@@ -306,9 +328,19 @@ function tagStyle(l) { return `background:#${l.color}20;color:#${l.color};`; }
function getFiltered() {
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);
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) {
const q = searchText.toLowerCase();
@@ -355,6 +387,27 @@ function renderSidebar() {
${sec.label} <span class="cnt">${countFor(sec)}</span>
</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;
}
@@ -407,11 +460,14 @@ function renderStats(items) {
function makeCard(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})">
<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-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">
<span>${p?`<span class="pdot pdot-${p}"></span>${p}`:''}</span>
<span>${timeAgo(getDiscovered(i)||i.created_at)} old &middot; upd ${timeAgo(i.updated_at)}</span>
@@ -591,6 +647,7 @@ function showNewIssue() {
<option value="skill">Skill</option>
<option value="project">Project</option>
<option value="security">Security</option>
<option value="client-project">Client Project</option>
</select></div>
</div>
<div class="frow">
@@ -609,6 +666,30 @@ function showNewIssue() {
<option value="component:harness">Harness / Skills</option>
</select></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>Description</label><textarea id="ni-body" placeholder="What happened? Steps to reproduce? Context?"></textarea></div>
<div class="factions">
@@ -619,6 +700,18 @@ function showNewIssue() {
</div>
`;
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);
}
function closeNew() { document.getElementById('new-overlay').classList.remove('open'); }
@@ -630,11 +723,16 @@ async function submitNew() {
const type=document.getElementById('ni-type').value;
const prio=document.getElementById('ni-prio').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');
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 ids=[lc[type],lc[prio]].filter(Boolean);
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 {
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; }

View File

@@ -109,6 +109,12 @@ docker run -d --name acg-tracker --restart unless-stopped \
- 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
## 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
- Dashboard source: `projects/msp-tools/bug-tracker/dashboard.html`