Skip to content

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:

  • institution JOIN latest-run score_stage1_esg JOIN 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):

  1. Header: breadcrumb, institution name, LEI/GICS/domicile metadata, run stamp.
  2. Red flags band: fossil-financing, weapons-manufacturing exposure. Red when exposed, green when clear.
  3. 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.
  4. Pillar breakdown: E / S / G boxes showing score, weight, coverage, covered-rule count.
  5. Rule evaluation: filter pills (All / E / S / G / Covered only / Uncovered only) above a grouped table. Rules grouped by theme per ADR-0005.
  6. Sidebar: watchlist findings for this institution, major shareholders, peer group with comparison, score history sparkline, export actions.
  7. 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):

  • institution for header
  • score_stage1_esg + score_pillar + score_sub_criterion for the scoring zones, all keyed to latest successful run_id
  • signal joined to rule for the rule evaluation table
  • signal_source joined to provide per-rule source info
  • peer_distribution for 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:

  1. How the score works — short prose explanation of coverage-weighted scoring, pillar weights, peer comparison, RAG bands. Links to ADRs for detail.
  2. Source register — table of every source, joined to latest scrape_run for 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.
  3. 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_source LEFT JOIN latest scrape_run rows (for status)
  • rule JOIN signal_source
  • signal count 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_run rows
  • 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=covered or ?coverage=uncovered or ?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.