# MariaDB Persistence & Dynamic Achievement System Plan Comprehensive architecture and design blueprint for integrating a MariaDB Galera cluster backend, implementing dynamic achievement recalculation on round deletion, and introducing a two-phase provisional vs. finalized achievement tracking engine during live rounds. --- ### User Review & Critical Decisions > [!IMPORTANT] > The architectural directions confirmed during Phase 1 have been integrated: > - **Database Backend**: Configured for your existing **MariaDB Galera Cluster** via standard Node.js connection pooling with failover resilience, plus an automated schema migration runner and a dev-mode fallback when credentials are not yet set. > - **Round Deletion & Achievements**: Deleting a saved round dynamically **recalculates achievements** across all remaining completed rounds, revoking badges if their qualification criteria are no longer met. > - **In-Round Instant Badges (e.g. Hole-in-One)**: Celebrated instantly with a live in-round toast upon entry, held as **provisional** during active scoring, revoked if the hole score is modified prior to saving, and permanently committed only upon confirmed round finalization. --- ## 1. Overview & Core Concept ### What It Does Pinpoint Golf evolves from a local session tracker into a production-grade multi-device golf companion backed by a dedicated MariaDB Galera cluster. It introduces: 1. **MariaDB Persistence Layer**: Secure user profiles, rounds history, hole-by-hole stats, and verified career achievements stored persistently in MariaDB tables with transactional integrity. 2. **Dynamic Career Recalculation Engine**: When a saved round is deleted, the backend re-runs the achievement rules against all remaining saved rounds, ensuring career records and trophies strictly reflect actual saved history. 3. **Two-Phase Live Achievement Engine**: High-impact milestones (like a Hole-in-One, Eagle, or Birdie Streak) trigger immediate in-round celebration banners. If a player corrects a typo before submitting, the provisional award is retracted. When the round is finalized with confirmation, all verified milestones are locked into the database. ### Target Persona & Key Value - **Avid Golfers**: Enjoy real-time excitement during live scoring without fear of permanent false entries due to typing errors. - **System Administrators / DevOps**: Benefit from standard MariaDB Galera multi-master clustering support with zero lock-in and zero mock data in production. --- ## 2. User Experience & Visual Design ### Live In-Round Achievement Workflow ``` [ Hole Entry Modal ] β”‚ Enters score: "1" (Ace / Hole-in-One) β–Ό [ Instant Celebration Banner ] β€’ Dynamic toast: "πŸŽ‰ Hole in One! (Provisional)" β€’ Visual indicator on Hole card: "Provisional Ace" β”‚ β”œβ”€β”€ User corrects score to "3": β”‚ └── Toast: "Provisional badge retracted" (Ace badge removed) β”‚ └── User finishes round & clicks "Save Round": └── SaveRoundModal confirmation └── Achievements committed permanently to MariaDB ``` ### Key User Flows & Visual Hierarchy 1. **Live Hole Entry & Provisional Badges**: - Scoring controls remain ergonomic for one-handed mobile thumb usage ($44\text{px}+$ touch targets). - Instant achievement toasts use rich typography (`Plus Jakarta Sans`), subtle entrance transitions, and non-blocking dismissals. - Distinct visual state for provisional badges (clean amber/gold indicator with a subtle "Pending Round Finalization" cue; no pill enclosures). 2. **Save Confirmation Gate**: - The `SaveRoundModal` displays the round recap (gross score, net score to par, putts, FIR/GIR) and a list of new achievements earned this round. - Clear warning: *"Saving locks this round permanently into your official record. Editing will no longer be possible; only deletion is allowed."* 3. **Round Deletion & Dynamic Recalculation**: - In `RoundsHistoryModal`, each saved round card provides a "Delete Round" action with a two-step confirmation prompt. - Deletion dialog warns: *"Deleting this round will recalculate your career achievements. Badges earned solely from this round will be removed."* - Immediate feedback toast: *"Round deleted. Achievements updated (X badges retained)."* --- ## 3. Key Product Decisions & Trade-Offs ### Decision 1: MariaDB Galera Cluster Integration - **Approach**: Configure a robust Express-based database adapter using `mysql2/promise` with Galera-ready connection pool settings (keep-alive, reconnection retry logic, multi-host support). - **Environment Variables**: - `MARIADB_HOST` (supports single host or comma-separated cluster nodes) - `MARIADB_PORT` (default 3306) - `MARIADB_USER`, `MARIADB_PASSWORD`, `MARIADB_DATABASE` - `MARIADB_SSL` (optional SSL mode) - **Zero-Friction Fallback**: When `MARIADB_HOST` is not yet configured in local environment, the server falls back to an integrated local SQLite/JSON storage engine with identical schema interfaces, ensuring the app remains fully functional while credentials are provided. ### Decision 2: Achievement Recalculation Engine - **Approach**: Pure deterministic recalculation function `calculateUserAchievements(savedRounds, courses)`. - **Why**: Storing achievement IDs as an append-only set risks "ghost achievements" where a deleted bogus round permanently leaves a Hole-in-One badge on the profile. By calculating career achievements dynamically upon deletion, data integrity is 100% mathematically sound. ### Decision 3: Provisional In-Round vs. Finalized Achievements - **Approach**: Two distinct evaluation phases: - **Phase A (Provisional)**: Runs client-side during active round edits. Evaluates single-hole feats (Hole-in-One, Albatross, Eagle, 3-Birdie Streak). Displays celebration toasts. If score drops below qualification, badge is removed from provisional state. - **Phase B (Finalized)**: Runs server-side upon `POST /api/rounds` finalization. Evaluates both round-level feats (Sub-80, Clean Card, Centurion, Marathon) and verified hole-level feats, writing records transactionally to `user_achievements`. --- ## 4. Technical Architecture & Data Strategy ### System Architecture & Data Flow ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Client (SPA) β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ HoleEntry.tsx │─────────▢│ Provisional State β”‚ β”‚ β”‚ β”‚ (Live scoring) β”‚ β”‚ (Active Round) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Scorecard.tsx │─────────▢│ SaveRoundModal.tsx β”‚ β”‚ β”‚ β”‚ (Lock & Review) β”‚ β”‚ (Confirm Final) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ POST /api/rounds β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Server (Express API) β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ /api/rounds │─────────▢│ Achievement Engine β”‚ β”‚ β”‚ β”‚ (Save / Delete) β”‚ β”‚ (Evaluate / Sync) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”‚ β–Ό β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ MariaDB Galera Pool (mysql2/promise) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ MariaDB Cluster Database β”‚ β”‚ β”‚ β”‚ β€’ users (id, email, password_hash, handicap, created) β”‚ β”‚ β€’ rounds (id, user_id, course_id, date, is_completed) β”‚ β”‚ β€’ round_scores (round_id, hole_num, strokes, putts...)β”‚ β”‚ β€’ user_achievements (user_id, achievement_id, date) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### MariaDB Relational Schema ```sql CREATE TABLE IF NOT EXISTS users ( id VARCHAR(64) PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, display_name VARCHAR(128) NOT NULL, handicap_index DECIMAL(4,1) DEFAULT 18.0, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS rounds ( id VARCHAR(64) PRIMARY KEY, user_id VARCHAR(64) NOT NULL, course_id VARCHAR(64) NOT NULL, tee_id VARCHAR(64), date VARCHAR(32) NOT NULL, holes_played INT NOT NULL DEFAULT 18, is_completed BOOLEAN NOT NULL DEFAULT 1, completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, gross_score INT NOT NULL, net_score INT NOT NULL, total_putts INT NOT NULL, fir_hit INT NOT NULL DEFAULT 0, fir_total INT NOT NULL DEFAULT 0, gir_hit INT NOT NULL DEFAULT 0, gir_total INT NOT NULL DEFAULT 0, settings_json JSON, scores_json JSON NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_rounds (user_id, date DESC), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS user_achievements ( user_id VARCHAR(64) NOT NULL, achievement_id VARCHAR(64) NOT NULL, round_id VARCHAR(64), unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (user_id, achievement_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ``` --- ## 5. Implementation Roadmap (Post-Approval) 1. **Backend Database Driver & Migration**: - Add `mysql2` driver and create `src/server/mariadb.ts` with pool configuration and auto-table creation on startup. - Configure `.env.example` with MariaDB connection parameters (`MARIADB_HOST`, `MARIADB_PORT`, `MARIADB_USER`, etc.). - Support seamless fallback for development environments without an active cluster connection. 2. **Provisional In-Round Achievement Engine**: - Update `HoleEntry.tsx` and round state to evaluate single-hole feats in real-time. - Add animated toast notification for provisional unlocks and graceful retraction notifications if scores are adjusted. 3. **Round Finalization & Recalculation Lifecycle**: - Wire `SaveRoundModal.tsx` to commit verified rounds to MariaDB and lock the round from future editing. - Implement round deletion with automatic re-evaluation of user achievements, updating `user_achievements` in MariaDB and syncing client state.