a470b6fe9d082995cc20d15a19ffd07a2b4abe2e
5 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a470b6fe9d |
feat(water-log/settings): sticky in-page section nav for Smartsheet card
Deploy to route.crispygoat.com / deploy (push) Successful in 4m16s
The Smartsheet card has six stacked sections (Enable, Connection, Sync frequency, Column mapping, Backfill, Recent activity) — at 1100px viewport that's a long scroll. Add a sticky 'On this page' pill nav at the top of the card that highlights the active section as the user scrolls, with anchor jump on click. - Each <Card> gets a stable id (smartsheet-enable, -connection, -frequency, -mapping, -backfill, -activity) and scroll-mt-20 so anchor jumps clear the sticky nav. - IntersectionObserver (rootMargin 0 0 -80% 0) tracks which section is in the top 20% of the viewport and updates activeSection state. - Pill click uses native scrollIntoView, which honors the global scroll-behavior: smooth on <html> (auto-flipped to instant under prefers-reduced-motion by globals.css — no JS matchMedia needed). - URL hash updates via history.replaceState so the destination is shareable without triggering a second jump. - Apple HIG polish: rounded-full pills, focus-visible ring, active:scale-[0.96], motion-safe: spring transitions, bg-[#fdfaf2]/92 with backdrop-blur for the sticky chrome. - All changes additive — no existing Card/ToggleRow behavior modified. Verified: npx tsc --noEmit clean, npm run build clean, npm run lint on this file clean. |
||
|
|
61d5e3c6fa |
polish(water-log/settings): Apple HIG refinements to the Smartsheet surface
Deploy to route.crispygoat.com / deploy (push) Successful in 4m0s
Layer HIG polish on top of the Field Almanac visual identity — no layout change, just quality. Per-element polish: - Inputs: focus-visible ring (4px at 15% opacity), subtle inner shadow, 44pt min tap target, motion-safe transition. - Selects: same focus ring + min height + transition. - Toggles: Apple spring curve (cubic-bezier 0.32,0.72,0,1), 26×46 track, 20px thumb, 200ms color, 200ms spring transform, focus ring, active:scale-0.97. - Buttons: focus-visible ring, active:scale-0.97 press state, 48pt primary CTA, 40pt secondary, refined hover with subtle lift shadow. - Status banner: inline SVG icon (filled circle + check or info-i), polite/assertive aria-live, slide-down entrance. - Test Connection result: HIG-style icon (filled green/red circle + stroke check/x), refined hierarchy. - Backfill result + error: same HIG pattern as status banner. - Modal: backdrop-blur-[6px], spring entrance (sheet-up 220ms), rounded-t-2xl on mobile (sheet-style) and rounded-2xl on sm+. - All animations: motion-safe: + a prefers-reduced-motion block in globals.css that disables them. Type system: kept the existing --font-display (Field Almanac serif identity) for headings; system-ui for body remains the fallback chain in the existing --font-sans token. New keyframes in globals.css: smartsheet-fade-in, smartsheet-sheet-up, smartsheet-slide-down. All short (180–220ms) to feel responsive. Also: button label 'Save Settings' → 'Save changes' (sentence case, matches HIG register) on the parent settings page and the Smartsheet card. |
||
|
|
7d3de121c6 |
feat(water-log): add manual backfill to push historical entries to Smartsheet
Deploy to route.crispygoat.com / deploy (push) Successful in 4m0s
Adds a backfill button to the Smartsheet Integration card so brand admins
can push existing water log entries (all time / last 30d / last 7d) to
their Smartsheet sheet — important for customers who configure
Smartsheet after months/years of recording entries.
- New enqueueSmartsheetBackfill(brandId, {since?}) server action:
- Counts candidates, enqueues in 500-row chunks with
onConflictDoNothing (UNIQUE on brand_id+entry_id absorbs dups).
- Runs one inline drain pass (batchSize=200) for immediate feedback.
- Cron picks up remaining queue rows across the next ticks.
- Requires active Smartsheet config and admin permission; logs an
audit event for the backfill.
- New "Backfill historical entries" card in SmartsheetIntegrationCard
with range selector, confirmation prompt, and inline result feedback
(counts of newly queued / already queued / drained synced / skipped /
failed / remaining).
- Sync engine dedup by Entry ID + UNIQUE constraint means re-runs are
safe: re-running never creates duplicate Smartsheet rows.
|
||
|
|
242c195265 |
feat(water-log): add Smartsheet setup instructions modal
Deploy to route.crispygoat.com / deploy (push) Successful in 3m54s
Adds a 'Setup instructions' button to the Connection card that opens a modal with full onboarding guidance: - Prerequisites (Smartsheet account, admin access) - Step 1: Generate an API token (with link to Smartsheet docs) - Step 2: Sheet structure — full schema table mapping our 7 fields to Smartsheet column types (TEXT_NUMBER / DATETIME / DATE), with required vs optional flags and example values - Collapsible 'starter header row' CSV snippet users can paste into row 1 of a new sheet - Step 3: Connect flow (paste URL + token, test, map columns, save) - Common issues troubleshooting (401, 404 slug URLs, DATE column rejecting ISO timestamps) Modal UX: - role='dialog', aria-modal, aria-labelledby - Closes on X button, 'Got it' footer button, click-outside backdrop, or Escape key - Sticky header + footer so scrollable content stays framed - Locked to existing cream + forest palette (Field Almanac style) Implementation: - Added 'setupModalOpen' to state, OPEN_SETUP_MODAL/CLOSE_SETUP_MODAL actions to reducer - Extended Card subcomponent with optional 'action' slot (used here for the trigger button, reusable for future per-card actions) - Added useEffect that registers a window keydown listener only while the modal is open; cleans up on close or unmount - FIELD_SCHEMA const declared alongside the modal so future edits to the schema live next to their documentation |
||
|
|
fc70344dab |
feat(water-log): Smartsheet sync integration
Deploy to route.crispygoat.com / deploy (push) Successful in 5m34s
Per-brand Smartsheet integration that pushes new water_log_entries to a
configured sheet. Config UI lives at /admin/water-log/settings.
Database (0093_water_smartsheet_sync.sql):
- water_smartsheet_config: one row per brand; AES-256-GCM encrypted API
token (BYTEA), sheet id, column mapping JSONB, sync_frequency
(realtime | every_15_minutes | hourly), enable flag, last-sync
metadata. RLS via existing current_brand_id() pattern.
- water_smartsheet_sync_queue: one row per entry awaiting sync;
tracks status / attempts / exponential backoff. UNIQUE(brand_id,
entry_id) for idempotent enqueue.
- water_smartsheet_sync_log: append-only audit of every sync attempt
(success or failure); backs the Recent Activity panel in the UI.
Server:
- src/lib/crypto.ts: AES-256-GCM encrypt/decrypt + token mask helper.
Reads SMARTSHEET_TOKEN_ENC_KEY. Throws on missing/wrong-size key.
- src/lib/smartsheet.ts: typed REST wrapper (no SDK; the official
smartsheet npm has Node 22 compat issues). getSheetMeta, addRow,
findRowByColumn + typed SmartsheetApiError.
- src/services/smartsheet-sync.ts: syncEntryToSmartsheet (one entry),
drainSyncQueue (batch with backoff), runScheduledSync (cron entry).
Sequential processing stays well under Smartsheet's 300 req/min.
Max 5 attempts, then row stays 'failed' permanently.
- src/actions/water-log/smartsheet.ts: 6 server actions. Token never
returned in plaintext — UI gets maskedToken only. Permission check
matches the rest of the water-log module (can_manage_water_log).
- src/app/api/water-log/smartsheet-sync/route.ts: POST cron route,
bearer auth via SMARTSHEET_CRON_SECRET (falls back to CRON_SECRET).
Optional {brandId} body for manual retry.
- vercel.json: added */15 * * * * cron entry. Single entry covers
both 15-min and hourly frequencies (route filters per-brand).
UI:
- src/components/admin/water-log/SmartsheetIntegrationCard.tsx:
self-contained client component with useReducer. Sections: enable
toggle, sheet URL/ID + API token + Test Connection, sync frequency
radio group, column mapping dropdowns (populated after Test),
Recent Activity panel.
- src/app/admin/water-log/settings/page.tsx: mounted the card as new
'§ 05 — Integrations' section after the existing admin-PIN settings.
Card takes brandId as a prop (CLAUDE.md 'Brand ID Threading').
Hooks:
- src/actions/water-log/field.ts::submitWaterEntry: calls
triggerSyncForEntry(brandId, entryId) in a fire-and-forget void
after the entry insert. Sync failures NEVER bubble to the field
worker.
Env vars (.env.example):
- SMARTSHEET_TOKEN_ENC_KEY (REQUIRED, 32 random bytes base64)
- SMARTSHEET_CRON_SECRET (optional bearer)
- SMARTSHEET_SYNC_TIMEOUT_MS (optional, default 8000)
Deploy:
1. openssl rand -base64 32 → SMARTSHEET_TOKEN_ENC_KEY in .env.local
AND Vercel dashboard
2. npm run migrate:one 93 (pushes 0093_water_smartsheet_sync.sql)
3. Push triggers .gitea/workflows/deploy.yml
Rollback: remove cron entry, DROP the 3 tables, revert field.ts +
settings page hooks, git revert the merge commit. See MEMORY.md
for full rollback story + sharp edges (no key rotation, slug URLs).
|