sync: auto-sync from HOWARD-HOME at 2026-07-18 12:21:37

Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-18 12:21:37
This commit is contained in:
2026-07-18 12:22:04 -07:00
parent 90e25f0979
commit 84c1bb62bb

View File

@@ -197,19 +197,17 @@ const SECTIONS = [
{ id: 'rmm-bugs', label: 'RMM Bugs', repos: ['gururmm','guru-rmm'], filter: i => getType(i)==='bug' },
{ id: 'rmm-features', label: 'RMM Features', repos: ['gururmm','guru-rmm'], filter: i => getType(i)==='feature' },
{ id: 'rmm-security', label: 'RMM Security', repos: ['gururmm','guru-rmm'], filter: i => getType(i)==='security' },
{ id: 'gc-all', label: 'GuruConnect', repos: ['guru-connect'], filter: () => true },
{ id: 'gc-all', label: 'GuruConnect', repos: ['guru-connect'], filter: i => !isThought(i) },
{ 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') && !isClientProject(i) },
{ id: 'ct-projects', label: 'Projects', repos: ['claudetools'], filter: i => (getType(i)==='project' || getType(i)==='feature') && !isClientProject(i) && !isThought(i) },
];
// Client project sections — built dynamically from labels
// Client sections are built dynamically from labels — only clients with issues appear in sidebar
const CLIENT_SECTIONS = [];
const ALL_REPOS = ['gururmm','guru-rmm','guru-connect','claudetools'];
const REPO_NAMES = { 'gururmm':'GuruRMM', 'guru-rmm':'GuruRMM', 'guru-connect':'GuruConnect', 'claudetools':'ClaudeTools' };
let allIssues = [], labelCache = {}, currentSection = 'all', currentView = 'board';
let searchText = '', showClosed = false, sortField = 'updated', sortDir = -1;
let searchText = '', sortField = 'updated', sortDir = -1;
// --- Auth ---
function getAuth() { return localStorage.getItem('acg_tk') || ''; }
@@ -291,6 +289,19 @@ function getType(i) {
return null;
}
function isClientProject(i) { return i.labels.some(l => l.name === 'client-project'); }
function isThought(i) { return i.labels.some(l => l.name === 'thought'); }
function getDueDate(i) { return i.due_date ? i.due_date.split('T')[0] : null; }
function isDueUrgent(i) {
const d = getDueDate(i);
if (!d) return false;
const diff = (new Date(d) - new Date()) / 86400000;
return diff <= 7;
}
function isDueOverdue(i) {
const d = getDueDate(i);
if (!d) return false;
return new Date(d) < new Date();
}
function getAssignee(i) {
const a = i.labels.find(l => l.name.startsWith('assigned:'));
return a ? a.name.split(':')[1] : null;
@@ -317,26 +328,28 @@ function tagStyle(l) { return `background:#${l.color}20;color:#${l.color};`; }
function getFiltered() {
let items = allIssues;
if (currentSection.startsWith('cp-')) {
// Client project section — dynamic
if (currentSection === 'my-howard') {
items = items.filter(i => getAssignee(i) === 'howard');
} else if (currentSection === 'my-mike') {
items = items.filter(i => getAssignee(i) === 'mike');
} else if (currentSection === 'thoughts') {
items = items.filter(i => isThought(i));
} else if (currentSection.startsWith('cp-')) {
items = items.filter(i => isClientProject(i));
if (currentSection !== 'cp-all') {
const clientTag = 'client:' + currentSection.slice(3);
items = items.filter(i => i.labels.some(l => l.name === clientTag));
}
} else if (currentSection === 'all') {
// Exclude client projects from "all issues"
items = items.filter(i => !isClientProject(i));
items = items.filter(i => !isClientProject(i) && !isThought(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) && !isClientProject(i));
if (sec) items = items.filter(i => sec.repos.includes(i._repo) && sec.filter(i) && !isClientProject(i) && !isThought(i));
}
if (searchText) {
const q = searchText.toLowerCase();
items = items.filter(i => i.title.toLowerCase().includes(q) || (i.body||'').toLowerCase().includes(q) || ('#'+i.number).includes(q));
}
// showClosed controls list view; board view always shows done column
if (!showClosed && currentView === 'list') items = items.filter(i => i.state !== 'closed');
return items;
}
@@ -348,7 +361,10 @@ function renderSidebar() {
if (sec) items = items.filter(i => sec.repos.includes(i._repo) && sec.filter(i));
return items.length;
};
const totalOpen = allIssues.filter(i=>i.state!=='closed').length;
const totalOpen = allIssues.filter(i=>i.state!=='closed' && !isClientProject(i) && !isThought(i)).length;
const howardCount = allIssues.filter(i=>i.state!=='closed' && getAssignee(i)==='howard').length;
const mikeCount = allIssues.filter(i=>i.state!=='closed' && getAssignee(i)==='mike').length;
const thoughtCount = allIssues.filter(i=>i.state!=='closed' && isThought(i)).length;
let html = `
<div class="sidebar-item ${currentSection==='all'?'active':''}" onclick="setSection('all')">
@@ -358,6 +374,14 @@ function renderSidebar() {
Recent Activity
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-section">My Work</div>
<div class="sidebar-item ${currentSection==='my-howard'?'active':''}" onclick="setSection('my-howard')">
Howard <span class="cnt">${howardCount}</span>
</div>
<div class="sidebar-item ${currentSection==='my-mike'?'active':''}" onclick="setSection('my-mike')">
Mike <span class="cnt">${mikeCount}</span>
</div>
<div class="sidebar-divider"></div>
<div class="sidebar-section">GuruRMM</div>
`;
for (const sec of SECTIONS.filter(s=>s.id.startsWith('rmm'))) {
@@ -398,6 +422,12 @@ function renderSidebar() {
</div>`;
}
// Thoughts section
html += `<div class="sidebar-divider"></div><div class="sidebar-section">Ideas / Thoughts</div>`;
html += `<div class="sidebar-item ${currentSection==='thoughts'?'active':''}" onclick="setSection('thoughts')">
Thoughts <span class="cnt">${thoughtCount}</span>
</div>`;
sb.innerHTML = html;
}
@@ -425,9 +455,6 @@ function renderToolbar() {
<button class="btn ${currentView==='board'?'active':''}" onclick="currentView='board';renderMain()">Board</button>
<button class="btn ${currentView==='list'?'active':''}" onclick="currentView='list';renderMain()">List</button>
</div>
<button class="btn ${showClosed?'active':''}" onclick="showClosed=!showClosed;renderMain()">
${showClosed ? 'Hiding closed in list' : 'Show closed in list'}
</button>
<button class="btn btn-primary" onclick="showNewIssue()">+ New Issue</button>
<button class="btn" onclick="loadIssues()">Refresh</button>
</div>`;
@@ -460,7 +487,7 @@ function makeCard(i) {
<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>
<span>${getDueDate(i)?`<span style="color:${isDueOverdue(i)?'var(--red)':isDueUrgent(i)?'var(--orange)':'var(--muted)'}">due ${getDueDate(i)}</span> &middot; `:''}${timeAgo(getDiscovered(i)||i.created_at)} old</span>
</div>
</div>`;
}
@@ -577,8 +604,14 @@ async function showDetail(repo, num) {
</div>
<div class="m-meta">
Created ${new Date(issue.created_at).toLocaleString()} &middot;
Updated ${new Date(issue.updated_at).toLocaleString()} &middot;
<a href="${giteaUrl}" target="_blank">Open in Gitea</a>
Updated ${new Date(issue.updated_at).toLocaleString()}
${getDueDate(issue) ? ` &middot; <span style="color:${isDueOverdue(issue)?'var(--red)':isDueUrgent(issue)?'var(--orange)':'inherit'}">Due: ${getDueDate(issue)}</span>` : ''}
&middot; <a href="${giteaUrl}" target="_blank">Open in Gitea</a>
</div>
<div style="margin-top:8px;display:flex;gap:8px;align-items:center;font-size:12px">
<label style="color:var(--muted)">Due date:</label>
<input type="date" value="${getDueDate(issue)||''}" onchange="setDueDate('${repo}',${num},this.value)" style="background:var(--bg);border:1px solid var(--border);border-radius:4px;color:var(--text);padding:4px 8px;font-size:12px">
${getDueDate(issue) ? '<button class="btn" style="font-size:11px;padding:3px 8px" onclick="setDueDate(\''+repo+'\','+num+',null)">Clear</button>' : ''}
</div>
<div class="m-actions" style="flex-wrap:wrap;gap:8px">
<select class="btn" onchange="changePriority('${repo}',${num},this.value)" style="font-size:12px">
@@ -598,6 +631,10 @@ async function showDetail(repo, num) {
<button class="btn" onclick="toggleLabel('${repo}',${num},'blocked')">Blocked</button>
<button class="btn" onclick="toggleLabel('${repo}',${num},'fixed')">Fixed</button>
<button class="btn btn-danger" onclick="changeState('${repo}',${num},'closed')">Close</button>
${isThought(issue) ? `
<button class="btn btn-success" style="font-size:12px" onclick="promoteThought('${repo}',${num},'feature')">Promote to Feature</button>
<button class="btn btn-success" style="font-size:12px" onclick="promoteThought('${repo}',${num},'bug')">Promote to Bug</button>
` : ''}
` : `
<button class="btn" onclick="changeState('${repo}',${num},'open')">Reopen</button>
`}
@@ -640,6 +677,29 @@ async function saveEdit(repo, num) {
showDetail(repo, num); loadIssues();
}
async function promoteThought(repo, num, newType) {
const lc = labelCache[repo] || {};
// Remove thought label
const thoughtId = lc['thought'];
if (thoughtId) await fetch(`${API}/repos/${OWNER}/${repo}/issues/${num}/labels/${thoughtId}`, {method:'DELETE', headers:headers()});
// Add new type label
const newId = lc[newType];
if (newId) await fetch(`${API}/repos/${OWNER}/${repo}/issues/${num}/labels`, {method:'POST', headers:{...headers(),'Content-Type':'application/json'}, body:JSON.stringify({labels:[newId]})});
await addComment(repo, num);
// Add a promotion comment
await fetch(`${API}/repos/${OWNER}/${repo}/issues/${num}/comments`, {method:'POST', headers:{...headers(),'Content-Type':'application/json'}, body:JSON.stringify({body:`Promoted from thought to ${newType}.`})});
showDetail(repo, num); loadIssues();
}
async function setDueDate(repo, num, date) {
const body = date ? {due_date: date + 'T00:00:00Z'} : {due_date: null};
await fetch(`${API}/repos/${OWNER}/${repo}/issues/${num}`, {
method:'PATCH', headers:{...headers(),'Content-Type':'application/json'},
body: JSON.stringify(body)
});
showDetail(repo, num); loadIssues();
}
async function addComment(repo,num) {
const t=document.getElementById('cmt-text').value.trim(); if(!t) return;
await fetch(`${API}/repos/${OWNER}/${repo}/issues/${num}/comments`, {method:'POST', headers:{...headers(),'Content-Type':'application/json'}, body:JSON.stringify({body:t})});
@@ -709,6 +769,7 @@ function showNewIssue() {
<option value="project">Project</option>
<option value="security">Security</option>
<option value="client-project">Client Project</option>
<option value="thought">Thought / Idea</option>
</select></div>
</div>
<div class="frow">