Phase 1 hardening + UI hygiene#4
Open
borisstoyanov wants to merge 14 commits into
Open
Conversation
- server: /api/prs/:prNumber/test-failures replaced the per-failure COUNT query loop with a single grouped query; added isNaN(prNumber) guard returning 400. - client: replaced the dead CRA boilerplate test (asserted "learn react") with real smoke tests for the header, the four nav tabs, and the mount API call. Resolved values set in beforeEach because CRA's Jest config sets resetMocks:true. npm test now passes, so deploys no longer need --skip-tests. - repo: removed check_staleness.js (one-off debug) and scrape-github-prs.js.backup; moved point-in-time work-logs into docs/history/; gitignore now excludes *.backup / *_temp.js / *.temp.js. - add CLAUDE.md for future Claude Code sessions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The smoketest and code-coverage parsing logic was copy-pasted across
three handlers (getHealthPRs, getPR, getAllOpenPRs) and had drifted
into three subtly different implementations. Consolidate into pure,
unit-tested modules and call them from all three sites.
- server/src/parsers/trillian.ts, codecov.ts: single source of truth,
no DB/network access. Canonical behaviour follows the most complete
variant (the /api/pr/:number handler): skipped ("did not run") tests
count toward the total, and a run is FAIL only when there is >=1
error.
- Fixes a latent bug in the all-open-PRs path, which used
`passed === total` and so marked otherwise-passing runs as FAIL
whenever any tests were skipped.
- Unify code-coverage parsing (all-open-PRs previously used a different
change regex and a GitHub URL instead of the codecov.io link).
- 26 unit tests via node:test + ts-node; test files excluded from the
tsc build. `npm test` now runs server then client.
- Net: ~360 lines of duplicated parsing removed from index.ts.
Behaviour changes to validate against production data:
- all-open-PRs smoketest status now reflects errors, not skipped tests.
- all-open-PRs coverage now links to codecov.io and parses +/- change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add helmet, per-IP rate limiting, and origin-scoped CORS - Harden artifact proxy: numeric ID validation + strict rate limit - Replace console.* with structured pino logging; log schema drift loudly - Make /api/health ping the DB (503 when down) - Document scraper-as-system-of-record and public-no-auth decisions (ADRs) - Add CONTEXT.md glossary Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Phase 1 hardening of the QA Portal backend: introduces baseline HTTP security protections and better operational behavior (logging, health checks), while also reducing drift by centralizing bot-comment parsing into dedicated pure parser modules.
Changes:
- Added security middleware and controls on the Express API (helmet, scoped CORS, rate limiting, proxy-aware client IPs, artifact proxy input validation).
- Replaced ad-hoc
console.*logging with structured pino logging and improved schema-drift warning logs. - Refactored Trillian smoketest + Codecov parsing into tested, reusable parser modules; adjusted build/test configuration to run server tests.
Reviewed changes
Copilot reviewed 15 out of 25 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| server/tsconfig.json | Excludes server *.test.ts from tsc compilation output. |
| server/src/parsers/trillian.ts | New single-source Trillian/BlueOrangutan smoketest parser utilities. |
| server/src/parsers/trillian.test.ts | Unit tests for the Trillian smoketest parser behavior. |
| server/src/parsers/codecov.ts | New single-source Codecov comment parser. |
| server/src/parsers/codecov.test.ts | Unit tests for Codecov parsing cases. |
| server/src/index.ts | Adds helmet/CORS/rate-limits/pino logging/health DB ping; uses new parser modules; optimizes N+1 query. |
| scripts/scrape-github-prs.js.backup | Removes an old backup copy from the repo. |
| package.json | Adds server+client test scripts; adds new backend dependencies (helmet/rate-limit/pino). |
| package-lock.json | Locks new dependencies and transitive updates (notably Node engine constraints). |
| docs/history/QA_TESTING_REPORT_PACKAGE_STALENESS.md | Adds historical QA report documentation. |
| docs/history/MILESTONE_ROLLBACK_PLAN.md | Adds historical rollback plan documentation. |
| docs/history/MILESTONE_IMPLEMENTATION_SUMMARY.md | Adds historical milestone implementation summary. |
| docs/history/MILESTONE_DEPLOYMENT_GUIDE.md | Adds historical deployment guide documentation. |
| docs/history/FIX_STALENESS_ISSUE.md | Adds historical write-up about a staleness issue. |
| docs/history/FEATURE_AVAILABLE_PACKAGES_COMPLETE.md | Adds historical available-packages feature documentation. |
| docs/history/CONTEXT_PROMPT.md | Adds historical context restoration prompt documentation. |
| docs/history/.github-copilot-setup.md | Adds historical Copilot setup notes. |
| docs/adr/0002-public-dashboard-no-auth.md | Adds ADR documenting intentional lack of auth + mitigation strategy. |
| docs/adr/0001-scraper-is-system-of-record.md | Adds ADR defining scraper as system-of-record and API drift behavior. |
| CONTEXT.md | Adds shared glossary/definitions for domain terms used in the portal. |
| client/src/App.test.tsx | Updates App tests to stub API calls and assert key UI navigation elements. |
| CLAUDE.md | Adds repository guidance for Claude Code (commands/architecture/constraints). |
| check_staleness.js | Removes a scratch staleness-check script. |
| .gitignore | Ignores scratch/temp/backup files. |
| .env.example | Documents new LOG_LEVEL and CORS_ORIGIN environment variables. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+21
to
+22
| // Behind nginx in production — trust the proxy so rate-limit sees the real client IP. | ||
| app.set('trust proxy', 1); |
Comment on lines
+28
to
+33
| // CORS scoped to the portal's own origin. CORS_ORIGIN may be a comma-separated list; | ||
| // if unset we fall back to reflecting any origin (dev convenience only). | ||
| const corsOrigins = process.env.CORS_ORIGIN | ||
| ? process.env.CORS_ORIGIN.split(',').map(o => o.trim()) | ||
| : undefined; | ||
| app.use(cors(corsOrigins ? { origin: corsOrigins } : {})); |
Comment on lines
+15
to
+16
| // Structured logger. Set LOG_LEVEL=debug locally for verbose output. | ||
| const logger = pino({ level: process.env.LOG_LEVEL || 'info' }); |
Comment on lines
+38
to
+42
| "express-rate-limit": "^8.5.2", | ||
| "helmet": "^8.2.0", | ||
| "mysql2": "^3.15.3", | ||
| "pino": "^10.3.1", | ||
| "pino-http": "^11.0.0" |
Comment on lines
229
to
231
| ).catch(err => { | ||
| console.warn('Could not fetch approvals:', err.message); | ||
| logger.warn({ err: err.message, source: 'pr_approvals' }, 'Schema drift: could not fetch approvals'); | ||
| return []; |
Comment on lines
634
to
636
| ).catch(err => { | ||
| console.warn('Could not fetch approvals:', err.message); | ||
| logger.warn({ err: err.message, source: 'pr_approvals' }, 'Schema drift: could not fetch approvals'); | ||
| return []; |
Comment on lines
641
to
643
| ).catch(err => { | ||
| console.warn('Could not fetch labels:', err.message); | ||
| logger.warn({ err: err.message, source: 'pr_health_labels' }, 'Schema drift: could not fetch labels'); | ||
| return []; |
Comment on lines
651
to
653
| ).catch(err => { | ||
| console.warn('Could not fetch package builds:', err.message); | ||
| logger.warn({ err: err.message, source: 'pr_package_builds' }, 'Schema drift: could not fetch package builds'); | ||
| return []; |
Comment on lines
917
to
919
| } catch (error: any) { | ||
| console.error('Error downloading artifact:', error.message); | ||
| logger.error({ err: error.message }, 'Error downloading artifact'); | ||
| if (error.response?.status === 410) { |
Comment on lines
+88
to
+92
| * Counts are read from the summary line, e.g. "141 look OK, 0 have errors, | ||
| * 3 did not run". Skipped ("did not run") tests are included in the total so | ||
| * that the pass ratio reflects the full suite. A run is FAIL only when there | ||
| * is at least one error — skipped tests alone do not fail a run. | ||
| */ |
- Single APP_VERSION constant (was v0.1.1 in header vs v1.0.4 in footer) - Delete unused ReadyToMerge component family + orphaned CSS (8 files); drop dead getReadyToMergePRs API method and its test mock - AllPRsView refresh icon ↻ to match the health tab Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove unused imports/vars: Routes/Route (App), isFresh (AllPRsView), stats state + UpgradeTestStats import + dead formatDate/getRelativeTime (UpgradeTests); drop the now-unused upgrade-stats fetch - Mark the mount-only useEffect with an exhaustive-deps disable Enables gating deploys on lint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CRA treats warnings as errors under CI=true, so a lint regression now fails the deploy step (set -e) instead of shipping. Client build is already CI-clean as of the previous commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Global :focus-visible outline so keyboard focus is visible everywhere - Shared clickable() helper (role=button, tabIndex, Enter/Space) applied to the previously mouse-only divs: AllPRsView stat-box filters, PRCard smoketest expanders, TestResults file accordions, UpgradeTests accordion headers + test rows + heatmap cells - aria-expanded on expandable controls; aria-pressed on filter toggles - aria-label on emoji-only status badges and the package-status icon so meaning isn't conveyed by glyph/color alone Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add :root token palette (border/text/bg/primary/accent/success/danger + shadow/radius scales) in index.css. Token values equal the hexes they replace, so the light theme is visually unchanged. - Migrate the 15 most-repeated colors (~200 occurrences) to var() across App/AllPRsView/PRCard/SearchBar/UpgradeTests CSS via exact-match replace. - Focus outline now uses var(--color-primary). Foundation for a prefers-color-scheme dark mode (override tokens in one place) and centralized contrast fixes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lanking loads - Tab bar: drop inconsistent emoji, add count badges (Health, All Open, Flaky) - Shared RefreshControls: "Updated Xm ago" + auto-refresh toggle + refresh, used on Health and All-PRs tabs (timeAgo/useNow util) - All-PRs: free-text filter by #, title, or assignee - Loading no longer blanks the screen on refresh — stale content stays with a slim top progress bar; full spinner only on first load - App fetches lightweight counts (all-open + flaky) for the badges Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The old `auto 1fr` grid centered the title inside the right column, pushing it off-center while the logo floated mid-left. Use a symmetric `1fr auto 1fr` grid so the logo is left-aligned and the title is centered across the header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add --surface token; migrate white card/table backgrounds, hardcoded dark grey text, and near-white fills to tokens so they theme correctly - Add @media (prefers-color-scheme: dark) overriding the token values; the structural chrome flips automatically since components reference tokens - A few vivid status/heatmap accent colors remain untokenized (flagged for QA) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Records the token-based theming approach, the dark-mode override mechanism (prefers-color-scheme + data-theme), the data-viz (heatmap) exemption, and the WCAG AA contrast target — decisions reached in a design-grilling session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Retune design tokens to one blue system (primary #1f6feb) so links, tabs, focus rings, and stat numbers stop clashing (was purple header + orange numbers + a third blue for links) - Redesign header: brand-blue gradient via --header-bg, slimmer, softer shadow - Recolor TestResults stat numbers orange -> primary blue; map flaky success/failure/error counts to semantic tokens - Refine neutrals (AA-tuned greys, cooler borders/bg), softer shadow tokens, add --color-warn / --header-bg / --color-primary-strong (light + dark) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the 1200px max-width on .header-content so the logo sits at the left gutter and the symmetric grid centers the title to the whole screen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1 of the architecture-review roadmap plus a UI-hygiene pass. Defensive + structural; no change to the data shown.
Security (server)
/api/, 10/min on the artifact proxy), CORS scoped toCORS_ORIGINartifactIdbefore spending the token;trust proxyfor real client IPsObservability (server)
console.*→ structured pino; schema-drift.catchblocks logWARNnaming the source (ADR-0001)/api/healthpings the DB (SELECT 1), returns 503 when down;[DEBUG]noise removedUI hygiene (client)
v0.1.1header vsv1.0.4footer) → singleAPP_VERSIONconstantReadyToMergecomponent family + orphaned CSS (8 files, ~350 lines); drop deadgetReadyToMergePRs↻)npm run buildpasses underCI=true(unused imports/vars; mount-onlyuseEffect)Docs / decisions
CONTEXT.mdglossary;docs/adr/0001-scraper-is-system-of-record.md;docs/adr/0002-public-dashboard-no-auth.md.env.exampledocumentsLOG_LEVEL/CORS_ORIGINVerification
tscclean, 19 tests passtscclean, 3 tests pass, production build passes underCI=true🤖 Generated with Claude Code