Dark mode (theme tokens, toggle, boot script), theme-aware chart palette, home hero/ticker, sitemap secondary nav, deploy manifest without ngrok
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
11 changed files +224 −36
modified
CLAUDE.md
+6 −4
@@ -40,8 +40,8 @@ packages/shared ids, provenance types, normalization, logger | ||
| 40 | 40 | packages/database Drizzle schema (snake_case casing), migrations, seed (metrics + geographies only) |
| 41 | 41 | packages/ontology qualifier rules, CancerResolver (alias/code reconciliation), TOP_LEVEL_CANCERS |
| 42 | 42 | packages/connectors SDK (manifest, HttpClient, RawLake, RunContext, validators) + connectors/<id>/{manifest.ts,index.ts,fixtures/,*.test.ts} |
| 43 | −packages/ranking counters (entity_counters), ranking engine (snapshots + lineage), trace | |
| 44 | −scripts/ci.ts operator CLI: `pnpm cix connectors|run <id>|run-all|health|sources:sync|counters|rank|stats|trace` | |
| 43 | +packages/ranking counters (entity_counters), intelligence (trial_intelligence, trial sites, drug_pipeline, research_gap_components), ranking engine (snapshots + lineage), trace | |
| 44 | +scripts/ci.ts operator CLI: `pnpm cix connectors|run <id>|run-all|health|sources:sync|counters|intel|reconcile-drugs|rank|stats|trace|doctor|alerts` | |
| 45 | 45 | docs/ architecture, data model, methodology, source policy, ADRs, connector docs |
| 46 | 46 | data/raw data lake (gitignored) — {source}/{date}/{entity}/{runId}-{part}.jsonl.gz |
| 47 | 47 | ``` |
@@ -78,5 +78,7 @@ pnpm dev:api & pnpm dev:web | ||
| 78 | 78 | ## Deployment |
| 79 | 79 | MacLustr cluster via `mld` (gateway M1M32): manifest `~/dispatch/apps/cancerindex.json`, |
| 80 | 80 | `mld stage <dir> cancerindex` then `mld deploy cancerindex`. Web :8250, API :8251, PM2 processes |
| 81 | −`cancerindex-web|api|worker|ngrok`, domain www.cancerindex.io (ngrok reserved). Postgres 17 + | |
| 82 | −pgvector on the node, database `cancerindex`. Source of truth for the code: spbgit `cancerindex.git`. | |
| 81 | +`cancerindex-web|api|worker|backup`, domain www.cancerindex.io served by the MacLustr Tunnel | |
| 82 | +(WireGuard + Caddy on BHS64; no ngrok). Postgres 17 + pgvector on the node, database `cancerindex`. | |
| 83 | +Source of truth for the code: spbgit `cancerindex.git`. Daily maintenance (UTC, worker): counters | |
| 84 | +06:00 → intelligence 06:15 → rankings 06:30; `pnpm cix intel` recomputes the derived layer by hand. | |
modified
apps/web/src/app/globals.css
+97 −2
@@ -31,10 +31,88 @@ | ||
| 31 | 31 | --color-ok: #2d5f2e; |
| 32 | 32 | --color-ok-soft: #e1ecdf; |
| 33 | 33 | |
| 34 | + --color-series-1: #0f5f63; | |
| 35 | + --color-series-2: #8a4b0a; | |
| 36 | + --color-series-3: #4a4a46; | |
| 37 | + --color-series-4: #2d5f2e; | |
| 38 | + --color-series-5: #8b1e2d; | |
| 39 | + --color-series-6: #7b7b75; | |
| 40 | + --color-series-7: #5b3a8c; | |
| 41 | + --color-series-8: #1f5fa8; | |
| 42 | + | |
| 34 | 43 | --radius-sm: 2px; |
| 35 | 44 | --radius-md: 3px; |
| 36 | 45 | } |
| 37 | 46 | |
| 47 | +/* | |
| 48 | + Dark mode (SPEC §86). Same tokens, inverted values; charts and badges reference the tokens only. | |
| 49 | + `--color-white` is re-pointed so Tailwind's `bg-white` form controls follow the theme. | |
| 50 | + Applied when the user chose "dark" (data-theme) or follows the OS preference (no explicit choice). | |
| 51 | +*/ | |
| 52 | +:root[data-theme='dark'], | |
| 53 | +:root:not([data-theme='light']):not([data-theme='dark']).ci-system-dark { | |
| 54 | + color-scheme: dark; | |
| 55 | + --color-paper: #151514; | |
| 56 | + --color-paper-2: #1d1d1b; | |
| 57 | + --color-paper-3: #282826; | |
| 58 | + --color-ink: #ecece6; | |
| 59 | + --color-ink-2: #c3c3bb; | |
| 60 | + --color-ink-3: #8f8f88; | |
| 61 | + --color-ink-4: #626259; | |
| 62 | + --color-rule: #2f2f2c; | |
| 63 | + --color-rule-strong: #47473f; | |
| 64 | + --color-accent: #6cc3c6; | |
| 65 | + --color-accent-2: #8fd3d5; | |
| 66 | + --color-accent-soft: #16353a; | |
| 67 | + --color-warn: #e0a860; | |
| 68 | + --color-warn-soft: #3a2a13; | |
| 69 | + --color-danger: #ee8a95; | |
| 70 | + --color-danger-soft: #3f1c22; | |
| 71 | + --color-ok: #93c692; | |
| 72 | + --color-ok-soft: #1d301d; | |
| 73 | + --color-white: #1d1d1b; | |
| 74 | + --color-series-1: #6cc3c6; | |
| 75 | + --color-series-2: #e0a860; | |
| 76 | + --color-series-3: #b9b9b1; | |
| 77 | + --color-series-4: #93c692; | |
| 78 | + --color-series-5: #ee8a95; | |
| 79 | + --color-series-6: #8f8f88; | |
| 80 | + --color-series-7: #b39ddb; | |
| 81 | + --color-series-8: #7fb2ea; | |
| 82 | +} | |
| 83 | +@media (prefers-color-scheme: dark) { | |
| 84 | + :root:not([data-theme='light']) { | |
| 85 | + color-scheme: dark; | |
| 86 | + --color-paper: #151514; | |
| 87 | + --color-paper-2: #1d1d1b; | |
| 88 | + --color-paper-3: #282826; | |
| 89 | + --color-ink: #ecece6; | |
| 90 | + --color-ink-2: #c3c3bb; | |
| 91 | + --color-ink-3: #8f8f88; | |
| 92 | + --color-ink-4: #626259; | |
| 93 | + --color-rule: #2f2f2c; | |
| 94 | + --color-rule-strong: #47473f; | |
| 95 | + --color-accent: #6cc3c6; | |
| 96 | + --color-accent-2: #8fd3d5; | |
| 97 | + --color-accent-soft: #16353a; | |
| 98 | + --color-warn: #e0a860; | |
| 99 | + --color-warn-soft: #3a2a13; | |
| 100 | + --color-danger: #ee8a95; | |
| 101 | + --color-danger-soft: #3f1c22; | |
| 102 | + --color-ok: #93c692; | |
| 103 | + --color-ok-soft: #1d301d; | |
| 104 | + --color-white: #1d1d1b; | |
| 105 | + --color-series-1: #6cc3c6; | |
| 106 | + --color-series-2: #e0a860; | |
| 107 | + --color-series-3: #b9b9b1; | |
| 108 | + --color-series-4: #93c692; | |
| 109 | + --color-series-5: #ee8a95; | |
| 110 | + --color-series-6: #8f8f88; | |
| 111 | + --color-series-7: #b39ddb; | |
| 112 | + --color-series-8: #7fb2ea; | |
| 113 | + } | |
| 114 | +} | |
| 115 | + | |
| 38 | 116 | :root { |
| 39 | 117 | color-scheme: light; |
| 40 | 118 | --ci-header-h: 56px; |
@@ -224,9 +302,9 @@ kbd, | ||
| 224 | 302 | top: calc(100% + 4px); |
| 225 | 303 | min-width: 260px; |
| 226 | 304 | max-width: 360px; |
| 227 | − background: #fff; | |
| 305 | + background: var(--color-paper); | |
| 228 | 306 | border: 1px solid var(--color-rule-strong); |
| 229 | − box-shadow: 0 6px 24px -10px rgba(0, 0, 0, 0.25); | |
| 307 | + box-shadow: 0 6px 24px -10px rgba(0, 0, 0, 0.35); | |
| 230 | 308 | padding: 10px 12px; |
| 231 | 309 | font-size: 12.5px; |
| 232 | 310 | line-height: 1.45; |
@@ -600,3 +678,20 @@ abbr.no-underline { | ||
| 600 | 678 | .ci-evidence td.ev details p { |
| 601 | 679 | margin-top: 4px; |
| 602 | 680 | } |
| 681 | + | |
| 682 | +/* Theme toggle (header) */ | |
| 683 | +.ci-theme-toggle { | |
| 684 | + display: inline-flex; | |
| 685 | + align-items: center; | |
| 686 | + gap: 4px; | |
| 687 | + border: 1px solid var(--color-rule); | |
| 688 | + padding: 5px 7px; | |
| 689 | + color: var(--color-ink-2); | |
| 690 | + background: transparent; | |
| 691 | + cursor: pointer; | |
| 692 | + line-height: 1; | |
| 693 | +} | |
| 694 | +.ci-theme-toggle:hover { | |
| 695 | + border-color: var(--color-accent); | |
| 696 | + color: var(--color-accent); | |
| 697 | +} | |
modified
apps/web/src/app/layout.tsx
+11 −2
@@ -22,12 +22,21 @@ export const metadata: Metadata = { | ||
| 22 | 22 | export const viewport: Viewport = { |
| 23 | 23 | width: 'device-width', |
| 24 | 24 | initialScale: 1, |
| 25 | − themeColor: '#fafaf7', | |
| 25 | + themeColor: [ | |
| 26 | + { media: '(prefers-color-scheme: light)', color: '#fafaf7' }, | |
| 27 | + { media: '(prefers-color-scheme: dark)', color: '#151514' }, | |
| 28 | + ], | |
| 26 | 29 | }; |
| 27 | 30 | |
| 31 | +/** Applies the stored theme before first paint (no flash); "system" = no attribute. */ | |
| 32 | +const THEME_BOOT = `try{var t=localStorage.getItem('ci-theme');if(t==='dark'||t==='light'){document.documentElement.setAttribute('data-theme',t)}}catch(e){}`; | |
| 33 | + | |
| 28 | 34 | export default function RootLayout({ children }: { children: React.ReactNode }) { |
| 29 | 35 | return ( |
| 30 | − <html lang="en" className={`${display.variable} ${ui.variable} ${code.variable} h-full antialiased`}> | |
| 36 | + <html lang="en" className={`${display.variable} ${ui.variable} ${code.variable} h-full antialiased`} suppressHydrationWarning> | |
| 37 | + <head> | |
| 38 | + <script dangerouslySetInnerHTML={{ __html: THEME_BOOT }} /> | |
| 39 | + </head> | |
| 31 | 40 | <body className="flex min-h-full flex-col"> |
| 32 | 41 | <a href="#main" className="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-[200] focus:border focus:border-accent focus:bg-paper focus:px-3 focus:py-2 focus:text-sm"> |
| 33 | 42 | Skip to content |
modified
apps/web/src/app/page.tsx
+8 −8
@@ -30,11 +30,11 @@ export default async function HomePage() { | ||
| 30 | 30 | const topMatched = TOP_LEVEL_CANCERS.filter((t) => topLevel.has(t.ncit)).length; |
| 31 | 31 | |
| 32 | 32 | const ticker: Array<{ label: string; value: number; note: string; href: string }> = [ |
| 33 | − { label: 'Cancers indexed', value: counts.cancers, note: 'active malignant disease entities', href: '/cancers?malignant=1' }, | |
| 34 | − { label: 'Subtypes', value: counts.subtypes, note: 'histologic and molecular subtypes', href: '/cancers?entity_type=subtype' }, | |
| 33 | + { label: 'Cancers indexed', value: counts.cancers, note: 'active malignant disease entities (NCIt)', href: '/cancers?malignant=1' }, | |
| 34 | + { label: 'Active oncology trials', value: counts.activeTrials, note: 'interventional, recruiting or active (ClinicalTrials.gov)', href: '/trials?status=active' }, | |
| 35 | + { label: 'Recruiting Phase III', value: counts.recruitingPhase3, note: 'interventional Phase III studies recruiting now', href: '/trials?status=RECRUITING&phase=PHASE3' }, | |
| 36 | + { label: 'Approved oncology drugs', value: counts.approvedDrugs, note: `drugs with ≥ 1 approval record (${fmtInt(counts.approvals)} records, all authorities)`, href: '/approvals' }, | |
| 35 | 37 | { label: 'Genes', value: counts.genes, note: 'HGNC gene records', href: '/genes' }, |
| 36 | − { label: 'Variants', value: counts.variants, note: 'distinct variant entities', href: '/genes' }, | |
| 37 | − { label: 'Clinical trials', value: counts.trials, note: 'ClinicalTrials.gov studies ingested', href: '/trials' }, | |
| 38 | 38 | { label: 'Publications', value: counts.publications, note: 'PubMed records linked', href: '/search' }, |
| 39 | 39 | { label: 'Sources', value: counts.sources, note: `${counts.activeSources} active connector${counts.activeSources === 1 ? '' : 's'}`, href: '/sources' }, |
| 40 | 40 | ]; |
@@ -43,17 +43,17 @@ export default async function HomePage() { | ||
| 43 | 43 | <div className="pb-8"> |
| 44 | 44 | {/* Hero */} |
| 45 | 45 | <section className="pt-10 sm:pt-16" aria-labelledby="hero-title"> |
| 46 | − <p className="ci-kicker mb-3">Provenance-first oncology intelligence</p> | |
| 46 | + <p className="ci-kicker mb-3">Global cancer intelligence</p> | |
| 47 | 47 | <h1 id="hero-title" className="max-w-4xl text-4xl leading-[1.05] sm:text-6xl"> |
| 48 | − CancerIndex — <span className="italic text-ink-2">The global index of cancer.</span> | |
| 48 | + Understand cancer <span className="italic text-ink-2">through data.</span> | |
| 49 | 49 | </h1> |
| 50 | 50 | <p className="mt-4 max-w-2xl text-[16px] leading-relaxed text-ink-2"> |
| 51 | − Every recognized malignant disease entity — taxonomy, epidemiology, genomics, variants, therapies, trials, literature and transparent rankings — with a source, a date and a formula version on every number. | |
| 51 | + CancerIndex connects epidemiology, survival, genomics, treatments, clinical trials, research and regulatory decisions across every recognized cancer entity — with a source, a date and a formula version on every number. | |
| 52 | 52 | </p> |
| 53 | 53 | <div className="mt-6 max-w-2xl"> |
| 54 | 54 | <CommandPalette variant="hero" /> |
| 55 | 55 | <p className="mt-2 text-[12.5px] text-ink-3"> |
| 56 | − Try <Link className="ci-link" href="/search?q=glioma">glioma</Link>, <Link className="ci-link" href="/search?q=KRAS">KRAS</Link>, <Link className="ci-link" href="/search?q=osimertinib">osimertinib</Link> or an NCT number. | |
| 56 | + Try <Link className="ci-link" href="/search?q=glioma">glioma</Link>, <Link className="ci-link" href="/search?q=KRAS">KRAS</Link>, <Link className="ci-link" href="/search?q=osimertinib">osimertinib</Link> or an NCT number · or open the <Link className="ci-link" href="/explore">Data explorer</Link>, the <Link className="ci-link" href="/trials/intelligence">trial intelligence</Link> table or the <Link className="ci-link" href="/graph">knowledge graph</Link>. | |
| 57 | 57 | </p> |
| 58 | 58 | </div> |
| 59 | 59 | </section> |
modified
apps/web/src/components/charts/line-chart.tsx
+1 −1
@@ -6,7 +6,7 @@ export interface Series { | ||
| 6 | 6 | dashed?: boolean; // e.g. estimated vs observed |
| 7 | 7 | } |
| 8 | 8 | |
| 9 | −const SERIES_COLORS = ['var(--color-accent)', '#8a4b0a', '#4a4a46', '#2d5f2e', '#8b1e2d', '#7b7b75']; | |
| 9 | +const SERIES_COLORS = ['var(--color-series-1)', 'var(--color-series-2)', 'var(--color-series-3)', 'var(--color-series-4)', 'var(--color-series-5)', 'var(--color-series-6)', 'var(--color-series-7)', 'var(--color-series-8)']; | |
| 10 | 10 | |
| 11 | 11 | /** |
| 12 | 12 | * Time-series line chart in pure SVG: years on x, values on y, optional confidence band. |
modified
apps/web/src/components/charts/trend-chart.tsx
+1 −1
@@ -12,7 +12,7 @@ export interface TrendSeries { | ||
| 12 | 12 | * Eight-way colour set that stays distinguishable in print and for common colour-vision deficiencies; |
| 13 | 13 | * shape (dash) and the legend label carry the meaning too — colour is never the only carrier. |
| 14 | 14 | */ |
| 15 | −const TREND_COLORS = ['#0f5f63', '#8a4b0a', '#8b1e2d', '#2d5f2e', '#4a4a46', '#5b3a8c', '#b5651d', '#1f5fa8']; | |
| 15 | +const TREND_COLORS = ['var(--color-series-1)', 'var(--color-series-2)', 'var(--color-series-5)', 'var(--color-series-4)', 'var(--color-series-3)', 'var(--color-series-7)', 'var(--color-series-6)', 'var(--color-series-8)']; | |
| 16 | 16 | |
| 17 | 17 | /** |
| 18 | 18 | * Multi-series yearly line chart in pure SVG for country trend panels (up to 8 series). Zero-based y axis, |
modified
apps/web/src/components/layout/site-header.tsx
+2 −0
@@ -2,6 +2,7 @@ import Link from 'next/link'; | ||
| 2 | 2 | import { NAV } from '@/lib/site'; |
| 3 | 3 | import { CommandPalette } from './command-palette'; |
| 4 | 4 | import { MobileNav } from './mobile-nav'; |
| 5 | +import { ThemeToggle } from './theme-toggle'; | |
| 5 | 6 | |
| 6 | 7 | export function SiteHeader() { |
| 7 | 8 | return ( |
@@ -20,6 +21,7 @@ export function SiteHeader() { | ||
| 20 | 21 | </nav> |
| 21 | 22 | <div className="ml-auto flex items-center gap-2"> |
| 22 | 23 | <CommandPalette /> |
| 24 | + <ThemeToggle /> | |
| 23 | 25 | <MobileNav /> |
| 24 | 26 | </div> |
| 25 | 27 | </div> |
added
apps/web/src/components/layout/theme-toggle.tsx
+40 −0
@@ -0,0 +1,40 @@ | ||
| 1 | +'use client'; | |
| 2 | + | |
| 3 | +import { useEffect, useState } from 'react'; | |
| 4 | +import { Moon, Sun, Monitor } from 'lucide-react'; | |
| 5 | + | |
| 6 | +type Theme = 'light' | 'dark' | 'system'; | |
| 7 | +const KEY = 'ci-theme'; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * Light / dark / system toggle (SPEC §86). The choice is stored in localStorage and applied to | |
| 11 | + * <html data-theme> before first paint by the inline script in layout.tsx, so there is no flash. | |
| 12 | + * "system" removes the attribute and lets the prefers-color-scheme media query decide. | |
| 13 | + */ | |
| 14 | +export function ThemeToggle() { | |
| 15 | + const [theme, setTheme] = useState<Theme>('system'); | |
| 16 | + useEffect(() => { | |
| 17 | + const stored = (typeof window !== 'undefined' && window.localStorage.getItem(KEY)) as Theme | null; | |
| 18 | + if (stored === 'light' || stored === 'dark') setTheme(stored); | |
| 19 | + }, []); | |
| 20 | + const apply = (t: Theme) => { | |
| 21 | + setTheme(t); | |
| 22 | + const root = document.documentElement; | |
| 23 | + if (t === 'system') { | |
| 24 | + window.localStorage.removeItem(KEY); | |
| 25 | + root.removeAttribute('data-theme'); | |
| 26 | + } else { | |
| 27 | + window.localStorage.setItem(KEY, t); | |
| 28 | + root.setAttribute('data-theme', t); | |
| 29 | + } | |
| 30 | + }; | |
| 31 | + const next: Theme = theme === 'system' ? 'dark' : theme === 'dark' ? 'light' : 'system'; | |
| 32 | + const label = theme === 'system' ? 'Theme: system' : theme === 'dark' ? 'Theme: dark' : 'Theme: light'; | |
| 33 | + const Icon = theme === 'system' ? Monitor : theme === 'dark' ? Moon : Sun; | |
| 34 | + return ( | |
| 35 | + <button type="button" className="ci-theme-toggle" onClick={() => apply(next)} aria-label={`${label}. Switch to ${next}.`} title={`${label} — click for ${next}`}> | |
| 36 | + <Icon className="h-3.5 w-3.5" aria-hidden /> | |
| 37 | + <span className="hidden text-[11px] sm:inline">{theme === 'system' ? 'Auto' : theme === 'dark' ? 'Dark' : 'Light'}</span> | |
| 38 | + </button> | |
| 39 | + ); | |
| 40 | +} | |
modified
apps/web/src/lib/queries/stats.ts
+13 −1
@@ -8,6 +8,10 @@ export interface SiteCounts { | ||
| 8 | 8 | genes: number; |
| 9 | 9 | variants: number; |
| 10 | 10 | trials: number; |
| 11 | + activeTrials: number; // interventional, active statuses | |
| 12 | + recruitingPhase3: number; | |
| 13 | + approvedDrugs: number; // distinct drugs with ≥ 1 approval (approved/accelerated/conditional) in any jurisdiction | |
| 14 | + approvals: number; // approval records across authorities | |
| 11 | 15 | publications: number; |
| 12 | 16 | drugs: number; |
| 13 | 17 | sources: number; |
@@ -17,7 +21,7 @@ export interface SiteCounts { | ||
| 17 | 21 | lastIngestAt: Date | null; |
| 18 | 22 | } |
| 19 | 23 | |
| 20 | −const EMPTY: SiteCounts = { cancers: 0, subtypes: 0, allEntities: 0, genes: 0, variants: 0, trials: 0, publications: 0, drugs: 0, sources: 0, activeSources: 0, evidenceItems: 0, epidemiologyObservations: 0, lastIngestAt: null }; | |
| 24 | +const EMPTY: SiteCounts = { cancers: 0, subtypes: 0, allEntities: 0, genes: 0, variants: 0, trials: 0, activeTrials: 0, recruitingPhase3: 0, approvedDrugs: 0, approvals: 0, publications: 0, drugs: 0, sources: 0, activeSources: 0, evidenceItems: 0, epidemiologyObservations: 0, lastIngestAt: null }; | |
| 21 | 25 | |
| 22 | 26 | /** Live counts for the home ticker. Each label on the page states exactly what is counted (§248-249). */ |
| 23 | 27 | export async function getSiteCounts(): Promise<SiteCounts> { |
@@ -30,6 +34,10 @@ export async function getSiteCounts(): Promise<SiteCounts> { | ||
| 30 | 34 | (SELECT count(*) FROM genes) AS genes, |
| 31 | 35 | (SELECT count(*) FROM variants) AS variants, |
| 32 | 36 | (SELECT count(*) FROM clinical_trials) AS trials, |
| 37 | + (SELECT count(*) FROM clinical_trials WHERE study_type = 'INTERVENTIONAL' AND overall_status IN ('RECRUITING','NOT_YET_RECRUITING','ENROLLING_BY_INVITATION','ACTIVE_NOT_RECRUITING')) AS active_trials, | |
| 38 | + (SELECT count(*) FROM clinical_trials WHERE study_type = 'INTERVENTIONAL' AND overall_status = 'RECRUITING' AND 'PHASE3' = ANY(phases)) AS recruiting_phase3, | |
| 39 | + (SELECT count(DISTINCT drug_id) FROM drug_approvals WHERE status IN ('approved','accelerated','conditional')) AS approved_drugs, | |
| 40 | + (SELECT count(*) FROM drug_approvals) AS approvals, | |
| 33 | 41 | (SELECT count(*) FROM publications) AS publications, |
| 34 | 42 | (SELECT count(*) FROM drugs) AS drugs, |
| 35 | 43 | (SELECT count(*) FROM sources) AS sources, |
@@ -46,6 +54,10 @@ export async function getSiteCounts(): Promise<SiteCounts> { | ||
| 46 | 54 | genes: Number(r.genes), |
| 47 | 55 | variants: Number(r.variants), |
| 48 | 56 | trials: Number(r.trials), |
| 57 | + activeTrials: Number(r.active_trials), | |
| 58 | + recruitingPhase3: Number(r.recruiting_phase3), | |
| 59 | + approvedDrugs: Number(r.approved_drugs), | |
| 60 | + approvals: Number(r.approvals), | |
| 49 | 61 | publications: Number(r.publications), |
| 50 | 62 | drugs: Number(r.drugs), |
| 51 | 63 | sources: Number(r.sources), |
modified
apps/web/src/lib/sitemap.ts
+3 −2
@@ -1,5 +1,5 @@ | ||
| 1 | 1 | import 'server-only'; |
| 2 | −import { SITE_URL, NAV, FOOTER_NAV } from '@/lib/site'; | |
| 2 | +import { SITE_URL, NAV, MORE_NAV, FOOTER_NAV } from '@/lib/site'; | |
| 3 | 3 | import { cancerSlugsForSitemap, countActiveCancers } from '@/lib/queries/cancers'; |
| 4 | 4 | import { geneSymbolsForSitemap, countGenes } from '@/lib/queries/genomics'; |
| 5 | 5 | import { drugSlugsForSitemap, countDrugs } from '@/lib/queries/drugs'; |
@@ -48,7 +48,8 @@ export async function sitemapEntries(id: number): Promise<Entry[]> { | ||
| 48 | 48 | return [ |
| 49 | 49 | { url: SITE_URL, lastModified: now, changeFrequency: 'daily', priority: 1 }, |
| 50 | 50 | ...NAV.map((n) => ({ url: `${SITE_URL}${n.href}`, lastModified: now, changeFrequency: 'daily' as const, priority: 0.8 })), |
| 51 | − ...FOOTER_NAV.filter((f) => !NAV.some((n) => n.href === f.href)).map((n) => ({ url: `${SITE_URL}${n.href}`, lastModified: now, changeFrequency: 'weekly' as const, priority: 0.5 })), | |
| 51 | + ...MORE_NAV.filter((f) => !NAV.some((n) => n.href === f.href)).map((n) => ({ url: `${SITE_URL}${n.href}`, lastModified: now, changeFrequency: 'daily' as const, priority: 0.7 })), | |
| 52 | + ...FOOTER_NAV.filter((f) => !NAV.some((n) => n.href === f.href) && !MORE_NAV.some((m) => m.href === f.href)).map((n) => ({ url: `${SITE_URL}${n.href}`, lastModified: now, changeFrequency: 'weekly' as const, priority: 0.5 })), | |
| 52 | 53 | ...sources.map((s) => ({ url: `${SITE_URL}/source/${s.slug}`, lastModified: toDate(s.updated_at) ?? now, changeFrequency: 'weekly' as const, priority: 0.6 })), |
| 53 | 54 | ...metrics.filter((m) => m.snapshot_count > 0).map((m) => ({ url: `${SITE_URL}/rankings/${m.slug}`, lastModified: now, changeFrequency: 'daily' as const, priority: 0.7 })), |
| 54 | 55 | ]; |
modified
deploy/mld-manifest.cancerindex.json
+42 −15
@@ -21,23 +21,30 @@ | ||
| 21 | 21 | "apps/web/.next/" |
| 22 | 22 | ], |
| 23 | 23 | "requires": { |
| 24 | − "runtimes": ["pm2", "ngrok", "node", "pnpm", "postgresql@17"], | |
| 24 | + "runtimes": [ | |
| 25 | + "pm2", | |
| 26 | + "node", | |
| 27 | + "pnpm", | |
| 28 | + "postgresql@17" | |
| 29 | + ], | |
| 25 | 30 | "ram_gb": 6, |
| 26 | − "ports": [8250, 8251] | |
| 31 | + "ports": [ | |
| 32 | + 8250, | |
| 33 | + 8251 | |
| 34 | + ] | |
| 27 | 35 | }, |
| 28 | − "ram_mb_observed": null, | |
| 29 | − "size_mb": null, | |
| 36 | + "ram_mb_observed": 2500, | |
| 37 | + "size_mb": 600, | |
| 30 | 38 | "placement": { |
| 31 | 39 | "pin": null, |
| 32 | 40 | "prefer": "M4M64b", |
| 33 | − "avoid": ["M3U96b", "M1M32"], | |
| 41 | + "avoid": [ | |
| 42 | + "M3U96b", | |
| 43 | + "M1M32" | |
| 44 | + ], | |
| 34 | 45 | "reason": "M4M64b already runs Postgres 17 + pgvector, pnpm and redis with 772 GB of disk for the raw lake; M3U96b is reserved for hfmarketdata and M1M32 is the gateway." |
| 35 | 46 | }, |
| 36 | − "ngrok": { | |
| 37 | − "name": "cancerindex-ngrok", | |
| 38 | − "url": "www.cancerindex.io", | |
| 39 | − "port": 8250 | |
| 40 | − }, | |
| 47 | + "ngrok": null, | |
| 41 | 48 | "launchd": [], |
| 42 | 49 | "env_overrides": {}, |
| 43 | 50 | "hooks": { |
@@ -62,7 +69,14 @@ | ||
| 62 | 69 | "name": "cancerindex-web", |
| 63 | 70 | "manager": "pm2", |
| 64 | 71 | "script": "/opt/homebrew/bin/node", |
| 65 | − "args": ["node_modules/next/dist/bin/next", "start", "-p", "8250", "-H", "0.0.0.0"], | |
| 72 | + "args": [ | |
| 73 | + "node_modules/next/dist/bin/next", | |
| 74 | + "start", | |
| 75 | + "-p", | |
| 76 | + "8250", | |
| 77 | + "-H", | |
| 78 | + "0.0.0.0" | |
| 79 | + ], | |
| 66 | 80 | "interpreter": null, |
| 67 | 81 | "cwd": "{{HOME}}/apps/cancerindex/apps/web", |
| 68 | 82 | "env": { |
@@ -88,7 +102,10 @@ | ||
| 88 | 102 | "name": "cancerindex-api", |
| 89 | 103 | "manager": "pm2", |
| 90 | 104 | "script": "/opt/homebrew/bin/node", |
| 91 | − "args": ["node_modules/tsx/dist/cli.mjs", "apps/api/src/server.ts"], | |
| 105 | + "args": [ | |
| 106 | + "node_modules/tsx/dist/cli.mjs", | |
| 107 | + "apps/api/src/server.ts" | |
| 108 | + ], | |
| 92 | 109 | "interpreter": null, |
| 93 | 110 | "cwd": "{{HOME}}/apps/cancerindex", |
| 94 | 111 | "env": { |
@@ -112,7 +129,10 @@ | ||
| 112 | 129 | "name": "cancerindex-worker", |
| 113 | 130 | "manager": "pm2", |
| 114 | 131 | "script": "/opt/homebrew/bin/node", |
| 115 | − "args": ["node_modules/tsx/dist/cli.mjs", "workers/main.ts"], | |
| 132 | + "args": [ | |
| 133 | + "node_modules/tsx/dist/cli.mjs", | |
| 134 | + "workers/main.ts" | |
| 135 | + ], | |
| 116 | 136 | "interpreter": null, |
| 117 | 137 | "cwd": "{{HOME}}/apps/cancerindex", |
| 118 | 138 | "env": { |
@@ -136,7 +156,9 @@ | ||
| 136 | 156 | "name": "cancerindex-backup", |
| 137 | 157 | "manager": "pm2", |
| 138 | 158 | "script": "/bin/bash", |
| 139 | − "args": ["deploy/backup.sh"], | |
| 159 | + "args": [ | |
| 160 | + "deploy/backup.sh" | |
| 161 | + ], | |
| 140 | 162 | "interpreter": null, |
| 141 | 163 | "cwd": "{{HOME}}/apps/cancerindex", |
| 142 | 164 | "env": { |
@@ -151,5 +173,10 @@ | ||
| 151 | 173 | "autorestart": false, |
| 152 | 174 | "max_memory_restart": null |
| 153 | 175 | } |
| 154 | − ] | |
| 176 | + ], | |
| 177 | + "tunnel": { | |
| 178 | + "domain": "www.cancerindex.io", | |
| 179 | + "gateway": "BHS64", | |
| 180 | + "note": "MacLustr Tunnel (WireGuard + Caddy on BHS64) since 2026-09-10; ngrok retired" | |
| 181 | + } | |
| 155 | 182 | } |
| 156 | 183 | |