← back to AbramsEgo
feat: cross-repo git activity feed
d24ccf1b8c53bf92fdb38c8c2761a5698e6b326c · 2026-07-01 11:20:55 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
A data/activity.jsonM public/index.htmlM server.js
Diff
commit d24ccf1b8c53bf92fdb38c8c2761a5698e6b326c
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Jul 1 11:20:55 2026 -0700
feat: cross-repo git activity feed
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
data/activity.json | 1 +
public/index.html | 28 ++++++++++++++++++++++++++++
server.js | 47 +++++++++++++++++++++++++++++++++++++++++++----
3 files changed, 72 insertions(+), 4 deletions(-)
diff --git a/data/activity.json b/data/activity.json
new file mode 100644
index 0000000..eb18154
--- /dev/null
+++ b/data/activity.json
@@ -0,0 +1 @@
+{"fetchedAt":"2026-07-01T18:20:12.834Z","repoCount":25,"commits":[{"repo":"Abrams","hash":"b49580b9","subject":"tick: refresh inventory + SWOT at 2026-07-01T18:17:01Z","date":"2026-07-01T11:17:02-07:00","author":"Abrams Panel"},{"repo":"AbramsEgo","hash":"0e2c42b","subject":"feat: local sell-it landing + pricing + waitlist (publish gated)","date":"2026-07-01T11:17:00-07:00","author":"Steve (AbramsEgo)"},{"repo":"quadrille-showroom","hash":"d6a840c","subject":"chore: refactor + v1.2.2 (session close)","date":"2026-07-01T11:12:29-07:00","author":"Steve"},{"repo":"quadrille-showroom","hash":"df1dc51","subject":"auto-save: 2026-07-01T11:11:52 (2 files) — public/js/showroom.js scripts/_reverify.mjs","date":"2026-07-01T11:12:04-07:00","author":"Steve Abrams"},{"repo":"Abrams","hash":"189a87c2","subject":"auto-save: 2026-07-01T11:11:52 (5 files) — data/abrams-indexes.json data/build-log.jsonl data/counters.json data/mag-20-results.json data/market.json","date":"2026-07-01T11:12:01-07:00","author":"Steve Abrams"},{"repo":"AbramsEgo","hash":"ef6ad60","subject":"auto-save: 2026-07-01T11:11:52 (2 files) — build-queue/tasks/04-model-usage-subagents.md build-queue/done/04-model-usage-subagents.md","date":"2026-07-01T11:11:57-07:00","author":"Steve Abrams"},{"repo":"corkwallcovering","hash":"7f71902","subject":"auto-save: 2026-07-01T11:11:52 (1 files) — scripts/apply-shopify-tags.mjs","date":"2026-07-01T11:11:53-07:00","author":"Steve Abrams"},{"repo":"commercialrealestate","hash":"8edbdfd","subject":"feat: CRE deeds part (b) — current_broker for FREE-fetchable listings (compass/coldwell/myelisting/M&M) via verified WebFetch; crexi 98% deferred (anti-bot, needs Browserbase)","date":"2026-07-01T11:07:11-07:00","author":"Steve Abrams"},{"repo":"corkwallcovering","hash":"2941458","subject":"chore: lint + refactor enrich-cork-ai.mjs, correct 2.5-flash cost constant, v0.1.1 (session close)","date":"2026-07-01T10:39:58-07:00","author":"Steve Abrams"},{"repo":"commercialrealestate","hash":"6be26bf","subject":"chore: lint ✓ (clean, no linter) · refactor ✓ (no change) · v0.3.0 (session close)","date":"2026-07-01T10:38:52-07:00","author":"Steve Abrams"},{"repo":"glitterwalls","hash":"c7de50a","subject":"GLM spinners: swap 26 swatch-spin GIFs for extracted still frames (mirror sync)","date":"2026-06-25T16:51:20-07:00","author":"Steve Abrams"},{"repo":"small-business-builder","hash":"82a9729","subject":"website-analysis: fix public-host critique page — whitelist screenshot+deep-dive, hide SHIP on public host (fantasea.agentabrams.com)","date":"2026-06-24T15:33:59-07:00","author":"Steve Abrams"},{"repo":"small-business-builder","hash":"0a5f356","subject":"auto-save: 2026-06-24T15:27:54 (1 files) — src/server/index.js","date":"2026-06-24T15:28:04-07:00","author":"Steve Abrams"},{"repo":"linenwallpaper","hash":"7eb0e7f","subject":"Update grid density slider to desktop-only (3-12 columns)","date":"2026-06-23T07:14:16-07:00","author":"Steve Abrams"},{"repo":"linenwallpaper","hash":"b222993","subject":"auto-save: 2026-06-22T21:48:03 (1 files) — public/index.html","date":"2026-06-22T21:48:10-07:00","author":"Steve Abrams"},{"repo":"dw-launches","hash":"763635b","subject":"dw-launches: pass through Norma's real publish reason (token-expired / no-creds) instead of stale hardcoded 'code not enabled' string","date":"2026-06-22T17:13:54-07:00","author":"Steve"},{"repo":"dw-launches","hash":"60312c9","subject":"auto-save: 2026-06-22T13:36:34 (1 files) — data/launches.json","date":"2026-06-22T13:36:44-07:00","author":"Steve Abrams"},{"repo":"bestwallpaperplace","hash":"9804165","subject":"deploy: scaffold .deploy.conf for banner deploy","date":"2026-06-21T16:57:32-07:00","author":"Steve"},{"repo":"bestwallpaperplace","hash":"9df327c","subject":"banner: wire universal promo-banner require above express.static (officer-approved; deploy gated)","date":"2026-06-21T16:48:55-07:00","author":"Steve"},{"repo":"retrowalls","hash":"1cf0d0e","subject":"Mount new-arrivals promo strip (Tier B): require ../_shared/_universal-promo-banner before express.static — text-only rotating 'New · <product> →' banner from /api/products?sort=newest. Local only; de","date":"2026-06-19T16:33:22-07:00","author":"Steve"},{"repo":"hospitalitywallpaper","hash":"fce81cd","subject":"Mount new-arrivals promo strip (Tier B): require ../_shared/_universal-promo-banner before express.static — text-only rotating 'New · <product> →' banner from /api/products?sort=newest. Local only; de","date":"2026-06-19T16:33:19-07:00","author":"Steve"},{"repo":"glitterwalls","hash":"2c8dce3","subject":"Mount new-arrivals promo strip (Tier B): require ../_shared/_universal-promo-banner before express.static — text-only rotating 'New · <product> →' banner from /api/products?sort=newest. Local only; de","date":"2026-06-19T16:33:18-07:00","author":"Steve"},{"repo":"1940swallpaper","hash":"5bcbe91","subject":"Mount new-arrivals promo strip (Tier B): require ../_shared/_universal-promo-banner before express.static — text-only rotating 'New · <product> →' banner from /api/products?sort=newest. Local only; de","date":"2026-06-19T16:33:16-07:00","author":"Steve"},{"repo":"retrowalls","hash":"70a042d","subject":"fix: safeImg allow same-origin /img/ proxy urls (vendor-neutral image proxy 2026-06-03) — product cards were falling back to hero-bg (images all wrong)","date":"2026-06-18T09:04:29-07:00","author":"Steve Abrams"},{"repo":"hospitalitywallpaper","hash":"56ed2de","subject":"fix: safeImg allow same-origin /img/ proxy urls (vendor-neutral image proxy 2026-06-03) — product cards were falling back to hero-bg (images all wrong)","date":"2026-06-18T09:04:26-07:00","author":"Steve Abrams"},{"repo":"1940swallpaper","hash":"c884c56","subject":"fix: safeImg allow same-origin /img/ proxy urls (vendor-neutral image proxy 2026-06-03) — product cards were falling back to hero-bg (images all wrong)","date":"2026-06-18T09:04:23-07:00","author":"SteveStudio2"},{"repo":"kravet-sheet-sync-2026-04-20","hash":"5a9322e","subject":"kravet: local crawl + staged additive import CSV for 3 net-net-new SKUs (W3863-166/W3867-77/W4013-86); prod load stays gated","date":"2026-06-10T17:21:10-07:00","author":"Steve Abrams"},{"repo":"kravet-sheet-sync-2026-04-20","hash":"a5e168a","subject":"kravet IMG-FIX: add ksi->all_images backfill SQL + scoped re-crawl helper","date":"2026-06-10T09:52:06-07:00","author":"Steve Abrams"},{"repo":"yolo-agent","hash":"8358fc1","subject":"untrack logs + done-task archive (approved cleanup)","date":"2026-06-09T13:05:25-07:00","author":"SteveStudio2"},{"repo":"yolo-agent","hash":"02bfcb0","subject":"chore: untrack stray .bak/.pre-* backups + add gitignore patterns","date":"2026-06-04T09:11:24-07:00","author":"SteveStudio2"},{"repo":"dw-fleet-registry","hash":"18b7545","subject":"deploy: ssh mkdir -p prod rooms/ before rsync (local openrsync lacks --mkpath)","date":"2026-06-01T16:36:36-07:00","author":"SteveStudio2"},{"repo":"dw-fleet-registry","hash":"a6f39b4","subject":"livefleet viewer: add fleet Sign out (SSO logout) link","date":"2026-06-01T16:29:18-07:00","author":"SteveStudio2"},{"repo":"dw-style-guides","hash":"40acb3a","subject":"DW style guides: Beverly Hills + Sunshine Pastels & Pinks (researched palette, pending info@ Drive reconciliation)","date":"2026-06-01T16:07:29-07:00","author":"SteveStudio2"},{"repo":"wallpaperestimator","hash":"8137c55","subject":"fleet rebuild: dw-header (hamburgers UL + centered name) + unique high-res hero + accent — local branch","date":"2026-06-01T15:58:25-07:00","author":"Steve Abrams"},{"repo":"Grant","hash":"d921396","subject":"Add 3 guest login tiers with capability-based access control","date":"2026-06-01T15:50:26-07:00","author":"Steve Abrams"},{"repo":"wallpaperestimator","hash":"4a6a933","subject":"dw-header: hide ANY pre-existing <header> (not just ns-header); title-case bare-slug names","date":"2026-06-01T15:47:51-07:00","author":"Steve Abrams"},{"repo":"Grant","hash":"a073178","subject":"Remove stray @next/swc-darwin-arm64 hard dependency (broke linux deploy)","date":"2026-06-01T15:32:01-07:00","author":"Steve Abrams"},{"repo":"cncp-failures-mockups","hash":"e860d55","subject":"Link favicon.svg in head (use branded SVG, not .ico fallback)","date":"2026-05-31T16:56:02-07:00","author":"SteveStudio2"},{"repo":"cncp-failures-mockups","hash":"844562b","subject":"Add per-site favicon (kills /favicon.ico 404)","date":"2026-05-31T16:32:27-07:00","author":"SteveStudio2"},{"repo":"dw-monitoring","hash":"772aea1","subject":"fix: correct package.json main entry and add backup-file patterns to .gitignore","date":"2026-05-30T23:53:31-07:00","author":"Steve Abrams"}]}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 0540bfb..197afc2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -161,6 +161,12 @@
<div class="list" id="subruns" style="max-height:340px"></div>
</section>
+ <!-- GIT ACTIVITY FEED (panel 6) -->
+ <section class="card span12">
+ <h2>📜 Activity <span class="muted" id="activity-sub">· recent commits across ~/Projects</span></h2>
+ <div id="activity" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:8px;max-height:440px;overflow:auto"></div>
+ </section>
+
<!-- AI CHAT -->
<section class="card span12 chat">
<h2>🤖 Ask AbramsEgo <span class="muted">· grounded in the live snapshot · cost shown per query</span></h2>
@@ -221,6 +227,9 @@ async function load(){
// ---- model usage + sub-agent runs (panel 5) ----
renderUsage(d);
+ // ---- git activity feed (panel 6) ----
+ renderActivity(d);
+
// ---- fleet ----
const k=d.kamatera||{}, l=d.localFleet||{};
$('kam-up').innerHTML=`<span class="${cls(100-(k.up/(k.total||1)*100),20,50)}">${k.up??'—'}</span><span class="dim" style="font-size:16px">/${k.total??'—'} up</span>`;
@@ -333,6 +342,25 @@ function renderUsage(d){
}).join('')||'<span class="muted">no sub-agent runs</span>';
}
}
+// git activity — each commit row is an ADMIN CARD → MUST show created date+time
+function renderActivity(d){
+ const a=d.activity||{};
+ const commits=a.commits||[];
+ const sub=$('activity-sub');
+ if(sub) sub.textContent=`· ${commits.length} commits · ${a.repoCount||0} repos · newest first`;
+ const el=$('activity');
+ if(!el) return;
+ el.innerHTML=commits.map(cm=>{
+ const iso=cm.date||'';
+ const dt=iso?new Date(iso):null;
+ const when=dt&&!isNaN(dt)?dt.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}):'—';
+ return `<div class="subcard" title="${esc(iso)}">
+ <div class="subcard-top"><span class="subcard-proj">${esc(cm.repo||'?')}</span>`+
+ `<span class="pill">${esc(cm.hash||'')}</span></div>`+
+ `<div class="subcard-task">${esc(cm.subject||'')}</div>`+
+ `<div class="subcard-when">🕓 ${esc(when)}${cm.author?' · '+esc(cm.author):''}</div></div>`;
+ }).join('')||'<span class="muted">no recent commits</span>';
+}
function statusDot(st){const c=st==='online'?'var(--good)':st?'var(--bad)':'var(--dim)';return `<span class="dot" style="background:${c}"></span>`;}
function fmtDur(s){if(s==null)return null;if(s<60)return s+'s';if(s<3600)return Math.floor(s/60)+'m';return Math.floor(s/3600)+'h';}
function esc(x){return (x==null?'':String(x)).replace(/[<>&]/g,m=>({'<':'<','>':'>','&':'&'}[m]));}
diff --git a/server.js b/server.js
index aad33c4..e317052 100644
--- a/server.js
+++ b/server.js
@@ -191,6 +191,45 @@ async function collectCrons() {
return { count: jobs.length, loadedCount: jobs.filter((j) => j.loaded).length, jobs };
}
+// --- GIT ACTIVITY FEED (panel 6) -------------------------------------------
+// Scans the ~25 most-recently-touched git repos under ~/Projects and pulls each
+// one's latest 1-2 commits, merged + sorted newest-first, capped at 40 rows.
+// Shelling out to git per-repo is slow, so it's throttled to ~3 min and cached
+// to disk (same stale-while-revalidate shape as the Kamatera collector); the
+// 30s snapshot loop reads the cache instantly except on the refresh tick.
+const PROJECTS_ROOT = path.join(HOME, 'Projects');
+const ACT_CACHE = path.join(DATA_DIR, 'activity.json');
+const ACT_SEP = '\x1f'; // unit separator — safe delimiter inside git --pretty
+let actLastFetch = 0;
+async function collectActivity() {
+ const now = Date.now();
+ if (now - actLastFetch > 3 * 60 * 1000) {
+ actLastFetch = now;
+ // find repos = ~/Projects/* dirs that have a .git, ranked by .git mtime
+ // (git rewrites .git/{logs,refs,index} on commit, so its mtime ≈ last commit)
+ let dirs = [];
+ try { dirs = fs.readdirSync(PROJECTS_ROOT, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith('.')); } catch (e) {}
+ const repos = [];
+ for (const d of dirs) {
+ try { const st = fs.statSync(path.join(PROJECTS_ROOT, d.name, '.git')); repos.push({ name: d.name, mtime: st.mtimeMs }); } catch (e) { /* not a repo */ }
+ }
+ repos.sort((a, b) => b.mtime - a.mtime);
+ const top = repos.slice(0, 25);
+ const rows = await Promise.all(top.map(async (r) => {
+ const g = await sh('git', ['-C', path.join(PROJECTS_ROOT, r.name), 'log', '-2', '--no-color', `--pretty=format:%h${ACT_SEP}%s${ACT_SEP}%aI${ACT_SEP}%an`], 5000);
+ if (!g.ok) return [];
+ return g.out.trim().split('\n').filter(Boolean).map((ln) => {
+ const [hash, subject, date, author] = ln.split(ACT_SEP);
+ return { repo: r.name, hash, subject: (subject || '').slice(0, 200), date, author };
+ }).filter((c) => c.hash && c.date);
+ }));
+ const commits = rows.flat().sort((a, b) => Date.parse(b.date) - Date.parse(a.date)).slice(0, 40);
+ try { fs.writeFileSync(ACT_CACHE, JSON.stringify({ fetchedAt: new Date().toISOString(), repoCount: top.length, commits })); } catch (e) {}
+ }
+ try { return JSON.parse(fs.readFileSync(ACT_CACHE, 'utf8')); }
+ catch (e) { return { commits: [], repoCount: 0, note: 'no activity snapshot yet' }; }
+}
+
// --- MODEL USAGE + SESSIONS + SUB-AGENT RUNS (panel 5) ---------------------
// Reads ~/.claude/cost-ledger.jsonl for per-model calls/tokens/$ (24h + all-time)
// and ~/.claude/projects/**/*.jsonl mtimes for active/recent Claude sessions +
@@ -472,8 +511,8 @@ let BUILDING = null; // in-flight build promise — callers coalesce onto it so
// returns FRESH data, never a stale in-flight snapshot.
async function doBuild() {
const wrap = async (fn) => { try { return await fn(); } catch (e) { return { error: e.message }; } };
- const [system, localFleet, kamatera, canaries, crons] = await Promise.all([
- wrap(collectSystem), wrap(collectLocalFleet), wrap(collectKamateraFleet), wrap(collectCanaries), wrap(collectCrons),
+ const [system, localFleet, kamatera, canaries, crons, activity] = await Promise.all([
+ wrap(collectSystem), wrap(collectLocalFleet), wrap(collectKamateraFleet), wrap(collectCanaries), wrap(collectCrons), wrap(collectActivity),
]);
const [sessions, wins, officers] = await Promise.all([
wrap(() => fetchJSON(`${CNCP}/api/sessions`)),
@@ -487,7 +526,7 @@ async function doBuild() {
SNAP = {
builtAt: new Date().toISOString(),
version: VERSION,
- system, localFleet, kamatera, canaries, crons,
+ system, localFleet, kamatera, canaries, crons, activity,
sessions: summarizeSessions(sessions),
wins: summarizeWins(wins),
officers: summarizeOfficers(officers),
@@ -555,7 +594,7 @@ app.get('/api/healthz', (req, res) => res.json({ ok: true, service: 'abramsego',
app.get('/api/data', (req, res) => { res.json(SNAP); });
app.get('/api/refresh', async (req, res) => { const s = await buildSnapshot(); res.json(s); });
// per-panel slices (served from cache)
-for (const key of ['system', 'localFleet', 'kamatera', 'canaries', 'crons', 'sessions', 'wins', 'officers', 'cost', 'revenue', 'pnl', 'usage']) {
+for (const key of ['system', 'localFleet', 'kamatera', 'canaries', 'crons', 'activity', 'sessions', 'wins', 'officers', 'cost', 'revenue', 'pnl', 'usage']) {
app.get(`/api/${key}`, (req, res) => res.json(SNAP[key] || {}));
}
← 672e1bc feat(chat): local-ollama AI chat panel ($0, cap-relieving)
·
back to AbramsEgo
·
feat(canaries): canary-verdicts health panel 497cc60 →