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; }