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: '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 · 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; }
|
||||
|
||||
Reference in New Issue
Block a user