Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Format roughly follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added
- **Submit screen reworked into one form; "bounty" retired for "request"** ([`frontend/src/app/submit/page.tsx`](frontend/src/app/submit/page.tsx), [`frontend/src/app/requests/`](frontend/src/app/requests), [`frontend/src/components/event/StatusBadge.tsx`](frontend/src/components/event/StatusBadge.tsx), [`frontend/src/components/ui/Pill.tsx`](frontend/src/components/ui/Pill.tsx), [`backend/app/routers/events/write.py`](backend/app/routers/events/write.py), [`backend/app/services/events.py`](backend/app/services/events.py), [`backend/app/routers/search.py`](backend/app/routers/search.py), [`backend/app/routers/admin.py`](backend/app/routers/admin.py), [`docs/design.md`](docs/design.md), [`planning/next.md`](planning/next.md)). The geolocation-vs-bounty mode toggle is gone: one form where you fill what you have, and two content-gated actions ("Publish geolocation", "Publish request") light up from a live requirements tick-list that escalates from the request floor to the full geolocation floor. A "Start from" chooser (Single / Bulk import) sits on top. The product term **"bounty" is retired for "request"** everywhere the user sees it: the route `/bounties` becomes `/requests` (redirect stubs kept), plus UI copy, the search filter, and internal names; the backend wire vocabulary follows (`type=request` search value, `/admin/seed-demo-requests`); the `requested` status value and the alembic / historical-changelog "bounty" are kept as they were. A new `secondary` Pill tone joins the palette, and the icon language settles as Megaphone = request, MapPin = geolocation (the old bounty target icon is retired). The Requests board drops its Open/Closed filter to show only open requests. Two backend relaxations for the real OSINT workflow: a request may now carry optional **proof images** (work started but not finished, reusing the geolocation intake), and an **event time without an event date** (an approximate hour from sun position is knowable before the day). `ruff` / `mypy` (78) / `pytest` (688) green; `tsc` / `eslint` (0 errors) / `vitest` (155) / `knip` / `palette` green; `api-types.ts` regenerated.
- **Light theme (Settings → Display)** ([`frontend/src/lib/theme.ts`](frontend/src/lib/theme.ts), [`frontend/src/lib/attributePreference.ts`](frontend/src/lib/attributePreference.ts), [`frontend/src/hooks/useTheme.ts`](frontend/src/hooks/useTheme.ts), [`frontend/src/hooks/useClientPreference.ts`](frontend/src/hooks/useClientPreference.ts), [`frontend/src/app/globals.css`](frontend/src/app/globals.css), [`frontend/src/components/map/Map.tsx`](frontend/src/components/map/Map.tsx), [`frontend/src/app/settings/page.tsx`](frontend/src/app/settings/page.tsx), [`frontend/src/app/layout.tsx`](frontend/src/app/layout.tsx), [`docs/design.md`](docs/design.md), [`planning/next.md`](planning/next.md)). A light / dark toggle joins the accent palette as a second, independent display axis. Dark stays the default; a manual switch flips it, browser-local (`localStorage` key `vidit:theme`) like the palette, with no OS `prefers-color-scheme` follow. The mechanism mirrors the accent system: `data-theme="light"` on `<html>` remaps the Tailwind `neutral-*` scale to a curated soft light ramp in `globals.css` (a soft warm grey canvas with warm off-white cards floating on it and dark-grey text, not a mechanical inversion that glares near-white), so every `neutral-*` utility, the scrollbar, and the MapLibre control chrome flip with no per-component change; dark carries no attribute. The block also mirrors the semantic `red` / `amber` scales (so danger + warning banners keep their meaning with dark text on a light tint instead of washing out) and sets `color-scheme` so native widgets track the theme. The map basemap can't read CSS variables, so `Map.tsx` swaps CARTO Dark Matter for its matched light counterpart Positron off `useTheme`, with a faint `sepia` on the canvas warming Positron's cool grey to match the surfaces, and flips the selected-point halo to a dark ring on the light basemap. The accent hue is untouched by the theme (the two axes compose). Both preferences (and the help-tooltip toggle) now share one plumbing: `attributePreference.ts` owns the `localStorage` ↔ `<html data-*>` reflection, `useClientPreference.ts` the `useSyncExternalStore` subscription, so no third near-duplicate store (kept `jscpd` at 0 % for the `typescript` set). A pre-paint inline script in the root layout applies the saved theme before first paint to avoid a dark flash. `tsc` / `eslint` / `knip` / `jscpd` / `palette:check` / `vitest` (160) green.
- **`vulture` dead-code gate on the backend** ([`backend/pyproject.toml`](backend/pyproject.toml), [`backend/vulture_whitelist.py`](backend/vulture_whitelist.py), [`.github/workflows/ci.yml`](.github/workflows/ci.yml), [`Makefile`](Makefile), [`docs/engineering.md`](docs/engineering.md), [`planning/next.md`](planning/next.md)). The backend gains a dead-code gate, the analogue of the frontend's `knip`: `vulture` finds unused functions, classes, methods, and fields that ruff's `F401` (unused imports only) does not. Config under `[tool.vulture]` scans `app` + `scripts`, excludes `alembic/versions`, and runs at `min_confidence = 60` (an unused module-level function scores 60, so the default 80 would let a genuinely orphaned function slip through and only ever catch unused locals). The framework noise that a 60 floor also surfaces is handled two ways: `ignore_decorators` (`@router.*` / `@app.*` route + middleware handlers, `@field_validator` / `@model_validator` methods) and `ignore_names` (`model_config`, the validator `cls`) clear the two blanket classes, and `vulture_whitelist.py` lists the residual attribute-access false positives (SQLAlchemy `Mapped[...]` columns hydrated on load, Pydantic response-model fields set by the service layer, dataclass fields read by attribute, one test-only helper). It runs in the `backend-lint` CI job after `mypy app` and locally via `make hygiene` (or `make vulture`); `vulture_whitelist.py` is excluded from ruff since its bare-name entries are vulture's own format, not runnable Python. The baseline triage found **no genuine dead code** (108 findings at confidence 60, all framework false positives), so the whitelist silences only framework magic, not real orphans. One column, `users.claimed_at`, has no explicit Python producer or consumer yet (its `server_default` stamps it and the assembled-profile claim flow that would write it is a deferred v0.4 item); it stays as a live, documented column that the ORM hydrates on load. `ruff` / `mypy` (82) / `vulture` / `pytest` (667) green.
- **Bounty test fixtures consolidated under `tests/events/`** ([`backend/tests/events/test_bounties.py`](backend/tests/events/test_bounties.py), [`backend/tests/events/conftest.py`](backend/tests/events/conftest.py), [`docs/engineering.md`](docs/engineering.md), [`planning/next.md`](planning/next.md)). `test_bounties.py` was a standalone module re-declaring the events package's `db` / `author` / tag fixtures; it moves under `tests/events/` and inherits them (the conftest's author teardown widened to a safe superset that also clears `event_claims` + `requested_by_id`, a no-op for the pure geolocations suites), and the local `_make_geolocated` helper folds into the shared `_make_geo`. Test-only, no production code. `ruff` / `mypy` (82) / `pytest` (667) green.
- **Bounty detail page reuses `EventDetailBody`** ([`frontend/src/components/event/EventDetailBody.tsx`](frontend/src/components/event/EventDetailBody.tsx), [`frontend/src/app/bounties/[id]/page.tsx`](frontend/src/app/bounties/%5Bid%5D/page.tsx), [`docs/design.md`](docs/design.md), [`planning/next.md`](planning/next.md)). The bounty detail page hand-rolled ~110 lines duplicating `EventDetailBody`'s Media + Details + curated-tag + Proof rendering. `EventDetailBody` is generalized (its `geo` prop widens so a bounty, an event without location / detected-from / requested-by, flows through with those spots rendered empty or dropped) plus a `detailExtras` slot for the bounty's Working-on + Closed rows; the page collapses to one `<EventDetailBody>` keeping its own actions. Follow-up to the bounty/geolocation cleanup. `tsc` / `knip` / `palette` / `vitest` (126) green.
Expand Down
12 changes: 8 additions & 4 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@

## Theme

**Dark minimal.** Uniform dark background, opaque panels, one warm accent for contrast. Dark for long-session comfort; data reads better on dark.
**Dark by default, light on demand.** Uniform background, opaque panels, one warm accent for contrast. Dark reads better for long-session, data-dense work and stays the default; light is a second axis for readers who want it.

The theme is independent of the accent hue. The pick is browser-local (`localStorage` key `vidit:theme`), applied as `data-theme="light"` on `<html>`, which remaps the Tailwind `neutral-*` scale (plus the semantic `red` / `amber` danger and warning scales, mirrored so their pale text stops go dark on the light tint) to a curated soft light ramp ([`globals.css`](../frontend/src/app/globals.css)); dark is the default and carries no attribute. The block also sets `color-scheme` so native widgets (scrollbars, date / select popups) track the theme. Every `neutral-*` utility flips with no per-component change, the same mechanism as the accent palette below, so both preferences share one plumbing ([`attributePreference.ts`](../frontend/src/lib/attributePreference.ts)). The orange accent is owned by the palette switch, so it is not theme-adjusted; accent text (links, the success banner) reads a touch lighter in light mode. The map basemap can't read CSS variables, so [`Map.tsx`](../frontend/src/components/map/Map.tsx) swaps CARTO Dark Matter for its light counterpart Positron off [`useTheme`](../frontend/src/hooks/useTheme.ts).

## Colour palette

### Foundation (dark)
### Foundation

The dark roles below are the default. Light theme re-points the same `neutral-*` scale to a curated soft ramp (`globals.css`): a soft warm grey canvas (`neutral-950`) with warm off-white cards (`neutral-900`) floating on it, and dark-grey text (`neutral-100` = `#232323`, not black), so a large light surface reads as easy on the eyes rather than a flat near-white glare. The light surfaces carry a faint warmth (`R > G > B`); the text greys stay neutral. It mirrors how the dark scale avoids pure black and pure white.

| Role | Color | Tailwind | Usage |
|------|-------|----------|-------|
| Background | `#0a0a0a` | `gray-950` | Global background, behind the map |
| Background | `#0a0a0a` | `neutral-950` | Global background, behind the map |
| Surface | `#171717` | `neutral-900` | Panels, cards, modals |
| Surface elevated | `#262626` | `neutral-800` | Inputs, interactive elements, hover |
| Border | `#333333` | `neutral-700` | Separators, field outlines |
Expand Down Expand Up @@ -97,7 +101,7 @@ Constants (the pill tones live on `<Pill>` as `PILL_TONE`; these colour-only pai

## Map

- **Style:** CARTO Dark Matter (with labels), `https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json`
- **Style:** CARTO Dark Matter (with labels), `https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json`; the light theme swaps its matched counterpart Positron, `.../gl/positron-gl-style/style.json` (see [Theme](#theme)), with a faint `sepia` on the canvas (`globals.css`) warming Positron's cool grey to match the warm light surfaces
- **Renderer:** MapLibre GL JS (vector tiles) with globe projection
- Map labels (cities, regions) are discreet light-gray
- Point geometry: default radius 6px, selected 7px + 2px white border; opacity 1.0 (points), 0.85 (clusters); pointer cursor on hover
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default function AuthLayout({
children: React.ReactNode;
}) {
return (
<main className="min-h-screen pl-14 flex items-center justify-center px-4 bg-[#0a0a0a]">
<main className="min-h-screen pl-14 flex items-center justify-center px-4 bg-neutral-950">
{children}
</main>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function Error({ error, reset }: ErrorProps) {
}, [error]);

return (
<main className="min-h-screen pl-14 flex items-center justify-center bg-[#0a0a0a] text-neutral-100">
<main className="min-h-screen pl-14 flex items-center justify-center bg-neutral-950 text-neutral-100">
<div className="max-w-md mx-auto px-6 text-center space-y-4">
<h1 className="text-2xl font-semibold text-orange-400">
Something went wrong
Expand Down
118 changes: 107 additions & 11 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
}
}

/* Dark is the default theme; the light block below flips this to `light`. Keeps
native widgets (scrollbars, date / select popups, autofill) in step. */
:root {
color-scheme: dark;
}

/* ---------------------------------------------------------------------------
Accent palette switching.

Expand Down Expand Up @@ -101,6 +107,79 @@
--color-orange-950: #4c0519;
}

/* ---------------------------------------------------------------------------
Light theme.

The second axis, independent of the accent palette above (which re-tints the
`orange-*` scale). The whole neutral base, backgrounds, text, borders, is a
Tailwind `neutral-*` utility compiling to `var(--color-neutral-*)`; light
mode re-points that scale on `<html data-theme="light">`, flipping the UI
with no per-component change. Dark is the default and stays inert (no
attribute, or `data-theme="dark"`, both keep Tailwind's defaults).

This is a curated soft ramp, not a mechanical inversion of the dark scale: a
flat near-white expanse is harsh on the eyes, so the canvas (`neutral-950`,
the largest surface) is a soft warm grey and the raised surfaces
(`neutral-900`, cards / panels / rail) sit a step *lighter*, reading as
near-white cards floating on the grey rather than the whole viewport going
white. Text tops out at a dark grey (`neutral-100` = #232323), not near-black,
so the contrast stays comfortable. The mid greys keep their order for borders
and muted text. This mirrors how the dark theme avoids pure black (#0a0a0a,
not #000) and pure white text (#f5f5f5, not #fff).

The map basemap can't read CSS variables, so `Map.tsx` swaps its style off
`useTheme` (dark-matter <-> positron); the marker hexes stay in `palette.ts`.
--------------------------------------------------------------------------- */
[data-theme="light"] {
/* Tell the UA to paint native widgets (date / select popups, autofill,
scrollbar corner) light too, so they track the theme instead of the OS. */
color-scheme: light;
--color-neutral-50: #1a1a1a;
--color-neutral-100: #232323;
--color-neutral-200: #303030;
--color-neutral-300: #454545;
--color-neutral-400: #585858;
--color-neutral-500: #767676;
--color-neutral-600: #9a9a9a;
/* The light surfaces carry a faint warmth (R > G > B) so the sidebar, cards,
and panels read as a warm off-white rather than a clinical grey; the text
greys above stay neutral. */
--color-neutral-700: #d0cdc7;
--color-neutral-800: #e4e1db;
--color-neutral-900: #f7f5f1;
--color-neutral-950: #eae8e4;

/* The semantic red / amber scales (danger + warning surfaces: FORM_ERROR_BANNER,
WARNING_CALLOUT, danger pills / buttons) are tuned light-on-dark, so on a
light surface the pale text stops wash out. Mirror each scale about its 500
midpoint: the hue stays, only lightness flips, giving dark saturated text on
a light tint (the light-mode counterpart of the dark recipe). The orange
accent is owned by the palette switch above, so it is deliberately left
alone here. */
--color-red-50: #450a0a;
--color-red-100: #7f1d1d;
--color-red-200: #991b1b;
--color-red-300: #b91c1c;
--color-red-400: #dc2626;
--color-red-500: #ef4444;
--color-red-600: #f87171;
--color-red-700: #fca5a5;
--color-red-800: #fecaca;
--color-red-900: #fee2e2;
--color-red-950: #fef2f2;
--color-amber-50: #451a03;
--color-amber-100: #78350f;
--color-amber-200: #92400e;
--color-amber-300: #b45309;
--color-amber-400: #d97706;
--color-amber-500: #f59e0b;
--color-amber-600: #fbbf24;
--color-amber-700: #fcd34d;
--color-amber-800: #fde68a;
--color-amber-900: #fef3c7;
--color-amber-950: #fffbeb;
}

/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
Expand Down Expand Up @@ -145,10 +224,10 @@
outline: none;
}

/* Custom scrollbar — dark theme */
/* Custom scrollbar: follows the neutral scale, so it flips with the theme */
* {
scrollbar-width: thin;
scrollbar-color: #404040 transparent;
scrollbar-color: var(--color-neutral-700) transparent;
}
*::-webkit-scrollbar {
width: 6px;
Expand All @@ -157,11 +236,11 @@
background: transparent;
}
*::-webkit-scrollbar-thumb {
background: #404040;
background: var(--color-neutral-700);
border-radius: 3px;
}
*::-webkit-scrollbar-thumb:hover {
background: #525252;
background: var(--color-neutral-600);
}

/* Native date/time inputs (date / time / datetime-local): mute the empty
Expand Down Expand Up @@ -190,27 +269,44 @@ input[type='datetime-local']:not(.has-value)::-webkit-datetime-edit {
order: 2 !important;
}

/* Dark theme for MapLibre controls */
/* MapLibre controls: paints follow the neutral scale, so they flip with the
theme; only the glyph inversion is theme-specific (see below). */
.maplibregl-ctrl-group {
background: #171717 !important;
border: 1px solid #404040 !important;
background: var(--color-neutral-900) !important;
border: 1px solid var(--color-neutral-700) !important;
}
.maplibregl-ctrl-group button {
border-color: #404040 !important;
border-color: var(--color-neutral-700) !important;
}
.maplibregl-ctrl-group button + button {
border-top-color: #404040 !important;
border-top-color: var(--color-neutral-700) !important;
}
.maplibregl-ctrl-group button span {
filter: invert(1);
}
.maplibregl-ctrl-attrib {
background: transparent !important;
color: #525252 !important;
color: var(--color-neutral-600) !important;
}
.maplibregl-ctrl-attrib a {
color: #525252 !important;
color: var(--color-neutral-600) !important;
}
.maplibregl-ctrl-attrib button.maplibregl-ctrl-attrib-button {
filter: invert(1);
}
/* MapLibre ships black glyph icons, inverted to white for the dark UI above.
In light mode keep them black (no invert) so they read on the light chrome. */
[data-theme="light"] .maplibregl-ctrl-group button span,
[data-theme="light"] .maplibregl-ctrl-attrib button.maplibregl-ctrl-attrib-button {
filter: none;
}
/* Warm the light basemap (Positron ships a cool grey) so it matches the warm
off-white surfaces, without hosting a custom style: a faint sepia on the map
canvas tints the greyscale land toward cream. The filter covers the whole
WebGL canvas, so the accent markers warm-shift slightly too; at 0.12 they stay
clearly on-hue (verified down to the blue palette, the coolest one). Light
theme only; the dark basemap (dark-matter) is untouched. The controls +
attribution are separate DOM, so they keep their neutral chrome above. */
[data-theme="light"] .maplibregl-canvas {
filter: sepia(0.12) saturate(1.05);
}
Loading
Loading