Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/app/components/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
fetchAllData,
type DashboardData,
} from "../../services/poll";
import { clearAuth, user, onAuthCleared, DASHBOARD_STORAGE_KEY } from "../../stores/auth";
import { expireToken, user, onAuthCleared, DASHBOARD_STORAGE_KEY } from "../../stores/auth";
import { pushNotification } from "../../lib/errors";
import { getClient, getGraphqlRateLimit } from "../../services/github";
import { formatCount } from "../../lib/format";
import { setsEqual } from "../../lib/collections";
Expand Down Expand Up @@ -191,7 +192,7 @@ async function pollFetch(): Promise<DashboardData> {
try {
localStorage.setItem(DASHBOARD_STORAGE_KEY, JSON.stringify(cachePayload));
} catch {
// localStorage full or unavailable — non-fatal
pushNotification("localStorage:dashboard", "Dashboard cache write failed — storage may be full", "warning");
}
}, 0);
} else {
Expand All @@ -208,9 +209,9 @@ async function pollFetch(): Promise<DashboardData> {
: null;

if (status === 401) {
// Hard redirect (not navigate()) forces a full page reload, which clears
// module-level state like _coordinator and dashboardData for the next user.
clearAuth();
// Token invalid — clear token only, preserve user config/view/dashboard.
// Hard redirect forces a full page reload, clearing module-level state.
expireToken();
window.location.replace("/login");
}
setDashboardData("loading", false);
Expand Down Expand Up @@ -359,6 +360,7 @@ export default function DashboardPage() {
userLogin={userLogin()}
allUsers={allUsers()}
trackedUsers={config.trackedUsers}
monitoredRepos={config.monitoredRepos}
/>
</Match>
<Match when={activeTab() === "pullRequests"}>
Expand All @@ -369,6 +371,7 @@ export default function DashboardPage() {
allUsers={allUsers()}
trackedUsers={config.trackedUsers}
hotPollingPRIds={hotPollingPRIds()}
monitoredRepos={config.monitoredRepos}
/>
</Match>
<Match when={activeTab() === "actions"}>
Expand Down
21 changes: 16 additions & 5 deletions src/app/components/dashboard/IssuesTab.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
import { config, type TrackedUser } from "../../stores/config";
import { viewState, setSortPreference, setTabFilter, resetTabFilter, resetAllTabFilters, ignoreItem, unignoreItem, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, type IssueFilterField } from "../../stores/view";
import type { Issue } from "../../services/api";
import type { Issue, RepoRef } from "../../services/api";
import ItemRow from "./ItemRow";
import UserAvatarBadge, { buildSurfacedByUsers } from "../shared/UserAvatarBadge";
import IgnoreBadge from "./IgnoreBadge";
Expand All @@ -25,6 +25,7 @@ export interface IssuesTabProps {
userLogin: string;
allUsers?: { login: string; label: string }[];
trackedUsers?: TrackedUser[];
monitoredRepos?: RepoRef[];
}

type SortField = "repo" | "title" | "author" | "createdAt" | "updatedAt" | "comments";
Expand Down Expand Up @@ -68,6 +69,10 @@ export default function IssuesTab(props: IssuesTabProps) {
new Set((config.upstreamRepos ?? []).map(r => r.fullName))
);

const monitoredRepoNameSet = createMemo(() =>
new Set((props.monitoredRepos ?? []).map(r => r.fullName))
);

const filterGroups = createMemo<FilterChipGroupDef[]>(() => {
const users = props.allUsers;
if (!users || users.length <= 1) return issueFilterGroups;
Expand Down Expand Up @@ -114,10 +119,13 @@ export default function IssuesTab(props: IssuesTabProps) {
}

if (tabFilter.user !== "all") {
const validUser = !props.allUsers || props.allUsers.some(u => u.login === tabFilter.user);
if (validUser) {
const surfacedBy = issue.surfacedBy ?? [props.userLogin.toLowerCase()];
if (!surfacedBy.includes(tabFilter.user)) return false;
// Items from monitored repos bypass the surfacedBy filter (all activity is shown)
if (!monitoredRepoNameSet().has(issue.repoFullName)) {
const validUser = !props.allUsers || props.allUsers.some(u => u.login === tabFilter.user);
if (validUser) {
const surfacedBy = issue.surfacedBy ?? [props.userLogin.toLowerCase()];
if (!surfacedBy.includes(tabFilter.user)) return false;
}
}
}

Expand Down Expand Up @@ -305,6 +313,9 @@ export default function IssuesTab(props: IssuesTabProps) {
>
<ChevronIcon size="md" rotated={!isExpanded()} />
{repoGroup.repoFullName}
<Show when={monitoredRepoNameSet().has(repoGroup.repoFullName)}>
<span class="badge badge-xs badge-ghost" aria-label="monitoring all activity">Monitoring all</span>
</Show>
<Show when={!isExpanded()}>
<span class="ml-auto flex items-center gap-2 text-xs font-normal text-base-content/60">
<span>{repoGroup.items.length} {repoGroup.items.length === 1 ? "issue" : "issues"}</span>
Expand Down
21 changes: 16 additions & 5 deletions src/app/components/dashboard/PullRequestsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
import { config, type TrackedUser } from "../../stores/config";
import { viewState, setSortPreference, ignoreItem, unignoreItem, setTabFilter, resetTabFilter, resetAllTabFilters, toggleExpandedRepo, setAllExpanded, pruneExpandedRepos, pruneLockedRepos, type PullRequestFilterField } from "../../stores/view";
import type { PullRequest } from "../../services/api";
import type { PullRequest, RepoRef } from "../../services/api";
import { deriveInvolvementRoles, prSizeCategory } from "../../lib/format";
import ExpandCollapseButtons from "../shared/ExpandCollapseButtons";
import ItemRow from "./ItemRow";
Expand Down Expand Up @@ -30,6 +30,7 @@ export interface PullRequestsTabProps {
allUsers?: { login: string; label: string }[];
trackedUsers?: TrackedUser[];
hotPollingPRIds?: ReadonlySet<number>;
monitoredRepos?: RepoRef[];
}

type SortField = "repo" | "title" | "author" | "createdAt" | "updatedAt" | "checkStatus" | "reviewDecision" | "size";
Expand Down Expand Up @@ -135,6 +136,10 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
new Set((config.upstreamRepos ?? []).map(r => r.fullName))
);

const monitoredRepoNameSet = createMemo(() =>
new Set((props.monitoredRepos ?? []).map(r => r.fullName))
);

const filterGroups = createMemo<FilterChipGroupDef[]>(() => {
const users = props.allUsers;
if (!users || users.length <= 1) return prFilterGroups;
Expand Down Expand Up @@ -199,10 +204,13 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
}

if (tabFilters.user !== "all") {
const validUser = !props.allUsers || props.allUsers.some(u => u.login === tabFilters.user);
if (validUser) {
const surfacedBy = pr.surfacedBy ?? [props.userLogin.toLowerCase()];
if (!surfacedBy.includes(tabFilters.user)) return false;
// Items from monitored repos bypass the surfacedBy filter (all activity is shown)
if (!monitoredRepoNameSet().has(pr.repoFullName)) {
const validUser = !props.allUsers || props.allUsers.some(u => u.login === tabFilters.user);
if (validUser) {
const surfacedBy = pr.surfacedBy ?? [props.userLogin.toLowerCase()];
if (!surfacedBy.includes(tabFilters.user)) return false;
}
}
}

Expand Down Expand Up @@ -418,6 +426,9 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
>
<ChevronIcon size="md" rotated={!isExpanded()} />
{repoGroup.repoFullName}
<Show when={monitoredRepoNameSet().has(repoGroup.repoFullName)}>
<span class="badge badge-xs badge-ghost" aria-label="monitoring all activity">Monitoring all</span>
</Show>
<Show when={!isExpanded()}>
<span class="ml-auto flex items-center gap-2 text-xs font-normal text-base-content/60 shrink-0">
<span>{repoGroup.items.length} {repoGroup.items.length === 1 ? "PR" : "PRs"}</span>
Expand Down
19 changes: 18 additions & 1 deletion src/app/components/onboarding/OnboardingWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Match,
} from "solid-js";
import { config, updateConfig, CONFIG_STORAGE_KEY } from "../../stores/config";
import { pushNotification } from "../../lib/errors";
import { fetchOrgs, type OrgEntry, type RepoRef } from "../../services/api";
import { getClient } from "../../services/github";
import RepoSelector from "./RepoSelector";
Expand All @@ -19,6 +20,9 @@ export default function OnboardingWizard() {
const [upstreamRepos, setUpstreamRepos] = createSignal<RepoRef[]>(
config.upstreamRepos.length > 0 ? [...config.upstreamRepos] : []
);
const [monitoredRepos, setMonitoredRepos] = createSignal<RepoRef[]>(
config.monitoredRepos.length > 0 ? [...config.monitoredRepos] : []
);

const [loading, setLoading] = createSignal(true);
const [error, setError] = createSignal<string | null>(null);
Expand Down Expand Up @@ -57,10 +61,15 @@ export default function OnboardingWizard() {
selectedOrgs: uniqueOrgs,
selectedRepos: selectedRepos(),
upstreamRepos: upstreamRepos(),
monitoredRepos: monitoredRepos(),
onboardingComplete: true,
});
// Flush synchronously — the debounced persistence effect won't fire before page unload
localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(config));
try {
localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(config));
} catch {
pushNotification("localStorage:config", "Config write failed — storage may be full", "warning");
}
window.location.replace("/dashboard");
}

Expand Down Expand Up @@ -116,6 +125,14 @@ export default function OnboardingWizard() {
upstreamRepos={upstreamRepos()}
onUpstreamChange={setUpstreamRepos}
trackedUsers={config.trackedUsers}
monitoredRepos={monitoredRepos()}
onMonitorToggle={(repo, monitored) => {
if (monitored) {
setMonitoredRepos((prev) => prev.some((r) => r.fullName === repo.fullName) ? prev : [...prev, repo]);
} else {
setMonitoredRepos((prev) => prev.filter((r) => r.fullName !== repo.fullName));
}
}}
/>
</Match>
</Switch>
Expand Down
70 changes: 51 additions & 19 deletions src/app/components/onboarding/RepoSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { fetchOrgs, fetchRepos, discoverUpstreamRepos, OrgEntry, RepoRef, RepoEntry } from "../../services/api";
import { getClient } from "../../services/github";
import { user } from "../../stores/auth";
import type { TrackedUser } from "../../stores/config";
import { relativeTime } from "../../lib/format";
import LoadingSpinner from "../shared/LoadingSpinner";
import FilterInput from "../shared/FilterInput";
Expand All @@ -25,7 +26,9 @@ interface RepoSelectorProps {
showUpstreamDiscovery?: boolean;
upstreamRepos?: RepoRef[];
onUpstreamChange?: (repos: RepoRef[]) => void;
trackedUsers?: { login: string; avatarUrl: string; name: string | null }[];
trackedUsers?: TrackedUser[];
monitoredRepos?: RepoRef[];
onMonitorToggle?: (repo: RepoRef, monitored: boolean) => void;
}

interface OrgRepoState {
Expand Down Expand Up @@ -225,6 +228,10 @@ export default function RepoSelector(props: RepoSelectorProps) {
new Set(props.selected.map((r) => r.fullName))
);

const monitoredSet = createMemo(() =>
new Set((props.monitoredRepos ?? []).map((r) => r.fullName))
);

const sortedOrgStates = createMemo(() => {
const states = orgStates();
// Defer sorting until all orgs have loaded: prevents layout shift during
Expand Down Expand Up @@ -527,26 +534,51 @@ export default function RepoSelector(props: RepoSelectorProps) {
{(repo) => {
return (
<li>
<label class="flex cursor-pointer items-start gap-3 px-4 py-3 hover:bg-base-200">
<input
type="checkbox"
checked={isSelected(repo().fullName)}
onChange={() => toggleRepo(repo())}
class="checkbox checkbox-primary checkbox-sm mt-0.5"
/>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="min-w-0 truncate text-sm font-medium text-base-content">
{repo().name}
</span>
<Show when={repo().pushedAt}>
<span class="ml-auto shrink-0 text-xs text-base-content/60">
{relativeTime(repo().pushedAt!)}
<div class="flex items-center">
<label class="flex cursor-pointer items-start gap-3 px-4 py-3 hover:bg-base-200 flex-1">
<input
type="checkbox"
checked={isSelected(repo().fullName)}
onChange={() => toggleRepo(repo())}
class="checkbox checkbox-primary checkbox-sm mt-0.5"
/>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="min-w-0 truncate text-sm font-medium text-base-content">
{repo().name}
</span>
</Show>
<Show when={repo().pushedAt}>
<span class="ml-auto shrink-0 text-xs text-base-content/60">
{relativeTime(repo().pushedAt!)}
</span>
</Show>
</div>
</div>
</div>
</label>
</label>
<Show when={isSelected(repo().fullName) && props.onMonitorToggle && !upstreamSelectedSet().has(repo().fullName)}>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
props.onMonitorToggle?.(toRepoRef(repo()), !monitoredSet().has(repo().fullName));
}}
class="btn btn-ghost btn-sm btn-circle mr-2"
classList={{
"text-info": monitoredSet().has(repo().fullName),
"text-base-content/20": !monitoredSet().has(repo().fullName),
}}
title={monitoredSet().has(repo().fullName) ? "Stop monitoring all activity" : "Monitor all activity"}
aria-label={monitoredSet().has(repo().fullName) ? "Stop monitoring all activity" : "Monitor all activity"}
aria-pressed={monitoredSet().has(repo().fullName)}
>
{/* Heroicons eye outline 16px */}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width={2} aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
</Show>
</div>
</li>
);
}}
Expand Down
20 changes: 18 additions & 2 deletions src/app/components/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createSignal, Show, onCleanup } from "solid-js";
import { createSignal, createMemo, Show, onCleanup } from "solid-js";
import { useNavigate } from "@solidjs/router";
import { config, updateConfig } from "../../stores/config";
import { config, updateConfig, setMonitoredRepo } from "../../stores/config";
import { clearAuth } from "../../stores/auth";
import { clearCache } from "../../stores/cache";
import { pushNotification } from "../../lib/errors";
Expand Down Expand Up @@ -54,6 +54,10 @@ export default function SettingsPage() {
const [localRepos, setLocalRepos] = createSignal<RepoRef[]>(config.selectedRepos);
const [localUpstream, setLocalUpstream] = createSignal<RepoRef[]>(config.upstreamRepos);

const monitoredRepoNames = createMemo(() =>
config.monitoredRepos.map(r => r.fullName).join(", ")
);

// ── Helpers ──────────────────────────────────────────────────────────────

async function mergeNewOrgs() {
Expand Down Expand Up @@ -142,6 +146,7 @@ export default function SettingsPage() {
selectedOrgs: config.selectedOrgs,
selectedRepos: config.selectedRepos,
upstreamRepos: config.upstreamRepos,
monitoredRepos: config.monitoredRepos,
trackedUsers: config.trackedUsers,
refreshInterval: config.refreshInterval,
hotPollInterval: config.hotPollInterval,
Expand Down Expand Up @@ -304,6 +309,15 @@ export default function SettingsPage() {
Tracking {localRepos().length} repos will use significant API quota per poll cycle
</p>
</Show>
<Show when={config.monitoredRepos.length > 0}>
<p class="text-xs text-info flex items-center gap-1 mt-0.5">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width={2} aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
Monitoring all: {monitoredRepoNames()}
</p>
</Show>
</div>
<button
type="button"
Expand All @@ -324,6 +338,8 @@ export default function SettingsPage() {
upstreamRepos={localUpstream()}
onUpstreamChange={handleUpstreamChange}
trackedUsers={config.trackedUsers}
monitoredRepos={config.monitoredRepos}
onMonitorToggle={setMonitoredRepo}
/>
</div>
</Show>
Expand Down
Loading
Loading