- index.css: add explicit .fc-col-header-cell.fc-day-sat/sun rules with
!important to override the generic header background, and cover
.fc-timegrid-col weekend cells so the tint reaches all views
- CalendarPage.tsx: render .fc-daygrid-event-dot manually in the timed
month-view eventContent branch — FC's eventContent hook replaces the
entire default inner content including the dot span, so the CSS target
had nothing to paint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Month timed events: dot + title only, hover reveals translucent card
- Month all-day events: keep translucent fill
- Time right-aligned in month view (ml-auto)
- Week/day view: title on top, time underneath for better scanning
- Remove 2px left accent border from all events
- Set color:'transparent' on FC event data to prevent inline style conflicts
- Recurring repeat icon preserved in all views
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous commit failed to remove inline color props due to CRLF line
endings. FullCalendar was still setting inline styles that override CSS.
calendarColor is now correctly in extendedProps for the eventDidMount callback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix category chips rendering in wrong position (between search and add button).
Chips now appear inline after the Categories toggle pill.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Category chips were rendering as a separate flex row that got pushed to the
far right (between search and add button). Flatten the layout so chips appear
inline immediately after the Categories toggle pill, separated by a divider.
Remove redundant wrapper divs from TodosPage, PeoplePage, LocationsPage —
CategoryFilterBar now owns its own flex-1 sizing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Convert hasSharingRef from useRef to useState in useCalendars so
refetchInterval reacts immediately when sharing is detected (P-01)
- Add extra="forbid" to ReorderItem schema to prevent mass-assignment (S-03)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1 fixes:
- W-01: Add start_period: 30s to backend healthcheck for migration window
- W-03: Narrow .dockerignore *.md to specific files (preserve alembic/README)
Phase 2 fixes:
- C-01: Wrap Argon2id calls in totp.py (disable, regenerate, backup verify,
backup store) — missed in initial AC-2 pass
- S-01: Extract async wrappers (ahash_password, averify_password,
averify_password_with_upgrade) into services/auth.py, refactor all
callers to use them instead of manual run_in_executor boilerplate
- W-01: Fix ntfy dedup regression — commit per category instead of per-user
to preserve dedup records if a later category fails
Phase 4 fixes:
- C-01: Fix optimistic drag-and-drop cache key to include date range
- C-02: Replace toISOString() with format() to avoid UTC date shift in
visible range calculation
- W-02: Initialize visibleRange from current month to eliminate unscoped
first fetch + immediate refetch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- AW-2: Scope calendar events fetch to visible date range via start/end
query params, leveraging existing backend support
- AW-3: Reduce calendar events poll from 5s to 30s (personal organiser
doesn't need 12 API calls/min)
- AS-4: Gate shared-calendar polling on hasSharedCalendars — saves 12
wasted API calls/min for personal-only users
- AS-2: Lazy-load all route components with React.lazy() — only
AdminPortal was previously lazy, now all 10 routes are code-split
- AS-1: Add Vite manualChunks to split FullCalendar (~400KB), React,
TanStack Query, and UI libs into separate cacheable chunks
- AS-3: Extract clockNow into isolated ClockDisplay memo component —
prevents all 8 dashboard widgets from re-rendering every minute
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- AW-1: Add composite index on calendar_members(user_id, status) for the
hot shared-calendar polling query
- AS-6: Add composite index on ntfy_sent(user_id, sent_at) for dedup lookups
- AW-5: Combine get_user_permission into single LEFT JOIN query instead of
2 sequential queries (called twice per event edit)
- AC-5: Batch cascade_on_disconnect — single GROUP BY + bulk UPDATE instead
of N per-calendar checks when a connection is severed
- AW-6: Collapse admin dashboard 5 COUNT queries into single conditional
aggregation using COUNT().filter()
- AC-3: Cache get_current_settings in request.state to avoid redundant
queries when multiple dependencies need settings in the same request
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- AC-1: Merge get_current_user into single JOIN query (session + user in
one round-trip instead of two sequential queries per request)
- AC-2: Wrap all Argon2id hash/verify calls in run_in_executor to avoid
blocking the async event loop (~150ms per operation)
- AW-7: Add connection pool config (pool_size=10, pool_pre_ping=True,
pool_recycle=1800) to prevent connection exhaustion under load
- AC-4: Batch-fetch tasks in reorder_tasks with IN clause instead of
N sequential queries during Kanban drag operations
- AW-4: Bulk NtfySent inserts with single commit per user instead of
per-notification commits in the dispatch job
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add .dockerignore for backend and frontend (DC-1: eliminates node_modules/
and .env from build context)
- Delete start.sh with --reload flag (DC-2: superseded by Dockerfile CMD)
- Create entrypoint.sh with exec uvicorn (DW-5: proper PID 1 signal handling)
- Pin base images to patch-level tags (DW-1: reproducible builds)
- Reorder Dockerfile: create appuser before COPY, use --chown (DW-2)
- Switch to npm ci for lockfile-enforced installs (DW-3)
- Add network segmentation: backend_net + frontend_net (DW-4: db unreachable
from frontend container)
- Add deploy.resources limits to all services (DW-6: OOM protection)
- Refactor proxy-params.conf to include security headers, deduplicate from
nginx.conf location blocks (DW-7)
- Add image/svg+xml to gzip_types (DS-1)
- Add wget healthcheck for frontend service (DS-2)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
W-02: Reset hasPrefetched ref when app re-locks so subsequent unlocks
refresh stale cache data.
S-01: Validate localStorage HSL values with regex to prevent CSS injection.
S-05: Defer prefetch until settings are loaded for accurate upcoming_days.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prefetches all main page queries (dashboard, upcoming, todos, reminders,
projects, people, locations) in parallel when the app unlocks, so the
TanStack Query cache is warm before the user navigates to each tab.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Browser paints cached Vite CSS (@layer base cyan defaults) before the
inline script populates the static style tag. Remove the competing
cyan defaults — accent vars now come exclusively from the static
<style id="umbra-accent"> tag in index.html, which the inline script
always populates with the correct color before first paint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Vite's initialization strips dynamically created elements from <head>.
Place <style id="umbra-accent"> directly in the HTML source instead of
creating it with createElement. Source-authored elements survive Vite's
head cleanup. The inline script populates it via textContent (XSS-safe).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The <style id="umbra-accent"> tag injected by index.html gets removed
during page initialization. useTheme now defensively recreates the tag
if it's missing, ensuring color changes from the settings page work.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Inline style attribute on <html> gets stripped during page load.
Switch to injecting a <style id="umbra-accent"> tag with !important
CSS custom properties which persists in the DOM and beats @layer base
defaults. useTheme updates the same style tag when settings load.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both the index.html inline script and useTheme now use setProperty
with 'important' priority flag. This is the highest CSS cascade
priority and cannot be overridden by Vite's stylesheet injection,
@layer rules, or source order. Removes the <style> tag injection
approach which was being overridden.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The inline script's style.setProperty values on <html> were being
stripped during Vite's CSS injection. Switch to injecting a <style>
tag with :root vars which persists in the DOM. Restore CSS defaults
as safety fallback. Update useTheme to sync both the style tag and
inline styles when settings load.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Guard useTheme effect to skip when settings are undefined, preventing
it from overwriting the inline script's cached color with cyan defaults.
Move CSS accent var defaults from index.css :root into the index.html
inline script so they are always set synchronously before paint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gate dashboard rendering on isLockResolved to prevent content flash
before lock state is known. Remove animate-fade-in from LockOverlay
so it renders instantly. Always write accent color to localStorage
(even default cyan) to prevent theme flash on reload. Resolve lock
state on auth query error to avoid permanent blank screen. Lift
mobileOpen state above lock gate to survive lock/unlock cycles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical: Lock state was purely React useState — refreshing the page reset it.
Now persisted server-side via is_locked/locked_at columns on user_sessions.
POST /auth/lock sets the flag, /auth/verify-password clears it, and
GET /auth/status returns is_locked so the frontend initializes correctly.
UI: Cache accent color in localStorage and apply via inline script in
index.html before React hydrates to eliminate the cyan flash on load.
UI: Increase TanStack Query gcTime from 5min to 30min so page data
survives component unmount/remount across tab switches without skeleton.
UI: Move Projects nav onClick from the icon element to the full-width
container div so the entire row is clickable when the sidebar is collapsed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Global ambient background with drifting gradient orbs, glassmorphism
cards, lightened color palette, live dashboard clock, calendar UI fixes,
and Upcoming widget polish. QA reviewed — 0 critical, all suggestions
addressed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
S-02: Confirmed drift-3 is used by auth/AmbientBackground — not dead code.
S-03: Extracted noise SVG data URI to module-level NOISE_SVG constant.
S-04: Added will-change: transform to drift orbs for GPU layer promotion.
S-05: Documented the 9 AM snooze default in getMinutesUntilTomorrowMorning.
S-06: Made calendar toolbar bg-card/95 with backdrop-blur-md for better
readability over the transparent FullCalendar grid.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
W-02: Renamed layout/AmbientBackground → AppAmbientBackground to avoid
naming collision with auth/AmbientBackground (IDE auto-import confusion).
S-01: Added visibilitychange listener to re-sync clock after tab
sleep/resume. Previously the interval would drift after laptop sleep
or long tab backgrounding.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Clock: Instead of starting a 60s interval from mount time (which drifts
from the system clock), calculate ms until the next :00 second mark,
setTimeout to that point, then setInterval every 60s from there.
Updated text: Replaced formatDistanceToNow (which flickered between
"less than a minute ago" / "a minute ago" / "2 minutes ago" on each
render) with a stable minute-based calculation derived from clockNow:
"just now" / "1 min ago" / "N min ago".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Displays current time before the date separated by a vertical bar:
"6:30 PM | Thursday, March 12, 2026". Updates every 60 seconds.
Uses tabular-nums for stable digit widths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Border was at 30% opacity — nearly invisible against the glassmorphic
card background. Restored to full border-border opacity for clear
section separation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Removed bg-card from sticky headers (was creating opaque bars against
glassmorphic card background)
- Reduced padding from pb-1.5 to py-0.5 for slimmer profile
- Added leading-none for proper vertical centering of chevron + text
- Softened border opacity to 30%, text to 70%, chevron to 60%
- Shrunk text from text-xs to text-[10px], chevron from h-3 to h-2.5
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The mobile view Select had md:hidden on the <select> element, but the
Select component wraps it in a <div> with an absolute ChevronDown icon
that remained visible. Moved md:hidden to a wrapper div so the entire
Select (including the chevron) is hidden on desktop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The outline variant used bg-background (opaque near-black) which created
a visible dark rectangle against semi-transparent card toolbars. Changed
to bg-transparent so outline buttons blend with their parent container.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
FullCalendar backgrounds were opaque while the toolbar used semi-transparent
glassmorphism cards, creating a patchy look. Now all FC elements match:
- Page background: transparent (ambient shows through grid)
- Column headers: semi-transparent (0.65 opacity)
- Neutral background: semi-transparent (0.65 opacity)
- More-popover: semi-transparent with backdrop blur
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Moved ambient from DashboardPage to AppLayout so all pages get the
drifting gradient effect, not just the dashboard
- Lightened card colors: --card 5% → 8%, --card-elevated 7% → 11%,
popover and FullCalendar backgrounds updated to match
- Renamed DashboardAmbient → AmbientBackground in layout/
- Glassmorphism class renamed dashboard-glass → ambient-glass,
applied at AppLayout content wrapper level
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drift animations translate orbs up to 80px, causing hard cutoff at
container edges. Giving orb layers inset: -100px provides enough
bleed room so the gradient edges are always beyond the overflow-hidden
boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Orbs repositioned centrally with larger ellipses (90%/80%) and higher
opacity (0.45/0.35) so glow is visible through glassmorphism cards
- Vignette reduced from 0.45 to 0.30, transparent zone expanded to 50%
- Card opacity reduced from 0.80 to 0.65 to let more ambient bleed through
- Added overflow-hidden on ambient container to prevent black bar artifacts
during drift animations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Blurred circle approach was invisible on near-black backgrounds.
Use radial-gradient orbs at 25%/15% opacity instead, with semi-transparent
cards (backdrop-filter: blur) so the ambient effect shows through.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Negative z-index (-z-10) placed orbs behind the body's opaque background,
making them invisible. Moved all ambient layers (orbs, noise texture,
vignette) into the DashboardAmbient component as absolute-positioned
children at z-0, with content at z-10. Boosted orb opacities to 12%/7%
for perceptible effect. Removed CSS pseudo-element approach in favor of
inline React elements for better stacking control.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three layered effects to make the dashboard feel alive:
1. DashboardAmbient: two accent-colored drifting orbs at very low opacity
(0.04/0.025) with 120px blur — subtle depth and movement
2. Noise texture + radial vignette via CSS pseudo-elements — breaks the
flat digital surface and draws focus to center content
3. Card breathe animation: data-driven 4s pulsing glow on CalendarWidget
(when event in progress) and TodoWidget (when overdue todos exist)
All effects respect prefers-reduced-motion, use accent CSS vars (works
with any user-chosen accent color), and are GPU-composited (transform +
opacity only) for negligible performance cost.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The type pill, time label, and priority pill were being removed on
hover and replaced with action buttons, causing layout reflow and
visible jitter. Now the labels stay rendered (invisible when hovered
for todos/reminders) to hold their space, and action buttons are
absolutely positioned on top. Events show no actions so their labels
stay visible on hover. Zero layout shift.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap TodoWidget in flex-1 container and add h-full to its Card so
the Upcoming Todos card stretches to fill remaining space in the
right column, keeping both columns visually aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace fixed maxHeight 520px with h-full + overflow-hidden so the
card stretches to match the right column height in the grid row.
The flex chain (Card flex-col → CardContent flex-1 min-h-0 →
ScrollArea flex-1 min-h-0) ensures content scrolls internally
within the row-determined height instead of capping independently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
C-01: Add 30-day lower bound on overdue todo/reminder queries to
prevent fetching entire history.
C-02: Remove dead include_past query param — past-event filtering
is handled client-side.
W-01: Add onError toast handlers to all three inline mutations.
W-02: Snooze dropdown opens upward (bottom-full) to avoid clipping
inside the ScrollArea overflow container.
S-06: Clamp getMinutesUntilTomorrowMorning() to max 1440 to stay
within ReminderSnooze schema bounds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: h-full on Card inside a flex-col parent with no explicit
height meant nothing constrained the card — ScrollArea max-h never
triggered overflow. Fix: Card uses maxHeight 520px as the outer cap,
flex-col layout with shrink-0 header, and min-h-0 on CardContent +
ScrollArea so the flex chain allows content to shrink and scroll.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The flex-col h-full layout caused the card to stretch to match the
grid row, pushing content beyond the ScrollArea max-height. Switched
to natural card height with max-h-[400px] on ScrollArea so the card
stays compact and scrolls internally without mismatching the right
column cards.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>