UI Pages
The read interface lives at esg-screen.org, server-rendered Express +
EJS/Handlebars, gated by Cloudflare Access. Per
ADR-0002.
This page lists each route, its purpose, what it reads from the DB, and its current build state.
For visual design, see Design system. For the reference mock, see Mock reference.
Route summary
| Route | Page | Status |
|---|---|---|
/ |
Institution index | pending |
/institution/:lei |
Institution detail | pending |
/methodology |
Methodology (source register + rule catalogue) | pending |
/runs |
Scrape run log | pending |
/watchlist |
Watchlist findings | pending |
/health |
Stack health endpoint | pending |
All five are part of the v1 read interface scope. Build order per the 19 May handoff is roughly: detail page first (the mock-v4 shape), then index, then methodology, then runs and watchlist.
/ — Institution index
Purpose: list view of all pilot institutions. The first page on the site, the landing for any session.
Display:
- One row per institution
- Columns: name, sector (GICS), composite ESG (RAG-coloured), coverage (RAG-coloured), red flags, last-run timestamp
- Sortable by any column
- Filterable by sector, red-flag state, coverage band
DB reads:
institutionJOIN latest-runscore_stage1_esgJOIN institution's red-flag columns- Latest run resolved via
(SELECT MAX(run_id) FROM scrape_run WHERE status='success')
Filter state: stored in URL query parameters (per ADR-0002 — no browser storage). Bookmarkable, survives reload.
Pagination: not needed at pilot scale (8 institutions). When the list grows, paginate at 50 per page.
/institution/:lei — Institution detail
Purpose: the deep-dive page. Per institution, everything the system knows. The screening workflow's primary surface.
Designed in institution-detail-mock-v4.html (the reference mock —
see Mock reference).
Structure (per ADR-0002):
- Header: breadcrumb, institution name, LEI/GICS/domicile metadata, run stamp.
- Red flags band: fossil-financing, weapons-manufacturing exposure. Red when exposed, green when clear.
- Hero panel: ESG composite + coverage as two equally-weighted numbers. Both RAG-coloured. Three secondary metrics underneath (confidence, peer rank, delta from previous run). Coverage explainer paragraph below.
- Pillar breakdown: E / S / G boxes showing score, weight, coverage, covered-rule count.
- Rule evaluation: filter pills (All / E / S / G / Covered only / Uncovered only) above a grouped table. Rules grouped by theme per ADR-0005.
- Sidebar: watchlist findings for this institution, major shareholders, peer group with comparison, score history sparkline, export actions.
- Financial Institutions Supplement: visually-distinct second zone (warmer background, divider band). Triangulated composite for financials only. Currently shows ESG + "awaiting" placeholders for credit and returns.
DB reads (many joins):
institutionfor headerscore_stage1_esg+score_pillar+score_sub_criterionfor the scoring zones, all keyed to latest successfulrun_idsignaljoined torulefor the rule evaluation tablesignal_sourcejoined to provide per-rule source infopeer_distributionfor peer comparison- For financials:
score_stage2_composite
Pillar weights: come from blend_weight table (E=0.40, S=0.30,
G=0.30 universally per current seed). Not hardcoded in templates.
Themes: come from src/config/rule-themes.js (UI config, not DB).
/methodology — Methodology
Purpose: source transparency surface. Per ADR-0003.
Three sections, all generated from DB joins:
- How the score works — short prose explanation of coverage-weighted scoring, pillar weights, peer comparison, RAG bands. Links to ADRs for detail.
- Source register — table of every source, joined to latest
scrape_runfor status. Columns: name, type, access mechanism, rules fed, status (live/pending/planned/deprecated/broken/degraded), last successful run, last error. All source links open the upstream in a new tab. - Rule catalogue — every rule, grouped by pillar and sub-criterion. Columns: rule ID, what it measures, source(s), scoring logic type (boolean / direct / deduction), status.
DB reads:
signal_sourceLEFT JOIN latestscrape_runrows (for status)ruleJOINsignal_sourcesignalcount grouped by (rule_id) for "is this rule receiving data"
This page is the live equivalent of the Register page on the ops site. Ops site is intent + design; product page is current state.
/runs — Scrape run log
Purpose: internal debugging surface. What happened on each scrape run.
Display:
- Reverse-chronological list of
scrape_runrows - Per run: started_at, finished_at, duration, status, error_count, signals_written, per-scraper breakdown
- Each run links to a detail view showing the specific signals written
DB reads: scrape_run, optionally joined to signal aggregated
by source.
Internal only. Not shared with clients.
/watchlist — Watchlist findings
Purpose: cross-institution surface for findings the score doesn't capture directly.
Example findings:
- NatWest's withdrawn SBTi commitment (high-confidence boolean=0 with context that matters)
- BHRRC allegations that don't map cleanly to a single rule
- Ethical Consumer third-party views (per ADR-0003 — used as watchlist only, never blended)
Display:
- One row per finding, grouped by institution
- Columns: institution, finding type, source, summary, observed_at
DB reads: a watchlist_finding table — schema TBD. The decision
on shape (separate table vs derived view vs column on signal) is one
of the open items raised in this chat session. Likely a separate table,
populated by the BHRRC scraper and the existing-signal-promotion logic.
Build state: pending. Surface lands when BHRRC scraper does, since that's the first source producing watchlist findings.
/health — Stack health
Purpose: programmatic health check endpoint. JSON response, not HTML.
Response shape:
{
"status": "ok",
"db": "ok",
"last_successful_scrape": "2026-05-19T02:00:14Z",
"last_successful_score": "2026-05-19T02:00:18Z",
"process_uptime_seconds": 123456
}
Use cases:
- PM2 monitoring
- Possible future external uptime monitoring
- Sanity check during deployments
Not gated by Cloudflare Access — used by infrastructure tooling. (Or: gated, but with a separate Access bypass for the specific path. TBD at implementation.)
Filtering and URL state
Per ADR-0002, no browser storage. All filter state lives in URL query parameters.
Conventions:
?pillar=E,S,G— comma-separated pillar filter?coverage=coveredor?coverage=uncoveredor?coverage=all?sort=composite_desc?sector=40— GICS sector filter
URLs are bookmarkable. Sharing a URL shares the view. (Internal sharing only — Cloudflare Access still gates access.)
Templating choice
EJS or Handlebars per ADR-0002 — either works. CC-on-VM picks during build. EJS is more familiar to most Node developers; Handlebars is slightly more constrained (which is good for keeping logic out of templates).
Default choice: EJS unless there's a reason to prefer Handlebars. Both render server-side, no build step.
CSS approach
Single base stylesheet at src/ui/public/base.css. CSS custom
properties for the palette (per Design system).
No CSS-in-JS, no Tailwind, no PostCSS pipeline. Pure CSS with custom
properties.
Inline minor styles in templates are acceptable for one-off page quirks. Anything reused belongs in the base sheet.
JS approach
Minimal vanilla JS for filtering and minor interactivity. No framework.
If interactive partial updates become needed (e.g. live-filtering the rules table without page reload), introduce HTMX. Not in v1.