Replace per-dataset label drawing with a post-pass in afterDatasetsDraw
that collects all museum line endpoints, sorts by Y, then pushes overlapping
labels apart with a connector line back to the actual data point.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Hover: non-hovered lines fade to 15% opacity so active line pops out
- End labels: museum name rendered at the tip of each line (always visible,
stays full-opacity even when dimmed) with 110px right-padding for space
- Labels toggle: button in chart controls shows/hides per-point value labels
- interaction mode set to nearest/no-intersect for responsive hover
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When multiple museums are present, the report trend chart now renders one
colored line per museum plus a bold Total line, mirroring dashboard behavior.
Legend is updated to list each museum with its corresponding color.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When no museum is selected, all museums get individual lines. When a subset
is selected, only those museums are shown. Both Dashboard and Comparison
trend charts now follow this pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When 2+ museums are selected, the trend chart now renders one colored line
per museum plus a bold Total line, instead of a single aggregated line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UI/UX redesign:
- Module cards with master toggle + badge state for all report sections
- BreakdownModule with indeterminate checkbox and metric pill sub-toggles
- PillGroup replaces all text toggles and <select> (Language, VAT,
Confidentiality, Trend metric, Orientation) for full visual consistency
- Visual orientation picker (portrait/landscape card buttons)
- Comparison period in accent-tinted block, revealed contextually
- Footer meta strip: section count, date range, orientation, comparison flag
- Removed generic subtitle copy
Accessibility (audit findings C1–C3, H2, H6, L1–L2):
- aria-pressed on all PillGroup and orientation buttons
- role="group" + aria-label on every pill group and orientation row
- aria-hidden on decorative module badges and footer separator dots
- :focus-visible on rf-metric-pill, rf-orient-btn, rf-upload-btn, rf-remove-btn
- aria-label on upload/remove logo buttons
- Semantic <h2> elements replace <div> group labels
- alert() replaced with inline role="alert" error messages in footer + logo field
- aria-live="polite" sr-only region for PDF generation status
- aria-busy on generate button during PDF creation
Dark mode & theming (H1):
- All rgba(37,99,235,...) hard-codes replaced with color-mix(in srgb,
var(--accent) N%, transparent) so tints follow the accent token in dark mode
- rf-module-header:hover uses var(--hover) instead of rgba(0,0,0,0.02)
Performance (H8):
- getUniqueMuseums/getUniqueChannels wrapped in useMemo([data])
PDF fixes:
- ▲/▼ Unicode glyphs (outside Helvetica Latin-1 range) replaced with +/- prefix
- Chart width adapts to orientation via CHART_W constant
- Y-axis labels added to trend chart (padL 38pt)
Responsive (H4–H5):
- rf-metric-pill touch target increased to 8px/14px on mobile
- Mobile footer shows section count only; period/orientation details hide
Cleanup (M3):
- Removed dead CSS: rf-toggle, rf-toggle-opt, rf-section-title,
rf-check-h-group, rf-inline-row (7 rules)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Settings link to desktop nav bar for admin users
- Rewrite Settings page from table layout to responsive card list (fixes unusable mobile state)
- Filter bar (Dashboard + Comparison): collapsible panel on mobile via display:contents trick; stacked full-width dropdowns replace horizontal scroll
- Active filter count badge shown in collapsed filter header
- AltMultiSelect dropdowns go full-width on mobile to prevent viewport overflow
- Chart control separators hidden on mobile to avoid crowding
- Metric grid: 2-col at ≤700px, 1-col at ≤480px
- Comparison period cards: smaller font and tighter padding at ≤680px
- Page shell padding reduced on mobile (48px→20px top, 24px→16px sides)
- Settings page gets correct 80px bottom padding for mobile nav
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
~300 lines of code that were independently duplicated in Dashboard.tsx
and Comparison.tsx are now in shared modules:
- src/lib/locale.ts — LC interface, EN and AR language configs (merged
fields from both pages into one unified interface)
- src/lib/dateHelpers.ts — MONTH_KEYS, isLeap, makePresets, guessPreset,
periodNameL, dateRangeTextL, currentMonth, shiftYear
- src/components/shared/PeriodPicker.tsx — InlinePicker + PeriodHero
- src/components/shared/AltMultiSelect.tsx — AltMultiSelect
- src/components/shared/MetricCard.tsx — MetricCard
Dashboard.tsx and Comparison.tsx now import from these shared modules.
Zero behavioral changes — all props, ARIA, and render output unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 130-line (Dashboard) and 155-line (Comparison) inline <style> JSX
blocks are removed and replaced with static CSS in App.css.
Font-family values that changed per language are now set as CSS custom
properties (--alt-body-font, --alt-display-font, --alt-mono-font) via
the root element's style prop — 3 vars instead of re-injecting 130+
lines of CSS on every language switch.
The redundant @import font URLs are dropped (fonts already preloaded
in index.html). Default values for the three font vars are defined in
:root so the page renders correctly before JS executes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses remaining medium and low severity audit findings:
- H2: Dark mode @media selector narrowed to :root:not([data-theme]) so
OS-preference and manual-override blocks are now mutually exclusive
- L2: Remove ~410 lines of dead Slides Builder CSS (no component exists)
- M2: VAT toggle uses flex spacer instead of margin-inline-start:auto,
preventing layout break when filter bar wraps at medium-small widths
- M3: Page content max-width aligned to 1400px (matches nav bar)
- M5: Period picker toggle now has aria-controls="period-picker-panel"
pointing to the InlinePicker root in both Dashboard and Comparison
- M6: Offline badge cache timestamp exposed via sr-only span for
screen reader accessibility (was title-only before)
- M7: chartOpts/barHorizOpts/barNoLegend wrapped in useMemo([baseOpts])
to prevent unnecessary Chart.js re-renders
- L4: Filter bar scrolls horizontally on mobile instead of wrapping
- L5: Metrics grid uses auto-fit/minmax to eliminate orphaned cards
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses critical and high-severity findings from UI audit:
- C1: Define missing CSS tokens (--hover, --bg-primary/secondary/tertiary)
fixing broken hover states and Slides Builder backgrounds
- C2: Chart colors now read CSS custom properties at render-time via
getChartTheme(), adapting tooltip, ticks, and grid to dark mode
- C3: Multi-select ARIA fixed — label elements now carry role="option"
and aria-selected for valid listbox semantics
- H1/M1: Remove unused --gold and duplicate --primary tokens;
replace all var(--primary) with var(--accent) throughout App.css
- H3/H4: Focus-visible outlines added to all custom interactive elements
(chips, controls, year buttons, hero button, multi-select trigger)
- H5: access-badge--full hardcoded colors replaced with design tokens
- H7: aria-pressed added to all chart toggle buttons
- L1: Hardcoded #fff/white replaced with var(--text-inverse)
- M4: index.html now preloads DM Serif Display, Outfit, and IBM Plex
Sans Arabic — all fonts actually used in the app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The header already has a language switcher; the one in the filter
bar was redundant on both Dashboard and Comparison pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace Dashboard/Comparison with DashboardDemo/PeriodSelectorDemo as primary pages at / and /comparison
- New editorial design: DM Serif Display + Outfit fonts, inline period picker, multi-select filters for museum/channel/district
- Full Arabic RTL support with IBM Plex Sans Arabic; EN/AR toggle synced to global LanguageContext
- Bar/pie chart toggle + absolute/percent toggle for museum, channel, district charts
- Refined top nav: transparent inactive links, accent active state, visual separator between nav links and utilities
- DateRangePicker, MultiSelect, FilterControls shared components added
- NavDemo: sidebar layout alternative (accessible at /nav-demo)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dashboard: PeriodPicker replaces year + quarter dropdowns. Defaults to
current month. YoY stat card now compares same range vs previous year.
Comparison: two independent PeriodPicker blocks (Period A and Period B).
Changing Period A auto-updates Period B to same period previous year,
but Period B remains freely editable.
Both pages use filterDataByDateRange; Filters type drops year/quarter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /api/etl/sync now returns immediately (202-style).
GET /api/etl/status shows running state, current month being
processed, and final result or error when done.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bundle tickets grant access to multiple museums, so each museum
should count 1 visitor — not 0.5. Revenue split remains 50/50.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PATCH /api/users/:id route to update user permissions
- Auth session stores and returns allowedMuseums/allowedChannels
- User type gains AllowedMuseums/AllowedChannels (JSON string fields)
- parseAllowed() with fail-closed semantics (empty string → null → no data)
- Dashboard/Comparison apply permission base filter before user filters
- Filter dropdowns (museums, channels, years, districts) derived from
permission-filtered data — restricted users only see their allowed options
- Settings UserRow component with inline checkbox pickers for access config
- Access badges in users table showing current restriction summary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Update deploy.yml to rsync server/, install deps, write .env from
Gitea secrets, and restart hihala-dashboard.service
- Move tsx to regular dependencies for production use
- Remove unused SESSION_SECRET from config
- Accept PORT env var as fallback for SERVER_PORT
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Server checks PIN against env (super admin) + NocoDB Users table
- Session stores name + role (admin/viewer)
- Admin: sees Settings page (seasons + users management)
- Viewer: sees Dashboard + Comparison only, no Settings
- Users CRUD on Settings page: add name + PIN + role, delete
- Settings link + nav hidden for non-admin users
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Server: POST /auth/login (verify PIN, set httpOnly cookie)
- Server: GET /auth/check, POST /auth/logout
- Client: Login page shown when not authenticated
- Session persists 7 days via httpOnly cookie
- PIN stored server-side only (ADMIN_PIN env var)
- Dashboard loads data only after successful auth
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The catch-all /api proxy was swallowing /api/seasons requests before
the specific proxy rule could match. Narrowing to /api/v2 fixes this
since all NocoDB REST calls use /api/v2/ paths.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>