diff --git a/lib/public/css/home-hub.css b/lib/public/css/home-hub.css index 9fb32a24..1d6f4318 100644 --- a/lib/public/css/home-hub.css +++ b/lib/public/css/home-hub.css @@ -497,6 +497,16 @@ text-align: center; flex-shrink: 0; } +/* Per-project alert indicator — same red used by .icon-strip-project-badge + (unread badge). A count wouldn't fit this row, so a plain dot is enough + to show WHERE the alert is coming from. */ +.hub-recent-alert-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + background: #e74c3c; +} .hub-recent-project-icon--blank { width: 20px; height: 20px; diff --git a/lib/public/modules/app-home-hub.js b/lib/public/modules/app-home-hub.js index bb93b0ee..ac8330d5 100644 --- a/lib/public/modules/app-home-hub.js +++ b/lib/public/modules/app-home-hub.js @@ -232,6 +232,22 @@ function formatScheduleTime(ts) { return DAY_NAMES[d.getDay()] + " " + timeStr; } +// Looks up whether a project has unresolved alerts, using the SAME source +// that drives the Projects-list unread badge (proj.unread, populated in +// renderProjectList() / app-projects.js and rendered as +// .icon-strip-project-badge.has-unread / .hub-project-sessions elsewhere). +// No separate alert data path — this is a read of the existing cache. +function projectHasAlert(projectSlug) { + if (!projectSlug) return false; + var projects = getCachedProjects(); + for (var i = 0; i < projects.length; i++) { + if (projects[i].slug === projectSlug) { + return (projects[i].unread || 0) > 0; + } + } + return false; +} + function formatRelativeTime(ts) { if (!ts) return ""; var diff = Date.now() - ts; @@ -460,10 +476,18 @@ export function handleHubRecentSessions(msg) { ? '' + escapeHtml(sess.agentName) + '' : ''; var dotClass = "hub-recent-dot" + (sess.isProcessing ? " processing" : ""); + // Alert indicator: reuse the same per-project unread count that drives + // the Projects-list badge (icon-strip-project-badge / hub-project-*), + // rather than a new data path. A count won't fit this row, so render a + // plain dot when the row's project has 1+ unresolved (unread) items. + var alertHtml = projectHasAlert(sess.projectSlug) + ? '' + : ''; item.innerHTML = '' + iconHtml + '' + escapeHtml(sess.title) + '' + + alertHtml + agentBadge + '' + timeStr + ''; item.addEventListener("click", function () { diff --git a/test/hub-recent-sessions-alert-dot-lr-2b1f03.test.js b/test/hub-recent-sessions-alert-dot-lr-2b1f03.test.js new file mode 100644 index 00000000..d05d82dc --- /dev/null +++ b/test/hub-recent-sessions-alert-dot-lr-2b1f03.test.js @@ -0,0 +1,92 @@ +// hub-recent-sessions-alert-dot-lr-2b1f03.test.js — lr-2b1f03 regression coverage. +// +// Feature: the Home Hub Projects list shows a per-project alert count badge +// (proj.unread, rendered as .icon-strip-project-badge.has-unread etc), but +// the Recent Sessions list showed no alert indicator at all. This adds a +// small red dot on each Recent Sessions row whose project has 1+ unread +// items — reusing the SAME per-project unread source that drives the +// Projects-list badge, rather than inventing a new data path. +// +// app-home-hub.js imports getCachedProjects from app-projects.js, and +// pulls in a long chain of DOM-touching modules (theme, scheduler, +// filebrowser, ws-ref, etc.) that assume a browser environment, so +// importing it directly under node:test is impractical. Matching the +// project's existing convention for DOM-heavy frontend modules (see +// mobile-home-toggle-lr-551048.test.js), this is a source-text regression +// check against the built file. + +"use strict"; + +var test = require("node:test"); +var assert = require("node:assert/strict"); +var fs = require("fs"); +var path = require("path"); + +var HOME_HUB_JS = fs.readFileSync( + path.join(__dirname, "../lib/public/modules/app-home-hub.js"), + "utf8" +); + +var HOME_HUB_CSS = fs.readFileSync( + path.join(__dirname, "../lib/public/css/home-hub.css"), + "utf8" +); + +test("app-home-hub.js: defines a projectHasAlert helper that reads getCachedProjects() unread state", () => { + var idx = HOME_HUB_JS.indexOf("function projectHasAlert"); + assert.ok(idx !== -1, "expected a projectHasAlert(projectSlug) helper to exist"); + var block = HOME_HUB_JS.slice(idx, idx + 600); + + assert.match( + block, + /getCachedProjects\s*\(\s*\)/, + "projectHasAlert must reuse getCachedProjects() — the same per-project cache that drives the Projects-list unread badge — rather than inventing a new data path" + ); + assert.match( + block, + /\.unread/, + "projectHasAlert must key off proj.unread, the same field rendered as the Projects-list alert count badge (app-projects.js renderProjectList / sidebar-projects.js .icon-strip-project-badge)" + ); +}); + +test("app-home-hub.js: handleHubRecentSessions renders an alert dot for rows whose project has unread activity", () => { + var idx = HOME_HUB_JS.indexOf("export function handleHubRecentSessions"); + assert.ok(idx !== -1, "expected handleHubRecentSessions to exist"); + var block = HOME_HUB_JS.slice(idx, idx + 2000); + + assert.match( + block, + /projectHasAlert\s*\(\s*sess\.projectSlug\s*\)/, + "handleHubRecentSessions must call projectHasAlert(sess.projectSlug) per row to decide whether to show the dot" + ); + assert.match( + block, + /hub-recent-alert-dot/, + "the alert indicator must use a dedicated hub-recent-alert-dot class, distinct from the existing hub-recent-dot (processing) indicator" + ); +}); + +test("app-home-hub.js: rows for projects with no alerts render unchanged (alertHtml is empty string, not a hidden element)", () => { + var idx = HOME_HUB_JS.indexOf("var alertHtml"); + assert.ok(idx !== -1, "expected an alertHtml local computed per session row"); + var block = HOME_HUB_JS.slice(idx, idx + 300); + + assert.match( + block, + /\?\s*' { + var idx = HOME_HUB_CSS.indexOf(".hub-recent-alert-dot"); + assert.ok(idx !== -1, "expected a .hub-recent-alert-dot rule in home-hub.css"); + var block = HOME_HUB_CSS.slice(idx, idx + 300); + + assert.match(block, /border-radius:\s*50%/, "alert indicator must be a circular dot"); + assert.match( + block, + /#e74c3c/, + "alert dot must use the same red (#e74c3c) as the existing .icon-strip-project-badge unread badge, matching established alert-color convention" + ); +});