Commit Graph

14 Commits

Author SHA1 Message Date
aaverbitskiy
8e6c19e9a4 feat: surface photos in the map balloon + fullscreen lightbox (v0.2.7)
Photos live on disk as photos/<board_key>/<file>.{jpg,jpeg,png} (a read-only
volume). A cached directory index (60s TTL) attaches a `photos` URL list to each
surface in /api/map; a /photos StaticFiles mount serves the files. Photos are
public — shown to anonymous visitors and managers alike.

The balloon renders a gallery (main image + thumbnail strip + "1 / N" counter);
clicking a thumbnail swaps the main image, and clicking the main image / "+N"
opens a fullscreen lightbox over the map (prev/next, Esc, backdrop close).
Surfaces without photos render exactly as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-15 08:00:10 +00:00
aaverbitskiy
5cabad9c33 feat: all-surfaces timeline + UI polish (v0.2.0)
Timeline can now list the entire inventory (occupied + free + never
booked), not just booked surfaces. New manager-only "Все поверхности"
checkbox (on by default; forced on and hidden for anonymous visitors so
they can spot what is currently free with no future bookings).

/api/boards gains all_surfaces: sources rows from board_info and unions
pf_board so a just-booked surface not yet in the inventory snapshot still
gets a row. Surface facets (city/dimension/search) narrow rows; booking
facets (brand/manager/status) narrow to booked surfaces; dates are a
window that narrows bars, not the surface set.

UI polish:
- timeline tooltip flips up/left near viewport edges
- anonymous "Войти" button filled accent, inverts to white-on-accent hover
- appearance button uses a gear icon instead of "+"
- header counter shows only "Поверхности: N"
- map "всего" counter includes surfaces without coordinates

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 14:48:25 +00:00
aaverbitskiy
b6b9c27cb0 auth: Keycloak OIDC — anonymous (truncated) vs manager (full) tiers
Backend (main.py + auth.py):
- Validate Bearer JWT against the intense-sale realm JWKS (RS256, issuer
  checked, aud skipped). Missing/invalid token = anonymous, never an error,
  so the public map keeps working when Keycloak is down.
- Anonymous truncation: /api/meta hides brand/manager/status facets;
  /api/bookings blanks brand/company_name/manager/task_id/task_status
  (bars stay, one neutral colour); /api/map keeps occupied_now but empties
  booking details. Hidden facet filters are ignored for anonymous callers.
- boards stay visible to all (physical inventory: address/city/dimension).

Frontend (keycloak-js):
- Silent SSO (check-sso) — anonymous by default, Войти/Выйти button.
- Bearer token attached + refreshed on every API call.
- Anonymous UI hides Brand/Manager/Status filters, Brand/Company toggles,
  appearance menu, collisions and labels; board detail panel (Planfix
  deep-links) gated to managers.

Test manager: user / 1234567890 (realm role `manager`).
2026-08-14 11:05:47 +00:00
aaverbitskiy
557522afcf ui: visual refresh, richer bar tooltip (+manager), header alignment fixes
Frontend (styles.css, index.html, main.ts):
- Design tokens: single accent/border/radius/shadow/spacing system; flat header
  (dropped the boxed groups) with a subtle bottom divider.
- Unified controls: 36px height, consistent focus ring; search field gets a
  magnifier icon; view-mode switch is now a compact vertical segmented control.
- Fixed the dropdown option font: `.field label` styles were leaking into the
  option <label>s (they're inside .field), forcing uppercase + bold; options are
  now normal weight / normal case.
- Header alignment: mode switch fills the row so its bottom lines up with the
  date fields; the counters block is pinned to the same bottom line.
- Dropdown lists no longer show the column-resizer line bleeding through: the
  header now establishes a stacking context (position:relative + z-index) so its
  panels sit above the timeline.
- Dark theme is implemented (tokens + no-flash) but hidden for now (toggle
  display:none, forced light) until it's polished — easy to re-enable.

Timeline tooltip (timeline.ts, types.ts):
- Replaced the plain multi-line text tooltip with a structured card: bold brand
  title + task №, company subtitle, localized dates + duration, status as a
  coloured dot, manager, a distinct collision section, and a "click to open in
  Planfix" hint. Left accent stripe in the bar colour; matches the map tooltip.
- Values are escaped (escapeHtml) before going into innerHTML.

Backend (main.py):
- /api/bookings now returns `manager` (Array(String)), canonicalized to the
  "Фамилия Имя" form (see canon_manager) so the tooltip can show it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-06 04:39:55 +00:00
aaverbitskiy
530bb4754e meta: run the 5 facet queries concurrently via asyncio.gather
/api/meta built the city/brand/dimension/manager/status option lists with five
sequential `await ch_query(...)` calls, so its latency was the sum of five
round-trips to ClickHouse. The queries are independent, and the frontend hits
/api/meta on every filter change, so this was 5x latency for no reason.

Wrap them in asyncio.gather so they fire in parallel; total time is now ~the
slowest single query plus overhead. Output verified identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 19:30:29 +00:00
aaverbitskiy
1f5b393675 bookings/map: read pf_board and board_info with FINAL to dedup ReplacingMergeTree
Both tables are ReplacingMergeTree; dedup happens only during background part
merges, not at read time. The ongoing etl_rw feed keeps inserting into pf_board,
so right after an insert that updates an existing row there is a short window
(before the forced merge, min_age_to_force_merge_seconds=15) where the same
task_id / board_key exists in two parts. A plain SELECT reads both parts and
returns the row twice.

The /api/boards query already collapses duplicates via argMax + GROUP BY, but
/api/bookings and the two /api/map queries read raw rows, so they could
intermittently double a booking (timeline/balloon) or a surface marker.

Add FINAL to those three reads (pf_board in bookings + map current, board_info
in map surfaces). At 463/515 rows the cost is negligible and it removes the
class of transient duplicates regardless of merge timing. Output verified
unchanged against the single-part baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 19:25:40 +00:00
aaverbitskiy
6816ba90f4 managers: normalize name word-order at read time, canonical "Фамилия Имя"
Planfix has emitted manager names in two word orders over time ("Имя Фамилия"
from the 26 Jul 2026 n8n backfill, "Фамилия Имя" from the ongoing etl_rw feed),
so one person appeared as two distinct filter options (e.g. "Александра Столбова"
vs "Столбова Александра"), splitting their tasks across two facet values.

Fix without touching stored data:
- MANAGER_KEY_ARR: SQL expression building a word-order- and case-independent
  key (lowercase, split, arraySort, rejoin) per manager array element.
- MANAGER_COND / MAP_MANAGER_COND now match on that key, so a selected manager
  catches rows stored under either order.
- parse_filters normalizes incoming manager values with the same key (_mkey).
- /api/meta returns canonical "Фамилия Имя" names (CANONICAL_MANAGERS),
  deduplicated; unknown names pass through unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 18:50:01 +00:00
aaverbitskiy
c7eae87660 filters: add faceted Status (task_status) filter — meta.statuses, boards/bookings/map filtering (map marker visibility), UI dropdown under Size in filter grid 2026-07-27 19:40:26 +00:00
aaverbitskiy
68417bf45e map: Yandex map section (3 modes + resizable split), green/red markers, hover tooltips for markers+clusters; fix map init race; header keeps counters inline (fields shrink first); add Manager (Менеджер) faceted filter 2026-07-26 05:05:04 +00:00
aaverbitskiy
d04e0ae024 db creds: move ClickHouse user/password to gitignored .env via compose env_file; drop hardcoded default from main.py 2026-07-23 05:19:13 +00:00
aaverbitskiy
66e501ef59 date filters: independent Дата начала (start_date>=) / Дата окончания (end_date<=), header fields under Город/Бренд; qualify pf_board.start_date/end_date to avoid alias shadowing 2026-07-21 10:07:39 +00:00
aaverbitskiy
fe06cdab72 dependent (faceted) filters: city/brand/dimension/search narrow each other 2026-07-21 08:38:53 +00:00
aaverbitskiy
5db77b7a06 brand filter (backend+frontend), compact checkbox grid, two-line counter 2026-07-18 11:15:43 +00:00
aaverbitskiy
6c47dada00 Import mapdash (FastAPI + vis-timeline) + docker-compose with static volume 2026-07-17 15:13:27 +00:00