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
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,5 @@ Scope is optional. Use imperative mood: "add feature", not "adds feature" or "ad
All PRs target `main` on `gordon-code/github-tracker`. Keep PRs focused — one feature or fix per PR makes review faster and reverts cleaner.

In the PR body, describe what changed and why. CI runs typecheck, unit tests, and E2E tests automatically. PRs need a passing CI run before merge.

When adding or changing user-facing features, update [docs/USER_GUIDE.md](docs/USER_GUIDE.md) to reflect the changes.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ A dashboard for tracking GitHub issues, PRs, and Actions workflow runs across ma

![Dashboard](docs/dashboard-screenshot.png)

## Documentation

For detailed feature documentation, see the [User Guide](docs/USER_GUIDE.md).

## Features

### Issues
Expand All @@ -18,7 +22,7 @@ Open issues where you're the creator, assignee, or mentioned. A scope filter let

### Pull Requests

Open PRs with CI status dots (green/yellow/red), review decision badges, size badges (XS–XXL by lines changed), and draft indicators. A "blocked" filter catches PRs where checks are failing or a review requested changes. The scope filter works here too. Reviewer avatars stack for multiple reviewers.
Open PRs with CI status dots (green/yellow/red), review decision badges, size badges (XS–XL by lines changed), and draft indicators. A "blocked" filter catches PRs where checks are failing or a review requested changes. The scope filter works here too. Reviewer avatars stack for multiple reviewers.

### Actions

Expand Down Expand Up @@ -96,11 +100,11 @@ src/
layout/ # Header, TabBar, FilterBar
onboarding/ # OnboardingWizard, OrgSelector, RepoSelector
settings/ # SettingsPage, TrackedUsersSection, ThemePicker, Section, SettingRow
shared/ # 18 shared components: FilterInput, FilterChips, StatusDot,
shared/ # 19 shared components: FilterInput, FilterChips, StatusDot,
# ReviewBadge, SizeBadge, RoleBadge, SortDropdown, PaginationControls,
# LoadingSpinner, SkeletonRows, ToastContainer, NotificationDrawer,
# RepoLockControls, UserAvatarBadge, ExpandCollapseButtons,
# RepoGitHubLink, ChevronIcon, ExternalLinkIcon
# RepoGitHubLink, ChevronIcon, ExternalLinkIcon, Tooltip/InfoTooltip
lib/ # 14 modules: format, errors, notifications, oauth, pat, url,
# flashDetection, grouping, reorderHighlight, collections,
# emoji, label-colors, sentry, github-emoji-map.json
Expand All @@ -117,8 +121,8 @@ src/
view.ts # View state (tabs, sorting, filters, ignored items, locked repos)
worker/
index.ts # OAuth token exchange endpoint, CORS, security headers
tests/ # 1522 unit/component tests across 69 test files
e2e/ # 14 E2E tests across 2 spec files
tests/ # unit/component tests across 70 test files
e2e/ # 15 E2E tests across 3 spec files
```

## Development
Expand Down
409 changes: 409 additions & 0 deletions docs/USER_GUIDE.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions e2e/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { setupAuth } from "./helpers";
test("settings page renders section headings", async ({ page }) => {
await setupAuth(page);
await page.goto("/settings");
await page.waitForLoadState("networkidle");

await expect(
page.getByRole("heading", { name: /organizations & repositories/i })
Expand Down
14 changes: 12 additions & 2 deletions src/app/components/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { pushNotification } from "../../lib/errors";
import { getClient, getGraphqlRateLimit } from "../../services/github";
import { formatCount } from "../../lib/format";
import { setsEqual } from "../../lib/collections";
import { Tooltip } from "../shared/Tooltip";

// ── Shared dashboard store (module-level to survive navigation) ─────────────

Expand Down Expand Up @@ -450,6 +451,15 @@ export default function DashboardPage() {
Source
</a>
<span aria-hidden="true">&middot;</span>
<a
href="https://github.com/gordon-code/github-tracker/blob/main/docs/USER_GUIDE.md"
target="_blank"
rel="noopener noreferrer"
class="link link-hover"
>
Guide
</a>
<span aria-hidden="true">&middot;</span>
<a
href="/privacy"
class="link link-hover"
Expand All @@ -460,11 +470,11 @@ export default function DashboardPage() {
<div class="flex justify-end">
<Show when={getGraphqlRateLimit()}>
{(rl) => (
<div class="tooltip tooltip-left" data-tip={`GraphQL API Rate Limits — resets at ${rl().resetAt.toLocaleTimeString()}`}>
<Tooltip content={`GraphQL API Rate Limits — resets at ${rl().resetAt.toLocaleTimeString()}`} placement="left" focusable>
<span class={`tabular-nums ${rl().remaining < rl().limit * 0.1 ? "text-warning" : ""}`}>
API RL: {rl().remaining.toLocaleString()}/{formatCount(rl().limit)}/hr
</span>
</div>
</Tooltip>
)}
</Show>
</div>
Expand Down
9 changes: 6 additions & 3 deletions src/app/components/dashboard/IgnoreBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createSignal, For, Show } from "solid-js";
import type { IgnoredItem } from "../../stores/view";
import { Tooltip } from "../shared/Tooltip";

interface IgnoreBadgeProps {
items: IgnoredItem[];
Expand Down Expand Up @@ -84,9 +85,11 @@ export default function IgnoreBadge(props: IgnoreBadgeProps) {
<p class="text-xs text-base-content/60 truncate">
{item.repo}
</p>
<p class="text-sm text-base-content truncate" title={item.title}>
{item.title}
</p>
<Tooltip content={item.title} class="min-w-0 w-full">
<p class="text-sm text-base-content truncate">
{item.title}
</p>
</Tooltip>
<p class="text-xs text-base-content/40">
Ignored {formatDate(item.ignoredAt)}
</p>
Expand Down
28 changes: 16 additions & 12 deletions src/app/components/dashboard/IssuesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { groupByRepo, computePageLayout, slicePageGroups, orderRepoGroups, isUse
import { createReorderHighlight } from "../../lib/reorderHighlight";
import RepoLockControls from "../shared/RepoLockControls";
import RepoGitHubLink from "../shared/RepoGitHubLink";
import { Tooltip } from "../shared/Tooltip";

export interface IssuesTabProps {
issues: Issue[];
Expand Down Expand Up @@ -271,17 +272,18 @@ export default function IssuesTab(props: IssuesTabProps) {
setPage(0);
}}
/>
<button
onClick={() => {
updateViewState({ hideDepDashboard: !viewState.hideDepDashboard });
setPage(0);
}}
class={`btn btn-xs rounded-full ${!viewState.hideDepDashboard ? "btn-primary" : "btn-ghost text-base-content/50"}`}
aria-pressed={!viewState.hideDepDashboard}
title="Toggle visibility of Dependency Dashboard issues"
>
Show Dep Dashboard
</button>
<Tooltip content="Show or hide Renovate Dependency Dashboard issues">
<button
onClick={() => {
updateViewState({ hideDepDashboard: !viewState.hideDepDashboard });
setPage(0);
}}
class={`btn btn-xs rounded-full ${!viewState.hideDepDashboard ? "btn-primary" : "btn-ghost text-base-content/50"}`}
aria-pressed={!viewState.hideDepDashboard}
>
Show Dep Dashboard
</button>
</Tooltip>
</div>
<div class="shrink-0 flex items-center gap-2 py-0.5">
<ExpandCollapseButtons
Expand Down Expand Up @@ -360,7 +362,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>
<Tooltip content="Showing all activity, not just yours" focusable>
<span class="badge badge-xs badge-ghost" aria-label="monitoring all activity">Monitoring all</span>
</Tooltip>
</Show>
<Show when={repoGroup.starCount != null && repoGroup.starCount > 0}>
<span class="text-xs text-base-content/50 font-normal" aria-label={`${repoGroup.starCount} stars`}>
Expand Down
133 changes: 70 additions & 63 deletions src/app/components/dashboard/ItemRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isSafeGitHubUrl } from "../../lib/url";
import { relativeTime, shortRelativeTime, formatCount } from "../../lib/format";
import { expandEmoji } from "../../lib/emoji";
import { labelColorClass } from "../../lib/label-colors";
import { Tooltip } from "../shared/Tooltip";

export interface ItemRowProps {
repo: string;
Expand Down Expand Up @@ -78,14 +79,15 @@ export default function ItemRow(props: ItemRowProps) {

{/* Repo badge */}
<Show when={!props.hideRepo}>
<span
class={`shrink-0 inline-flex items-center rounded-full font-mono font-medium
bg-primary/10 text-primary
${isCompact() ? "text-xs px-2 py-0.5" : "text-xs px-2.5 py-1"}`}
title={props.repo}
>
{props.repo}
</span>
<Tooltip content={props.repo} class="shrink-0 relative z-10">
<span
class={`shrink-0 inline-flex items-center rounded-full font-mono font-medium
bg-primary/10 text-primary
${isCompact() ? "text-xs px-2 py-0.5" : "text-xs px-2.5 py-1"}`}
>
{props.repo}
</span>
</Tooltip>
</Show>

{/* Main content */}
Expand Down Expand Up @@ -127,75 +129,80 @@ export default function ItemRow(props: ItemRowProps) {
<div class="relative z-10">{props.surfacedByBadge}</div>
</Show>
<span class="inline-flex items-center gap-1 whitespace-nowrap">
<time
datetime={props.createdAt}
title={staticDateInfo().createdTitle}
aria-label={dateDisplay().createdLabel}
>
{dateDisplay().created}
</time>
<Show when={shouldShowUpdated()}>
<span aria-hidden="true">{"\u00B7"}</span>
<Tooltip content={staticDateInfo().createdTitle} class="relative z-10">
<time
datetime={props.updatedAt}
title={staticDateInfo().updatedTitle}
aria-label={dateDisplay().updatedLabel}
datetime={props.createdAt}
aria-label={dateDisplay().createdLabel}
>
{dateDisplay().updated}
{dateDisplay().created}
</time>
</Tooltip>
<Show when={shouldShowUpdated()}>
<span aria-hidden="true">{"\u00B7"}</span>
<Tooltip content={staticDateInfo().updatedTitle} class="relative z-10">
<time
datetime={props.updatedAt}
aria-label={dateDisplay().updatedLabel}
>
{dateDisplay().updated}
</time>
</Tooltip>
</Show>
</span>
<Show when={props.isPolling}>
<span class="loading loading-spinner loading-xs text-base-content/40" />
</Show>
<Show when={(props.commentCount ?? 0) > 0}>
<span class="flex items-center gap-0.5">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z"
clip-rule="evenodd"
/>
</svg>
{formatCount(props.commentCount!)}
</span>
<Tooltip content={`${props.commentCount} total ${props.commentCount === 1 ? "comment" : "comments"}`} focusable class="relative z-10">
<span class="flex items-center gap-0.5">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z"
clip-rule="evenodd"
/>
</svg>
{formatCount(props.commentCount!)}
</span>
</Tooltip>
</Show>
</div>

{/* Ignore button — visible on hover */}
<button
data-ignore-btn
onClick={() => props.onIgnore()}
class={`relative z-10 shrink-0 self-center rounded p-1
text-base-content/30
hover:text-error
opacity-0 group-hover:opacity-100 focus:opacity-100
transition-opacity focus:outline-none focus:ring-2 focus:ring-error`}
title="Ignore this item"
aria-label={`Ignore #${props.number} ${props.title}`}
>
{/* Eye-slash icon */}
<svg
xmlns="http://www.w3.org/2000/svg"
class={isCompact() ? "h-3.5 w-3.5" : "h-4 w-4"}
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
<Tooltip content="Ignore" class="shrink-0 self-center relative z-10">
<button
data-ignore-btn
onClick={() => props.onIgnore()}
class={`shrink-0 self-center rounded p-1
text-base-content/30
hover:text-error
opacity-0 group-hover:opacity-100 focus:opacity-100
transition-opacity focus:outline-none focus:ring-2 focus:ring-error`}
aria-label={`Ignore #${props.number} ${props.title}`}
>
<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>
</button>
{/* Eye-slash icon */}
<svg
xmlns="http://www.w3.org/2000/svg"
class={isCompact() ? "h-3.5 w-3.5" : "h-4 w-4"}
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>
</button>
</Tooltip>
</div>
);
}
2 changes: 2 additions & 0 deletions src/app/components/dashboard/PersonalSummaryStrip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createMemo, For, Show } from "solid-js";
import type { Issue, PullRequest, WorkflowRun } from "../../services/api";
import type { TabId } from "../layout/TabBar";
import { viewState, updateViewState, resetAllTabFilters, setTabFilter } from "../../stores/view";
import { InfoTooltip } from "../shared/Tooltip";

interface SummaryCount {
label: string;
Expand Down Expand Up @@ -176,6 +177,7 @@ export default function PersonalSummaryStrip(props: PersonalSummaryStripProps) {
</>
)}
</For>
<InfoTooltip content="Click any count to view those items." placement="bottom" />
</div>
</Show>
);
Expand Down
17 changes: 11 additions & 6 deletions src/app/components/dashboard/PullRequestsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { createReorderHighlight } from "../../lib/reorderHighlight";
import { createFlashDetection } from "../../lib/flashDetection";
import RepoLockControls from "../shared/RepoLockControls";
import RepoGitHubLink from "../shared/RepoGitHubLink";
import { Tooltip } from "../shared/Tooltip";

export interface PullRequestsTabProps {
pullRequests: PullRequest[];
Expand Down Expand Up @@ -470,7 +471,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>
<Tooltip content="Showing all activity, not just yours" focusable>
<span class="badge badge-xs badge-ghost" aria-label="monitoring all activity">Monitoring all</span>
</Tooltip>
</Show>
<Show when={repoGroup.starCount != null && repoGroup.starCount > 0}>
<span class="text-xs text-base-content/50 font-normal" aria-label={`${repoGroup.starCount} stars`}>
Expand Down Expand Up @@ -605,11 +608,13 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
</span>
</Show>
<Show when={pr.enriched !== false && pr.reviewerLogins.length > 0}>
<span class="text-xs text-base-content/60" title={pr.reviewerLogins.join(", ")}>
Reviewers: {pr.reviewerLogins.slice(0, 5).join(", ")}
{pr.reviewerLogins.length > 5 && ` +${pr.reviewerLogins.length - 5} more`}
{pr.totalReviewCount > pr.reviewerLogins.length && ` (${pr.totalReviewCount} total)`}
</span>
<Tooltip content={pr.reviewerLogins.join(", ")} focusable>
<span class="text-xs text-base-content/60">
Reviewers: {pr.reviewerLogins.slice(0, 5).join(", ")}
{pr.reviewerLogins.length > 5 && ` +${pr.reviewerLogins.length - 5} more`}
{pr.totalReviewCount > pr.reviewerLogins.length && ` (${pr.totalReviewCount} total)`}
</span>
</Tooltip>
</Show>
</div>
</ItemRow>
Expand Down
Loading