Production deploy: rootDir fix, systemd unit, README, favicon
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgBEW6qAwgn2fcQbA2f4ZQ
This commit is contained in:
parent
c9d810c8e5
commit
c0e46391e5
58
README.md
Normal file
58
README.md
Normal file
@ -0,0 +1,58 @@
|
||||
# pr-monitor
|
||||
|
||||
Single-user dashboard for tracking PRs across multiple GitHub repos: CI checks
|
||||
segmented into lint / specs / e2e / other (every check visible, with per-chip
|
||||
dropdowns), review state derived from review-thread resolution, and a live
|
||||
unified timeline of all comments and reviews with persistent read/unread state.
|
||||
|
||||
## How it works
|
||||
|
||||
- Node/TypeScript server polls the GitHub GraphQL API every 60s using a two-tier
|
||||
query: a cheap `search()` list query whose fields double as change signals
|
||||
(PR `updatedAt` does **not** change when checks or thread resolution change),
|
||||
then batched detail queries only for changed PRs.
|
||||
- State is cached in SQLite (`data/pr-monitor.sqlite`, `node:sqlite`) so the UI
|
||||
loads instantly on restart; timeline events and read/unread survive restarts.
|
||||
- The browser gets a `sync` event over SSE each poll and refetches via
|
||||
TanStack Query. React 19 + Vite SPA, light/dark/system theme, responsive
|
||||
(two-column desktop, tabbed mobile).
|
||||
|
||||
## Config
|
||||
|
||||
`config.json` — repos, the `involves:<user>` filter flag, poll interval, port,
|
||||
and per-repo check-name → category mappings (exact names or `*` globs, e.g.
|
||||
`"Preview E2E *"`). Unmatched checks land in `other`; nothing is ever hidden.
|
||||
|
||||
Auth: `GITHUB_TOKEN` env var, falling back to `gh auth token`.
|
||||
|
||||
## Commands
|
||||
|
||||
```sh
|
||||
pnpm dev # server on :4000 (tsx watch)
|
||||
pnpm dev:web # vite dev server on :5173, proxies /api
|
||||
pnpm sync:once # one poll, prints a PR/category table (smoke test)
|
||||
pnpm build # vite build + tsc server build
|
||||
pnpm start # run the production build
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
systemd user unit (survives reboots via linger):
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
cp deploy/pr-monitor.service ~/.config/systemd/user/
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now pr-monitor
|
||||
loginctl enable-linger $USER
|
||||
```
|
||||
|
||||
Serves the built SPA + API on http://localhost:4000.
|
||||
|
||||
## Notes
|
||||
|
||||
- Own activity appears in the timeline but is never marked unread; the first-ever
|
||||
sync marks the historical backfill as read (`BACKFILL_UNREAD=1` to override).
|
||||
- Events are append-only: comments later deleted on GitHub remain in the timeline.
|
||||
- Polling stretches to 5 min automatically when the GraphQL rate budget dips
|
||||
below 500 points.
|
||||
13
deploy/pr-monitor.service
Normal file
13
deploy/pr-monitor.service
Normal file
@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=PR monitor dashboard
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory=/home/apps/vibes/pr-monitor
|
||||
ExecStart=/home/apps/.local/bin/mise exec -- node dist-server/server/index.js
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=NODE_ENV=production
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@ -1,4 +1,4 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { z } from 'zod';
|
||||
@ -54,7 +54,18 @@ function compileNameGlob(pattern: string): (name: string) => boolean {
|
||||
return (name) => regex.test(name);
|
||||
}
|
||||
|
||||
export const rootDir = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
/** Project root: walk up from this module (which may live in server/ or
|
||||
* dist-server/server/) until config.json appears. */
|
||||
function findRoot(): string {
|
||||
let dir = dirname(fileURLToPath(import.meta.url));
|
||||
for (let i = 0; i < 4; i++) {
|
||||
dir = dirname(dir);
|
||||
if (existsSync(join(dir, 'config.json'))) return dir;
|
||||
}
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
export const rootDir = findRoot();
|
||||
|
||||
export function loadConfig(path = join(rootDir, 'config.json')): AppConfig {
|
||||
const raw = configSchema.parse(JSON.parse(readFileSync(path, 'utf8')));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user