Skip to content

Commit 761b500

Browse files
committed
feat: adds bot user tracking and per-repo monitor-all mode
1 parent 08d7fd5 commit 761b500

20 files changed

+1290
-108
lines changed

src/app/components/dashboard/DashboardPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ export default function DashboardPage() {
359359
userLogin={userLogin()}
360360
allUsers={allUsers()}
361361
trackedUsers={config.trackedUsers}
362+
monitoredRepos={config.monitoredRepos}
362363
/>
363364
</Match>
364365
<Match when={activeTab() === "pullRequests"}>
@@ -369,6 +370,7 @@ export default function DashboardPage() {
369370
allUsers={allUsers()}
370371
trackedUsers={config.trackedUsers}
371372
hotPollingPRIds={hotPollingPRIds()}
373+
monitoredRepos={config.monitoredRepos}
372374
/>
373375
</Match>
374376
<Match when={activeTab() === "actions"}>

src/app/components/dashboard/IssuesTab.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface IssuesTabProps {
2525
userLogin: string;
2626
allUsers?: { login: string; label: string }[];
2727
trackedUsers?: TrackedUser[];
28+
monitoredRepos?: { fullName: string }[];
2829
}
2930

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

72+
const monitoredRepoNameSet = createMemo(() =>
73+
new Set((props.monitoredRepos ?? []).map(r => r.fullName))
74+
);
75+
7176
const filterGroups = createMemo<FilterChipGroupDef[]>(() => {
7277
const users = props.allUsers;
7378
if (!users || users.length <= 1) return issueFilterGroups;
@@ -114,10 +119,13 @@ export default function IssuesTab(props: IssuesTabProps) {
114119
}
115120

116121
if (tabFilter.user !== "all") {
117-
const validUser = !props.allUsers || props.allUsers.some(u => u.login === tabFilter.user);
118-
if (validUser) {
119-
const surfacedBy = issue.surfacedBy ?? [props.userLogin.toLowerCase()];
120-
if (!surfacedBy.includes(tabFilter.user)) return false;
122+
// Items from monitored repos bypass the surfacedBy filter (all activity is shown)
123+
if (!monitoredRepoNameSet().has(issue.repoFullName)) {
124+
const validUser = !props.allUsers || props.allUsers.some(u => u.login === tabFilter.user);
125+
if (validUser) {
126+
const surfacedBy = issue.surfacedBy ?? [props.userLogin.toLowerCase()];
127+
if (!surfacedBy.includes(tabFilter.user)) return false;
128+
}
121129
}
122130
}
123131

@@ -305,6 +313,9 @@ export default function IssuesTab(props: IssuesTabProps) {
305313
>
306314
<ChevronIcon size="md" rotated={!isExpanded()} />
307315
{repoGroup.repoFullName}
316+
<Show when={monitoredRepoNameSet().has(repoGroup.repoFullName)}>
317+
<span class="badge badge-xs badge-ghost" aria-label="monitoring all activity">Monitoring all</span>
318+
</Show>
308319
<Show when={!isExpanded()}>
309320
<span class="ml-auto flex items-center gap-2 text-xs font-normal text-base-content/60">
310321
<span>{repoGroup.items.length} {repoGroup.items.length === 1 ? "issue" : "issues"}</span>

src/app/components/dashboard/PullRequestsTab.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface PullRequestsTabProps {
3030
allUsers?: { login: string; label: string }[];
3131
trackedUsers?: TrackedUser[];
3232
hotPollingPRIds?: ReadonlySet<number>;
33+
monitoredRepos?: { fullName: string }[];
3334
}
3435

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

139+
const monitoredRepoNameSet = createMemo(() =>
140+
new Set((props.monitoredRepos ?? []).map(r => r.fullName))
141+
);
142+
138143
const filterGroups = createMemo<FilterChipGroupDef[]>(() => {
139144
const users = props.allUsers;
140145
if (!users || users.length <= 1) return prFilterGroups;
@@ -199,10 +204,13 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
199204
}
200205

201206
if (tabFilters.user !== "all") {
202-
const validUser = !props.allUsers || props.allUsers.some(u => u.login === tabFilters.user);
203-
if (validUser) {
204-
const surfacedBy = pr.surfacedBy ?? [props.userLogin.toLowerCase()];
205-
if (!surfacedBy.includes(tabFilters.user)) return false;
207+
// Items from monitored repos bypass the surfacedBy filter (all activity is shown)
208+
if (!monitoredRepoNameSet().has(pr.repoFullName)) {
209+
const validUser = !props.allUsers || props.allUsers.some(u => u.login === tabFilters.user);
210+
if (validUser) {
211+
const surfacedBy = pr.surfacedBy ?? [props.userLogin.toLowerCase()];
212+
if (!surfacedBy.includes(tabFilters.user)) return false;
213+
}
206214
}
207215
}
208216

@@ -418,6 +426,9 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
418426
>
419427
<ChevronIcon size="md" rotated={!isExpanded()} />
420428
{repoGroup.repoFullName}
429+
<Show when={monitoredRepoNameSet().has(repoGroup.repoFullName)}>
430+
<span class="badge badge-xs badge-ghost" aria-label="monitoring all activity">Monitoring all</span>
431+
</Show>
421432
<Show when={!isExpanded()}>
422433
<span class="ml-auto flex items-center gap-2 text-xs font-normal text-base-content/60 shrink-0">
423434
<span>{repoGroup.items.length} {repoGroup.items.length === 1 ? "PR" : "PRs"}</span>

src/app/components/onboarding/OnboardingWizard.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export default function OnboardingWizard() {
1919
const [upstreamRepos, setUpstreamRepos] = createSignal<RepoRef[]>(
2020
config.upstreamRepos.length > 0 ? [...config.upstreamRepos] : []
2121
);
22+
const [monitoredRepos, setMonitoredRepos] = createSignal<RepoRef[]>(
23+
config.monitoredRepos.length > 0 ? [...config.monitoredRepos] : []
24+
);
2225

2326
const [loading, setLoading] = createSignal(true);
2427
const [error, setError] = createSignal<string | null>(null);
@@ -53,10 +56,14 @@ export default function OnboardingWizard() {
5356

5457
function handleFinish() {
5558
const uniqueOrgs = [...new Set(selectedRepos().map((r) => r.owner))];
59+
// Prune monitoredRepos to only repos still in selectedRepos
60+
const selectedSet = new Set(selectedRepos().map((r) => r.fullName));
61+
const prunedMonitoredRepos = monitoredRepos().filter((r) => selectedSet.has(r.fullName));
5662
updateConfig({
5763
selectedOrgs: uniqueOrgs,
5864
selectedRepos: selectedRepos(),
5965
upstreamRepos: upstreamRepos(),
66+
monitoredRepos: prunedMonitoredRepos,
6067
onboardingComplete: true,
6168
});
6269
// Flush synchronously — the debounced persistence effect won't fire before page unload
@@ -116,6 +123,14 @@ export default function OnboardingWizard() {
116123
upstreamRepos={upstreamRepos()}
117124
onUpstreamChange={setUpstreamRepos}
118125
trackedUsers={config.trackedUsers}
126+
monitoredRepos={monitoredRepos()}
127+
onMonitorToggle={(repo, monitored) => {
128+
if (monitored) {
129+
setMonitoredRepos((prev) => prev.some((r) => r.fullName === repo.fullName) ? prev : [...prev, repo]);
130+
} else {
131+
setMonitoredRepos((prev) => prev.filter((r) => r.fullName !== repo.fullName));
132+
}
133+
}}
119134
/>
120135
</Match>
121136
</Switch>

src/app/components/onboarding/RepoSelector.tsx

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { fetchOrgs, fetchRepos, discoverUpstreamRepos, OrgEntry, RepoRef, RepoEntry } from "../../services/api";
1111
import { getClient } from "../../services/github";
1212
import { user } from "../../stores/auth";
13+
import type { TrackedUser } from "../../stores/config";
1314
import { relativeTime } from "../../lib/format";
1415
import LoadingSpinner from "../shared/LoadingSpinner";
1516
import FilterInput from "../shared/FilterInput";
@@ -25,7 +26,9 @@ interface RepoSelectorProps {
2526
showUpstreamDiscovery?: boolean;
2627
upstreamRepos?: RepoRef[];
2728
onUpstreamChange?: (repos: RepoRef[]) => void;
28-
trackedUsers?: { login: string; avatarUrl: string; name: string | null }[];
29+
trackedUsers?: TrackedUser[];
30+
monitoredRepos?: RepoRef[];
31+
onMonitorToggle?: (repo: RepoRef, monitored: boolean) => void;
2932
}
3033

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

231+
const monitoredSet = createMemo(() =>
232+
new Set((props.monitoredRepos ?? []).map((r) => r.fullName))
233+
);
234+
228235
const sortedOrgStates = createMemo(() => {
229236
const states = orgStates();
230237
// Defer sorting until all orgs have loaded: prevents layout shift during
@@ -527,26 +534,48 @@ export default function RepoSelector(props: RepoSelectorProps) {
527534
{(repo) => {
528535
return (
529536
<li>
530-
<label class="flex cursor-pointer items-start gap-3 px-4 py-3 hover:bg-base-200">
531-
<input
532-
type="checkbox"
533-
checked={isSelected(repo().fullName)}
534-
onChange={() => toggleRepo(repo())}
535-
class="checkbox checkbox-primary checkbox-sm mt-0.5"
536-
/>
537-
<div class="min-w-0 flex-1">
538-
<div class="flex items-center gap-2">
539-
<span class="min-w-0 truncate text-sm font-medium text-base-content">
540-
{repo().name}
541-
</span>
542-
<Show when={repo().pushedAt}>
543-
<span class="ml-auto shrink-0 text-xs text-base-content/60">
544-
{relativeTime(repo().pushedAt!)}
537+
<div class="flex items-center">
538+
<label class="flex cursor-pointer items-start gap-3 px-4 py-3 hover:bg-base-200 flex-1">
539+
<input
540+
type="checkbox"
541+
checked={isSelected(repo().fullName)}
542+
onChange={() => toggleRepo(repo())}
543+
class="checkbox checkbox-primary checkbox-sm mt-0.5"
544+
/>
545+
<div class="min-w-0 flex-1">
546+
<div class="flex items-center gap-2">
547+
<span class="min-w-0 truncate text-sm font-medium text-base-content">
548+
{repo().name}
545549
</span>
546-
</Show>
550+
<Show when={repo().pushedAt}>
551+
<span class="ml-auto shrink-0 text-xs text-base-content/60">
552+
{relativeTime(repo().pushedAt!)}
553+
</span>
554+
</Show>
555+
</div>
547556
</div>
548-
</div>
549-
</label>
557+
</label>
558+
<Show when={isSelected(repo().fullName) && props.onMonitorToggle && !upstreamSelectedSet().has(repo().fullName)}>
559+
<button
560+
type="button"
561+
onClick={(e) => {
562+
e.stopPropagation();
563+
props.onMonitorToggle?.(toRepoRef(repo()), !monitoredSet().has(repo().fullName));
564+
}}
565+
class="btn btn-ghost btn-sm btn-circle mr-2"
566+
classList={{ "opacity-40": !monitoredSet().has(repo().fullName) }}
567+
title={monitoredSet().has(repo().fullName) ? "Stop monitoring all activity" : "Monitor all activity"}
568+
aria-label={monitoredSet().has(repo().fullName) ? "Stop monitoring all activity" : "Monitor all activity"}
569+
aria-pressed={monitoredSet().has(repo().fullName)}
570+
>
571+
{/* Heroicons eye outline 16px */}
572+
<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">
573+
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
574+
<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" />
575+
</svg>
576+
</button>
577+
</Show>
578+
</div>
550579
</li>
551580
);
552581
}}

src/app/components/settings/SettingsPage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createSignal, Show, onCleanup } from "solid-js";
22
import { useNavigate } from "@solidjs/router";
3-
import { config, updateConfig } from "../../stores/config";
3+
import { config, updateConfig, setMonitoredRepo } from "../../stores/config";
44
import { clearAuth } from "../../stores/auth";
55
import { clearCache } from "../../stores/cache";
66
import { pushNotification } from "../../lib/errors";
@@ -142,6 +142,7 @@ export default function SettingsPage() {
142142
selectedOrgs: config.selectedOrgs,
143143
selectedRepos: config.selectedRepos,
144144
upstreamRepos: config.upstreamRepos,
145+
monitoredRepos: config.monitoredRepos,
145146
trackedUsers: config.trackedUsers,
146147
refreshInterval: config.refreshInterval,
147148
hotPollInterval: config.hotPollInterval,
@@ -324,6 +325,8 @@ export default function SettingsPage() {
324325
upstreamRepos={localUpstream()}
325326
onUpstreamChange={handleUpstreamChange}
326327
trackedUsers={config.trackedUsers}
328+
monitoredRepos={config.monitoredRepos}
329+
onMonitorToggle={(repo, monitored) => setMonitoredRepo(repo, monitored)}
327330
/>
328331
</div>
329332
</Show>

src/app/components/settings/TrackedUsersSection.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,16 @@ export default function TrackedUsersSection(props: TrackedUsersSectionProps) {
118118
<span class="text-sm font-medium truncate">
119119
{trackedUser.name ?? trackedUser.login}
120120
</span>
121-
<Show when={trackedUser.name}>
122-
<span class="text-xs text-base-content/60 truncate">
123-
{trackedUser.login}
124-
</span>
125-
</Show>
121+
<div class="flex items-center gap-1">
122+
<Show when={trackedUser.name}>
123+
<span class="text-xs text-base-content/60 truncate">
124+
{trackedUser.login}
125+
</span>
126+
</Show>
127+
<Show when={trackedUser.type === "bot"}>
128+
<span class="badge badge-xs badge-outline" aria-label={`${trackedUser.login} is a bot account`}>bot</span>
129+
</Show>
130+
</div>
126131
</div>
127132
<button
128133
type="button"

0 commit comments

Comments
 (0)