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:
@@ -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 · 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> · `:''}${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()} ·
|
||||
Updated ${new Date(issue.updated_at).toLocaleString()} ·
|
||||
<a href="${giteaUrl}" target="_blank">Open in Gitea</a>
|
||||
Updated ${new Date(issue.updated_at).toLocaleString()}
|
||||
${getDueDate(issue) ? ` · <span style="color:${isDueOverdue(issue)?'var(--red)':isDueUrgent(issue)?'var(--orange)':'inherit'}">Due: ${getDueDate(issue)}</span>` : ''}
|
||||
· <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">
|
||||
|
||||
Reference in New Issue
Block a user