Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
a4cb8d4
chore: project scaffolding with SolidJS + Tailwind + CF Workers
wgordon17 Mar 20, 2026
065f8e2
feat: adds IndexedDB cache with ETag support
wgordon17 Mar 20, 2026
c7a5d86
feat: adds Worker OAuth endpoint with security headers
wgordon17 Mar 20, 2026
f18105e
feat: add config and view state stores
wgordon17 Mar 20, 2026
56f25c5
feat: adds GitHub OAuth login flow with CSRF protection
wgordon17 Mar 20, 2026
78f2626
feat: adds OAuth login flow with CSRF protection
wgordon17 Mar 20, 2026
bf7c021
ci: adds deploy and preview workflows
wgordon17 Mar 20, 2026
36b1a1a
feat: adds Octokit client with ETag caching
wgordon17 Mar 20, 2026
1e007e0
feat: adds dashboard shell with header and tabs
wgordon17 Mar 20, 2026
cfe7295
feat: adds GitHub API service layer with fixtures
wgordon17 Mar 20, 2026
4b19ddd
feat: adds onboarding wizard with org selection
wgordon17 Mar 20, 2026
fa3417c
feat: adds issues tab with sortable item rows
wgordon17 Mar 20, 2026
9a22512
feat: adds GHA actions tab with workflow runs
wgordon17 Mar 20, 2026
fdaa39f
docs: adds project README with structure overview
wgordon17 Mar 20, 2026
b2a5660
feat: adds repo selection to onboarding wizard
wgordon17 Mar 20, 2026
77fa2e6
feat: adds pull requests tab with check status
wgordon17 Mar 20, 2026
c4ef895
feat: adds poll coordinator with visibility-aware refresh
wgordon17 Mar 20, 2026
9bf9516
feat: adds ignore/unignore system with badge
wgordon17 Mar 20, 2026
d48d814
feat: adds settings page with all options
wgordon17 Mar 20, 2026
b2ac158
feat: adds desktop notifications for new items
wgordon17 Mar 20, 2026
4678e82
feat: adds dark mode support across all components
wgordon17 Mar 21, 2026
e2b49fb
fix: addresses critical QA and security review findings
wgordon17 Mar 21, 2026
db287ce
fix: addresses memory leak and performance review findings
wgordon17 Mar 21, 2026
9511f7a
docs: adds swarm report and updates README with full feature list
wgordon17 Mar 21, 2026
aa65c11
fix: addresses quality gate adversarial review findings
wgordon17 Mar 21, 2026
f20cd92
fix: uses cursor-based eviction in evictStaleEntries (ADV-016)
wgordon17 Mar 21, 2026
cf80526
fix: addresses post-MVP review findings (ADV/SEC/PERF/QA)
wgordon17 Mar 21, 2026
dfe06fd
perf: migrates to GitHub Search API for issues and PRs
wgordon17 Mar 21, 2026
c55b5c6
perf: batches PR check status into single GraphQL call
wgordon17 Mar 21, 2026
fc07cdd
chore: removes dead fixture data and simplifies PR dedup wrapper
wgordon17 Mar 21, 2026
a8431be
fix: addresses domain review findings
wgordon17 Mar 21, 2026
e81c739
fix: adds warning when search results hit 1000-item cap
wgordon17 Mar 21, 2026
81edc40
fix: adds error logging for all silent failure paths
wgordon17 Mar 21, 2026
4daf458
feat: adds error propagation, workflow pagination, and cache cleanup
wgordon17 Mar 21, 2026
222b2cf
perf: optimizes API call efficiency with 7 targeted improvements
wgordon17 Mar 21, 2026
d6cc653
fix: corrects quality gate findings in API optimization
wgordon17 Mar 21, 2026
730e1b3
fix: adds max staleness threshold to notifications gate
wgordon17 Mar 21, 2026
8af073c
fix: addresses Layer 2 subagent review findings
wgordon17 Mar 21, 2026
a867909
fix: disables notifications gate on 403 and fixes test data shape
wgordon17 Mar 21, 2026
33297cd
fix: breaks circular dependency between auth.ts and poll.ts
wgordon17 Mar 21, 2026
f2e8668
feat(ci): adds preview deployments and E2E test step
wgordon17 Mar 21, 2026
4572c23
test: adds comprehensive test coverage (475 tests, E2E)
wgordon17 Mar 21, 2026
e63f8ab
fix(test): adds GraphQL E2E intercept and SDR-013 test
wgordon17 Mar 21, 2026
1bf0477
refactor(test): removes tautological toBeDefined assertions
wgordon17 Mar 21, 2026
8062ed9
fix(test): addresses medium-severity review findings
wgordon17 Mar 21, 2026
42d8af2
refactor(test): migrates fireEvent to userEvent
wgordon17 Mar 21, 2026
dd91d7b
feat: adds metadata UX, filter chips, REST fallback, error surfacing
wgordon17 Mar 22, 2026
a756afd
test: adds metadata, filter, badge, and REST fallback tests
wgordon17 Mar 22, 2026
d8d4e04
feat(errors): surfaces data-loss warnings and deduplicates by source
wgordon17 Mar 22, 2026
67e1583
fix: addresses PR review findings across security, reactivity, and tests
wgordon17 Mar 22, 2026
fe1c3f9
fix: addresses PR review findings, HttpOnly refresh token
wgordon17 Mar 23, 2026
ff10826
fix(config): adds ASSETS binding, scrubs .serena, updates DEPLOY.md
wgordon17 Mar 23, 2026
48a1eec
fix(api): parses repository_url, sequential search, dual rate limits
wgordon17 Mar 23, 2026
3be091e
fix(dashboard): persists state across nav, fixes skeleton flash
wgordon17 Mar 23, 2026
75340c5
feat(ui): adds save indicator, privacy page, dashboard footer
wgordon17 Mar 23, 2026
4d694f1
fix: addresses PR review findings across auth, worker, and tests
wgordon17 Mar 23, 2026
b94bef0
chore: adds .playwright-mcp to gitignore
wgordon17 Mar 23, 2026
2ac4651
chore: adds prek.toml pre-commit hooks
wgordon17 Mar 23, 2026
0f8c60d
chore: adds CSP hash fix, pre-push E2E hook to prek
wgordon17 Mar 23, 2026
f383a3f
fix: resets store state on logout, fixes E2E test assertions
wgordon17 Mar 23, 2026
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
3 changes: 3 additions & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
GITHUB_CLIENT_ID=your_client_id_here
GITHUB_CLIENT_SECRET=your_client_secret_here
ALLOWED_ORIGIN=http://localhost:5173
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# GitHub App client ID — embedded into client-side bundle at build time by Vite.
# This is public information (visible in the OAuth authorize URL).
# Set this as a GitHub Actions variable (not a secret) for CI/CD.
VITE_GITHUB_CLIENT_ID=your_github_app_client_id_here
29 changes: 29 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Deploy
on:
push:
branches: [main]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
- run: pnpm test
- name: Verify CSP hash
run: node scripts/verify-csp-hash.mjs
- run: pnpm run build
env:
VITE_GITHUB_CLIENT_ID: ${{ vars.VITE_GITHUB_CLIENT_ID }}
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy
101 changes: 101 additions & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Preview
on:
pull_request:
permissions:
contents: read
pull-requests: write
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run typecheck
- run: pnpm test
- name: Verify CSP hash
run: node scripts/verify-csp-hash.mjs
- name: Install Playwright browsers
run: npx playwright install chromium --with-deps
- name: Run E2E tests
run: pnpm test:e2e
env:
VITE_GITHUB_CLIENT_ID: ${{ vars.VITE_GITHUB_CLIENT_ID }}
preview:
needs: ci
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Verify CSP hash
run: node scripts/verify-csp-hash.mjs
- run: pnpm run build
env:
VITE_GITHUB_CLIENT_ID: ${{ vars.VITE_GITHUB_CLIENT_ID }}
- name: Slugify branch name
id: slug
env:
BRANCH: ${{ github.head_ref }}
run: echo "alias=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9]/-/g; s/--*/-/g; s/^-//; s/-$//' | tr 'A-Z' 'a-z' | sed 's/^[^a-z]*//' | cut -c1-48 | sed 's/-$//; s/^$/preview/')" >> "$GITHUB_OUTPUT"
- name: Upload preview version
id: preview
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: versions upload --preview-alias ${{ steps.slug.outputs.alias }}
- name: Comment preview URL
uses: actions/github-script@v7
env:
WRANGLER_OUTPUT: ${{ steps.preview.outputs.command-output }}
BRANCH_ALIAS: ${{ steps.slug.outputs.alias }}
BRANCH_NAME: ${{ github.head_ref }}
with:
script: |
const output = process.env.WRANGLER_OUTPUT || '';
const urlMatch = output.match(/https:\/\/[^\s"'`]+\.workers\.dev[^\s"'`]*/);
const alias = process.env.BRANCH_ALIAS;
const branch = process.env.BRANCH_NAME;
const url = urlMatch ? urlMatch[0] : `Preview alias: ${alias} (check workflow logs for URL)`;
const marker = '<!-- cf-preview -->';
const body = [
'### Preview Deployment',
'',
urlMatch ? `[${url}](${url})` : url,
'',
`Branch: \`${branch}\` | Alias: \`${alias}\``,
'',
marker,
].join('\n');

const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body?.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ dist/
*.local
.env
hack/
.serena/
.playwright-mcp/
146 changes: 146 additions & 0 deletions DEPLOY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Deployment Guide

## GitHub Actions Secrets and Variables

### Secrets (GitHub repo → Settings → Secrets and variables → Actions → Secrets)

**`CLOUDFLARE_API_TOKEN`**
1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com) → My Profile → API Tokens
2. Click "Create Token"
3. Use the "Edit Cloudflare Workers" template
4. Scope to your account and zone as needed
5. Copy the token and add it as a secret

**`CLOUDFLARE_ACCOUNT_ID`**
1. Go to [Cloudflare Dashboard](https://dash.cloudflare.com)
2. Select your account (Account Home)
3. Copy the Account ID from the right sidebar
4. Add it as a secret

### Variables (GitHub repo → Settings → Secrets and variables → Actions → Variables)

**`VITE_GITHUB_CLIENT_ID`**
- This is the GitHub App Client ID (not a secret — it is embedded in the built JS bundle)
- Add it as an Actions **variable** (not a secret)
- See GitHub App setup below for how to obtain it

## GitHub App Setup

1. Go to GitHub → Settings → Developer settings → GitHub Apps → **New GitHub App**
2. Fill in the basic details:
- **App name**: your app name (e.g. `gh-tracker-yourname`)
- **Description**: `Personal dashboard for tracking GitHub issues, PRs, and Actions runs across repos and orgs.`
- **Homepage URL**: `https://gh.gordoncode.dev`
3. Under **Identifying and authorizing users**:
- **Callback URLs** — register all three:
- `https://gh.gordoncode.dev/oauth/callback` (production)
- `https://github-tracker.<account>.workers.dev/oauth/callback` (preview — GitHub's subdomain matching should allow per-branch preview aliases like `alias.github-tracker.<account>.workers.dev` to work; verify after first preview deploy)
- `http://localhost:5173/oauth/callback` (local dev)
- ✅ **Expire user authorization tokens** — check this. The app uses short-lived access tokens (8hr) with HttpOnly cookie-based refresh token rotation.
- ✅ **Request user authorization (OAuth) during installation** — check this. Streamlines the install + authorize flow into one step.
4. Under **Post installation**:
- Leave **Setup URL** blank
- Leave **Redirect on update** unchecked
5. Under **Webhook**:
- ❌ Uncheck **Active** — the app polls; it does not use webhooks.
6. Under **Permissions**:

**Repository permissions** (read-only):

| Permission | Access | Used for |
|------------|--------|----------|
| **Actions** | Read-only | `GET /repos/{owner}/{repo}/actions/runs` — workflow run list |
| **Checks** | Read-only | `GET /repos/{owner}/{repo}/commits/{ref}/check-runs` — PR check status (REST fallback) |
| **Commit statuses** | Read-only | `GET /repos/{owner}/{repo}/commits/{ref}/status` — legacy commit status (REST fallback) |
| **Issues** | Read-only | `GET /search/issues?q=is:issue` — issue search |
| **Metadata** | Read-only | Automatically granted when any repo permission is set. Required for basic repo info. |
| **Pull requests** | Read-only | `GET /search/issues?q=is:pr`, `GET /repos/{owner}/{repo}/pulls/{pull_number}`, `/reviews` — PR search, detail, and reviews |

**Organization permissions:**

| Permission | Access | Used for |
|------------|--------|----------|
| **Members** | Read-only | `GET /user/orgs` — list user's organizations for the org selector |

**Account permissions:**

| Permission | Access | Used for |
|------------|--------|----------|
| _(none required)_ | | |

7. Under **Where can this GitHub App be installed?**:
- **Any account** — the app uses OAuth authorization (not installation tokens), so any GitHub user needs to be able to authorize via the login flow
8. Click **Create GitHub App**
9. Note the **Client ID** — this is your `VITE_GITHUB_CLIENT_ID`
10. Click **Generate a new client secret** and save it for the Worker secrets below

### Notifications API limitation

The GitHub Notifications API (`GET /notifications`) does not support GitHub App user access tokens — only classic personal access tokens. The app uses notifications as a polling optimization gate (skip full fetch when nothing changed). When the notifications endpoint returns 403, the gate **auto-disables** and the app falls back to time-based polling. No functionality is lost; polling is just slightly less efficient.

## Cloudflare Worker Secrets

These are set via wrangler CLI and are stored in the Cloudflare Worker runtime (not in GitHub).

### Production environment

```sh
wrangler secret put GITHUB_CLIENT_ID
wrangler secret put GITHUB_CLIENT_SECRET
wrangler secret put ALLOWED_ORIGIN
```

- `GITHUB_CLIENT_ID`: same value as `VITE_GITHUB_CLIENT_ID`
- `GITHUB_CLIENT_SECRET`: the Client Secret from your GitHub App
- `ALLOWED_ORIGIN`: `https://gh.gordoncode.dev`

### Preview versions

Preview deployments use `wrangler versions upload` (not a separate environment), so they inherit production secrets automatically. No additional secret configuration is needed.

CORS note: Preview URLs are same-origin (SPA and API share the same `*.workers.dev` host), so the `ALLOWED_ORIGIN` strict-equality check is irrelevant — browsers don't enforce CORS on same-origin requests.

**Migration note:** If you previously deployed with `wrangler deploy --env preview`, an orphaned `github-tracker-preview` worker may still exist. Delete it via `wrangler delete --name github-tracker-preview` or through the Cloudflare dashboard.

## Worker API Endpoints

| Endpoint | Method | Auth | Purpose |
|----------|--------|------|---------|
| `/api/oauth/token` | POST | none | Exchange OAuth code for access token. Refresh token set as HttpOnly cookie. |
| `/api/oauth/refresh` | POST | cookie | Refresh expired access token. Reads `github_tracker_rt` HttpOnly cookie. Sets rotated cookie. |
| `/api/oauth/logout` | POST | none | Clears the `github_tracker_rt` HttpOnly cookie (`Max-Age=0`). |
| `/api/health` | GET | none | Health check. Returns `OK`. |

### Refresh Token Security

The refresh token (6-month lifetime) is stored as an **HttpOnly cookie** — never in `localStorage` or the response body. This protects the high-value long-lived credential from XSS:

- Production cookie: `__Host-github_tracker_rt` with `HttpOnly; Secure; SameSite=Strict; Path=/`
- Local dev: `github_tracker_rt` with `HttpOnly; SameSite=Lax; Path=/` (no `Secure` — localhost is HTTP; no `__Host-` prefix — requires `Secure`)
- The short-lived access token (8hr) is held in-memory only (never persisted to `localStorage`); on page reload, `refreshAccessToken()` obtains a fresh token via the cookie
- On logout, the client calls `POST /api/oauth/logout` to clear the cookie
- GitHub rotates the refresh token on each use; the Worker sets the new value as a cookie

### CORS

- `Access-Control-Allow-Origin`: exact match against `ALLOWED_ORIGIN` (no wildcards)
- `Access-Control-Allow-Credentials: true`: enables cookie-based refresh for cross-origin preview deploys
- Same-origin requests (production, local dev) send cookies automatically without CORS

## Local Development

Copy `.dev.vars.example` to `.dev.vars` and fill in your values. Wrangler picks up `.dev.vars` automatically for local `wrangler dev` runs.

## Deploy Manually

```sh
pnpm run build
wrangler deploy
```

For preview (uploads a version without promoting to production):

```sh
pnpm run build
wrangler versions upload --preview-alias my-feature
```
84 changes: 82 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,83 @@
# github-tracker
# GitHub Tracker

GitHub dashboard for tracking issues, PRs, and Actions runs across repos and orgs.
Dashboard SPA tracking GitHub issues, PRs, and GHA workflow runs across multiple repos/orgs. Built with SolidJS on Cloudflare Workers.

## Features

- **Issues Tab** — Open issues where you're the creator, assignee, or mentioned. Sortable, filterable, paginated.
- **Pull Requests Tab** — Open PRs with CI check status indicators (green/yellow/red dots). Draft badges, reviewer names.
- **Actions Tab** — GHA workflow runs grouped by repo and workflow. Accordion collapse, PR run toggle.
- **Onboarding Wizard** — Two-step org/repo selection with search filtering and bulk select.
- **Settings Page** — Refresh interval, notification preferences, theme (light/dark/system), density, GitHub Actions limits.
- **Desktop Notifications** — New item alerts with per-type toggles and batching.
- **Ignore System** — Hide specific items with an "N ignored" badge and unignore popover.
- **Dark Mode** — System-aware with flash prevention via inline script + CSP SHA-256 hash.
- **ETag Caching** — Conditional requests (304s are free against GitHub's rate limit).
- **Auto-refresh** — Visibility-aware polling that pauses when tab is hidden.

## Tech Stack

- **Frontend:** SolidJS + Tailwind CSS v4 + TypeScript (strict)
- **Build:** Vite 8 + @cloudflare/vite-plugin
- **Hosting:** Cloudflare Workers (static assets + OAuth endpoint)
- **API:** @octokit/core with throttling, retry, pagination plugins
- **State:** localStorage (config/view) + IndexedDB (API cache with ETags)
- **Testing:** Vitest 4 (happy-dom for browser, @cloudflare/vitest-pool-workers for Worker)
- **Package Manager:** pnpm

## Development

```sh
pnpm install
pnpm run dev # Start Vite dev server
pnpm test # Run browser tests (130 tests)
pnpm run typecheck # TypeScript check
pnpm run build # Production build (~241KB JS, ~31KB CSS)
```

## Project Structure

```
src/
app/
components/
dashboard/ # DashboardPage, IssuesTab, PullRequestsTab, ActionsTab, ItemRow, WorkflowRunRow, IgnoreBadge
layout/ # Header, TabBar, FilterBar
onboarding/ # OnboardingWizard, OrgSelector, RepoSelector
settings/ # SettingsPage (7 config sections + data management)
shared/ # FilterInput, LoadingSpinner, StatusDot
pages/ # LoginPage, OAuthCallback
services/
api.ts # GitHub API methods (fetchOrgs, fetchRepos, fetchIssues, fetchPRs, fetchWorkflowRuns)
github.ts # Octokit client factory with ETag caching and rate limit tracking
poll.ts # Poll coordinator with visibility-aware auto-refresh
stores/
auth.ts # OAuth token management with auto-refresh
cache.ts # IndexedDB cache with TTL eviction and ETag support
config.ts # Zod v4-validated config with localStorage persistence
view.ts # View state (tabs, sorting, ignored items, filters)
lib/
notifications.ts # Desktop notification permission, detection, and dispatch
worker/
index.ts # OAuth token exchange/refresh endpoint, CORS, security headers
tests/
fixtures/ # GitHub API response fixtures (orgs, repos, issues, PRs, runs)
services/ # API service, Octokit client, and poll coordinator tests
stores/ # Config and cache store tests
components/ # ItemRow and IssuesTab component tests
lib/ # Notification tests
worker/ # Worker OAuth endpoint tests
```

## Security

- Strict CSP: `script-src 'self'` (SHA-256 exception for dark mode script only)
- OAuth CSRF protection via `crypto.getRandomValues` state parameter
- CORS locked to exact origin (strict equality, no substring matching)
- Access token in-memory only (never persisted); refresh token in `__Host-` HttpOnly cookie
- Auto-refresh on 401 and on page load via HttpOnly cookie
- All GitHub API strings auto-escaped by SolidJS JSX (no innerHTML)

## Deployment

See [DEPLOY.md](./DEPLOY.md) for Cloudflare, GitHub App, and CI/CD setup.
Loading