Skip to content
Merged
4 changes: 2 additions & 2 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,11 +335,11 @@ The Tracked tab lets you pin issues and PRs into a personal TODO list that you c

**Pinning items:** On the Issues and Pull Requests tabs, hover over any row to reveal a bookmark icon. Click it to pin the item to your tracked list. Click it again to unpin. The bookmark appears filled and highlighted on tracked items.

**Tracked tab:** Shows your pinned items in a flat list (not grouped by repo). Each item displays a type badge (Issue or PR) and uses live data from the poll cycle — statuses, check results, and labels stay current. Items whose repo is no longer being polled show a minimal fallback row with stored metadata.
**Tracked tab:** Shows your pinned items in a flat list (not grouped by repo). Each item shows a repo badge, a type badge (Issue or PR), and uses live data from the poll cycle — labels, comments, and timestamps stay current. Tracked PRs display the same metadata as the Pull Requests tab: review status, size badge, check status dot, draft indicator, and role badge. Tracked issues show a role badge (author/assignee). In compact density, the repo badge abbreviates to just the repo name (hover for the full owner/repo). Items whose repo is no longer being polled show a minimal fallback row with stored metadata. PRs being hot-polled show a shimmer animation and a spinner in the left margin.

**Reordering:** Use the chevron buttons on the left side of each row to move items up or down. Items slide smoothly into their new position.

**Auto-removal:** When a tracked issue is closed or a tracked PR is merged, it is automatically removed from the list. Closure is detected by absence from the `is:open` poll results. For PRs detected as closed by the hot poll, removal happens within seconds.
**Auto-removal:** When a tracked issue is closed or a tracked PR is merged, it is automatically removed from the list. Closure is detected by absence from the `is:open` poll results. For PRs detected as closed by the hot poll, removal happens within seconds. Auto-removal is suspended when the API returns errors (e.g., rate limiting) to prevent false pruning.

**Relationship to other features:** The Tracked tab bypasses the org/repo filter — it always shows all your pinned items regardless of which repo filter is active. Ignoring an item from the Issues or Pull Requests tab also removes it from the tracked list. The tracked list is preserved when tracking is disabled and restored when re-enabled.

Expand Down
Binary file modified docs/dashboard-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ export default function App() {
document.documentElement.setAttribute("data-theme", resolveTheme(config.theme));
});

createEffect(() => {
document.documentElement.dataset.density = config.viewDensity;
});

onMount(() => {
// Listen for system theme changes so "auto" reacts immediately
const mq = window.matchMedia("(prefers-color-scheme: dark)");
Expand Down
8 changes: 3 additions & 5 deletions src/app/components/dashboard/ActionsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createEffect, createMemo, For, Show } from "solid-js";
import { createStore } from "solid-js/store";
import type { WorkflowRun } from "../../services/api";
import { config } from "../../stores/config";
import { viewState, setViewState, setTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, type ActionsFilterField } from "../../stores/view";
import WorkflowSummaryCard from "./WorkflowSummaryCard";
import IgnoreBadge from "./IgnoreBadge";
Expand Down Expand Up @@ -221,8 +220,8 @@ export default function ActionsTab(props: ActionsTabProps) {
return (
<div class="divide-y divide-base-300">
{/* Toolbar */}
<div class="flex items-start gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
<div class="flex flex-wrap items-center gap-3 min-w-0 flex-1">
<div class="flex items-start px-4 py-2 gap-3 compact:py-0.5 compact:gap-2 border-b border-base-300 bg-base-100">
<div class="flex flex-wrap items-center min-w-0 flex-1 gap-3 compact:gap-2">
<label class="flex items-center gap-1.5 text-sm text-base-content/70 cursor-pointer select-none">
<input
type="checkbox"
Expand Down Expand Up @@ -299,7 +298,7 @@ export default function ActionsTab(props: ActionsTabProps) {
<button
onClick={() => toggleExpandedRepo("actions", repoGroup.repoFullName)}
aria-expanded={isExpanded()}
class="flex-1 flex items-center gap-2 px-4 py-2.5 text-left text-sm font-semibold text-base-content"
class="flex-1 flex items-center gap-2 px-4 py-2.5 compact:py-1.5 text-left text-sm font-semibold text-base-content"
>
<ChevronIcon size="md" rotated={!isExpanded()} />
{repoGroup.repoFullName}
Expand Down Expand Up @@ -356,7 +355,6 @@ export default function ActionsTab(props: ActionsTabProps) {
expanded={isWfExpanded()}
onToggle={() => toggleWorkflow(wfKey)}
onIgnoreRun={handleIgnore}
density={config.viewDensity}
refreshTick={props.refreshTick}
hotPollingRunIds={props.hotPollingRunIds}
flashingRunIds={flashingRunIds()}
Expand Down
34 changes: 34 additions & 0 deletions src/app/components/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ function resetDashboardData(): void {
const [hasFetchedFresh, setHasFetchedFresh] = createSignal(false);
export function _resetHasFetchedFresh(value = false) { setHasFetchedFresh(value); }

const [lastFetchHadErrors, setLastFetchHadErrors] = createSignal(false);

// Clear dashboard data and stop polling on logout to prevent cross-user data leakage
onAuthCleared(() => {
resetDashboardData();
Expand Down Expand Up @@ -144,6 +146,17 @@ async function pollFetch(): Promise<DashboardData> {
});
// When notifications gate says nothing changed, keep existing data
if (!data.skipped) {
const hasErrors = data.errors.length > 0;
setLastFetchHadErrors(hasErrors);

// When the fetch had errors and returned no data, keep stale dashboard
// visible rather than wiping it to empty. This prevents the summary strip,
// tab counts, and tracked items from vanishing during rate limiting.
if (hasErrors && data.issues.length === 0 && data.pullRequests.length === 0 && data.workflowRuns.length === 0) {
setDashboardData("loading", false);
return data;
}

setHasFetchedFresh(true);
const now = new Date();

Expand Down Expand Up @@ -254,6 +267,22 @@ export default function DashboardPage() {
const [hotPollingPRIds, setHotPollingPRIds] = createSignal<ReadonlySet<number>>(new Set());
const [hotPollingRunIds, setHotPollingRunIds] = createSignal<ReadonlySet<number>>(new Set());

if (import.meta.env.DEV) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).__debug = {
forceHotPoll: () => {
const allPrIds = new Set<number>(dashboardData.pullRequests.map(pr => pr.id));
setHotPollingPRIds(allPrIds);
console.info(`[debug] Shimmer ON for ${allPrIds.size} PRs. Call __debug.clearHotPoll() to stop.`);
},
clearHotPoll: () => {
setHotPollingPRIds(new Set<number>());
setHotPollingRunIds(new Set<number>());
console.info("[debug] Shimmer OFF");
},
};
}

function resolveInitialTab(): TabId {
const tab = config.rememberLastTab ? viewState.lastActiveTab : config.defaultTab;
if (tab === "tracked" && !config.enableTracking) return "issues";
Expand Down Expand Up @@ -283,6 +312,9 @@ export default function DashboardPage() {
const issues = dashboardData.issues;
const prs = dashboardData.pullRequests;
if (!config.enableTracking || viewState.trackedItems.length === 0 || !hasFetchedFresh()) return;
// Never prune when the last fetch had errors (rate limit, network failure, etc.)
// — the missing items are likely just unfetched, not closed/merged.
if (lastFetchHadErrors()) return;

const polledRepos = new Set([
...config.selectedRepos.map((r) => r.fullName),
Expand Down Expand Up @@ -506,6 +538,8 @@ export default function DashboardPage() {
issues={dashboardData.issues}
pullRequests={dashboardData.pullRequests}
refreshTick={refreshTick()}
userLogin={userLogin()}
hotPollingPRIds={hotPollingPRIds()}
/>
</Match>
<Match when={activeTab() === "actions"}>
Expand Down
6 changes: 3 additions & 3 deletions src/app/components/dashboard/IgnoreBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@ export default function IgnoreBadge(props: IgnoreBadgeProps) {
<Tooltip content={`${props.items.length} ignored item${props.items.length === 1 ? "" : "s"}`}>
<button
onClick={() => setOpen((v) => !v)}
class="btn btn-ghost btn-sm relative"
class="btn btn-ghost btn-sm compact:btn-xs relative"
aria-haspopup="true"
aria-expanded={open()}
aria-label={`${props.items.length} ignored items`}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 compact:h-3.5 compact:w-3.5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd" />
<path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.065 7 9.542 7 .847 0 1.669-.105 2.454-.303z" />
</svg>
<span class="badge badge-neutral badge-xs absolute -top-1 -right-1">{props.items.length}</span>
<span class="badge badge-neutral badge-xs absolute -top-1 -right-1 compact:text-[8px] compact:-top-0.5 compact:-right-0.5 compact:px-0.5 compact:min-w-3 compact:h-3">{props.items.length}</span>
</button>
</Tooltip>

Expand Down
7 changes: 3 additions & 4 deletions src/app/components/dashboard/IssuesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ export default function IssuesTab(props: IssuesTabProps) {
return (
<div class="flex flex-col h-full">
{/* Filter chips + ignore badge toolbar */}
<div class="flex items-start gap-3 px-4 py-2 border-b border-base-300 bg-base-100">
<div class="flex flex-wrap items-center gap-3 min-w-0 flex-1">
<div class="flex items-start px-4 py-2 gap-3 compact:py-0.5 compact:gap-2 border-b border-base-300 bg-base-100">
<div class="flex flex-wrap items-center min-w-0 flex-1 gap-3 compact:gap-2">
<FilterToolbar
groups={filterGroups()}
values={viewState.tabFilters.issues}
Expand Down Expand Up @@ -354,7 +354,7 @@ export default function IssuesTab(props: IssuesTabProps) {
<button
onClick={() => toggleExpandedRepo("issues", repoGroup.repoFullName)}
aria-expanded={isExpanded()}
class="flex-1 flex items-center gap-2 px-4 py-2.5 text-left text-sm font-semibold text-base-content"
class="flex-1 flex items-center gap-2 px-4 py-2.5 compact:py-1.5 text-left text-sm font-semibold text-base-content"
>
<ChevronIcon size="md" rotated={!isExpanded()} />
{repoGroup.repoFullName}
Expand Down Expand Up @@ -411,7 +411,6 @@ export default function IssuesTab(props: IssuesTabProps) {
onIgnore={() => handleIgnore(issue)}
onTrack={config.enableTracking ? () => handleTrack(issue) : undefined}
isTracked={config.enableTracking ? trackedIssueIds().has(issue.id) : undefined}
density={config.viewDensity}
commentCount={issue.comments}
surfacedByBadge={
props.trackedUsers && props.trackedUsers.length > 0
Expand Down
Loading