Files
Tanken/data/schema.sql
T
Felitendo 96c0cb48ac feat: anchor price colors on country-wide 24h percentile band
Replace the monotonically-growing P20/P80 session anchor with a P10/P50/P90
band computed from the last 24 hours of station_prices, per country and
fuel. Frontend renders a three-stop HSL gradient (green -> pale yellow at
the median -> red), so 1.82 EUR and 1.84 EUR near the median read as
near-identical instead of greenish vs brownish. When the band is missing
(API down, empty DB, <50 samples), markers stay neutral grey — no
fallback to the old session-anchor logic.

- station_prices.fuel_type column + idempotent backfill from
  scan_locations (DE) and station_cache (AT)
- persistPriceSnapshot now writes one snapshot per fuel so fuel_type is
  correct on every row
- new getCountryPriceBand with a 10-minute in-memory cache
- new GET /api/price-band?fuel=...
2026-05-11 13:49:06 +02:00

140 lines
5.1 KiB
SQL

CREATE TABLE IF NOT EXISTS app_meta (
key TEXT PRIMARY KEY,
value_json JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
data_json JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
data_json JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS oauth_states (
key TEXT PRIMARY KEY,
data_json JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS price_history (
id BIGSERIAL PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
min_price DOUBLE PRECISION,
avg_price DOUBLE PRECISION,
max_price DOUBLE PRECISION,
station TEXT,
num_stations INTEGER
);
CREATE INDEX IF NOT EXISTS idx_price_history_timestamp ON price_history(timestamp DESC);
-- Location-based scanning support
ALTER TABLE price_history ADD COLUMN IF NOT EXISTS location_id TEXT;
CREATE INDEX IF NOT EXISTS idx_price_history_location ON price_history(location_id, timestamp DESC);
-- Per-station price data (populated by scheduler scans)
CREATE TABLE IF NOT EXISTS station_prices (
id BIGSERIAL PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
location_id TEXT,
station_name TEXT NOT NULL,
station_brand TEXT NOT NULL DEFAULT '',
price DOUBLE PRECISION NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_station_prices_timestamp ON station_prices(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_station_prices_location ON station_prices(location_id, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_station_prices_name ON station_prices(station_name, timestamp DESC);
-- Tankerkönig station IDs disambiguate rows whose station_name collides
-- across real-world branches (e.g. many rows named "JET TANKSTELLE"). Older
-- rows have NULL here; readers should prefer station_id when available
-- and fall back to station_name otherwise.
ALTER TABLE station_prices ADD COLUMN IF NOT EXISTS station_id TEXT;
CREATE INDEX IF NOT EXISTS idx_station_prices_id ON station_prices(station_id, timestamp DESC);
-- fuel_type is needed for country-wide price-band aggregation (color heatmap).
-- New rows are written with the fuel they were scanned for; the two UPDATEs
-- backfill historical rows from scan_locations (DE admin-curated) and
-- station_cache (AT grid + DE cached). Both UPDATEs are no-ops once all rows
-- have a fuel_type, so they are safe to leave in the boot path.
ALTER TABLE station_prices ADD COLUMN IF NOT EXISTS fuel_type TEXT;
CREATE INDEX IF NOT EXISTS idx_station_prices_fuel_time ON station_prices(fuel_type, timestamp DESC);
UPDATE station_prices sp
SET fuel_type = sl.fuel_type
FROM scan_locations sl
WHERE sp.location_id = sl.id
AND sp.fuel_type IS NULL;
UPDATE station_prices sp
SET fuel_type = sc.fuel_type
FROM station_cache sc
WHERE sp.location_id = sc.location_id
AND sp.fuel_type IS NULL;
-- Persistent station cache (survives restarts, populated by admin scan locations + AT grid)
CREATE TABLE IF NOT EXISTS station_cache (
location_id TEXT PRIMARY KEY,
lat DOUBLE PRECISION NOT NULL,
lng DOUBLE PRECISION NOT NULL,
radius_km DOUBLE PRECISION NOT NULL,
fuel_type TEXT NOT NULL,
stations JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Admin-curated scan locations (max 25 km radius — Tankerkönig list.php limit)
CREATE TABLE IF NOT EXISTS scan_locations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
country TEXT NOT NULL DEFAULT 'de',
lat DOUBLE PRECISION NOT NULL,
lng DOUBLE PRECISION NOT NULL,
radius_km DOUBLE PRECISION NOT NULL CHECK (radius_km BETWEEN 1 AND 25),
fuel_type TEXT NOT NULL DEFAULT 'diesel',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_by TEXT,
source_request_id TEXT,
last_scan_at TIMESTAMPTZ,
last_scan_station_count INTEGER,
last_scan_error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_scan_locations_enabled ON scan_locations(enabled);
-- User-submitted requests for new scan locations
CREATE TABLE IF NOT EXISTS location_requests (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
lat DOUBLE PRECISION NOT NULL,
lng DOUBLE PRECISION NOT NULL,
radius_km DOUBLE PRECISION NOT NULL CHECK (radius_km BETWEEN 1 AND 25),
note TEXT,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','denied')),
admin_note TEXT,
reviewed_by TEXT,
reviewed_at TIMESTAMPTZ,
resulting_location_id TEXT REFERENCES scan_locations(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_location_requests_user ON location_requests(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_location_requests_status ON location_requests(status, created_at DESC);
-- Legacy dump-import cleanup (dump-import feature was removed)
DROP TABLE IF EXISTS known_stations;
DELETE FROM station_cache WHERE location_id LIKE 'de-import-%';