# π Chart-Board β Phased Work & Task Breakdown
> **Project:** Chart-Board (CodeIgniter 4)
> **Stack:** PHP 8.1 Β· CI4 Β· MySQL 8 Β· Bootstrap 5 Β· ApexCharts.js Β· Alpine.js
> **Total Estimated Duration:** ~14 Weeks (Solo) / ~7 Weeks (2-Dev Team)
---
## π Legend
| Symbol | Meaning |
|--------|---------|
| π΄ | Blocker β must be done before next task |
| π‘ | Important β high priority |
| π’ | Normal priority |
| βοΈ | Backend task |
| π¨ | Frontend task |
| π§ͺ | Testing task |
| π | Documentation task |
---
## ββββββββββββββββββββββββββββββββββββββββββ
## PHASE 1 β Project Foundation & Setup
### Estimated Time: 3β4 Days
## ββββββββββββββββββββββββββββββββββββββββββ
### 1.1 Environment & Scaffolding
- [x] π΄ βοΈ Install CodeIgniter 4 via Composer (`composer create-project codeigniter4/appstarter chart-board`)
- [x] π΄ βοΈ Configure `.env` file β `baseURL`, `database.*`, `CI_ENVIRONMENT`
- [X] π΄ βοΈ Set up MySQL database `chartboard` with `utf8mb4` charset
- [ ] π΄ βοΈ Run the full `chartboard.sql` schema to create all 15+ tables
- [x] π‘ βοΈ Configure `app/Config/Database.php` for MySQL connection
- [x] π‘ βοΈ Set up `app/Config/App.php` β timezone, base URL, session settings
- [x] π’ βοΈ Configure `app/Config/Cache.php` β file-based cache for dev, Redis for prod
- [x] π’ βοΈ Configure `app/Config/Email.php` β SMTP settings for alerts/verification
- [x] π’ βοΈ Set writable directory permissions (`chmod -R 777 writable/`)
- [x] π’ π Create `.gitignore` β exclude `.env`, `writable/`, `vendor/`
### 1.2 Front-End Base Setup
- [x] π΄ π¨ Integrate Bootstrap 5 via CDN or npm build pipeline
- [x] π΄ π¨ Integrate ApexCharts.js via CDN
- [x] π‘ π¨ Integrate Alpine.js for lightweight reactivity
- [x] π‘ π¨ Create base layout file `app/Views/layouts/main.php` β sidebar + topbar + content slot
- [x] π‘ π¨ Create `app/Views/layouts/auth.php` β centered card layout for login/register
- [x] π’ π¨ Set up global CSS variables file `public/assets/css/variables.css`
- [x] π’ π¨ Set up global JS file `public/assets/js/app.js` β sidebar toggle, toast, helpers
- [x] π’ π¨ Add favicon, logo mark assets to `public/assets/images/`
### 1.3 CI4 Structure Setup
- [x] π‘ βοΈ Create base `BaseController.php` β set shared data (user session, workspace)
- [x] π‘ βοΈ Create `app/Config/Routes.php` skeleton β group routes by module
- [x] π‘ βοΈ Create `AuthFilter.php` β redirect unauthenticated users to login
- [x] π‘ βοΈ Create `RoleFilter.php` β check workspace role before allowing access
- [x] π’ βοΈ Create `ApiAuthFilter.php` β validate `Authorization: Bearer` token for API routes
- [x] π’ βοΈ Set up CI4 Encryption service config (`app/Config/Encryption.php`)
- [x] π’ βοΈ Generate encryption key with `php spark key:generate`
---
## ββββββββββββββββββββββββββββββββββββββββββ
## PHASE 2 β Authentication & User Management
### Estimated Time: 4β5 Days
## ββββββββββββββββββββββββββββββββββββββββββ
### 2.1 User Model & Migration
- [x] π΄ βοΈ Create `UserModel.php` β CRUD, soft delete, findByEmail, findByApiToken
- [x] π‘ βοΈ Create `Migration_CreateUsersTable.php` (already in SQL; create CI4 migration file)
- [x] π’ βοΈ Create `InitialSeeder.php` β seed default super admin + default workspace
### 2.2 Registration
- [x] π΄ βοΈ `Auth/RegisterController.php` β show form, validate input, hash password, save user
- [x] π΄ βοΈ Generate `verify_token`, send verification email on registration
- [x] π‘ π¨ `app/Views/auth/register.php` β registration form with name, email, password, confirm password
- [x] π‘ βοΈ `Auth/RegisterController::verify()` β handle email verification token link
- [x] π’ π¨ Show success flash message after registration
- [ ] π’ π§ͺ Test: register with valid data, duplicate email, weak password
### 2.3 Login & Logout
- [x] π΄ βοΈ `Auth/LoginController.php` β validate credentials, check `email_verified`, start session
- [x] π΄ βοΈ Store user data in CI4 session: `user_id`, `name`, `email`, `role`
- [x] π΄ π¨ `app/Views/auth/login.php` β email + password form, remember me checkbox
- [x] π‘ βοΈ `Auth/LoginController::logout()` β destroy session, redirect to login
- [x] π‘ βοΈ Insert record into `user_sessions` on login, delete on logout
- [ ] π’ π§ͺ Test: wrong password, unverified email, active session redirect
### 2.4 Password Reset
- [x] π‘ βοΈ `Auth/PasswordController::forgot()` β generate `reset_token`, set `reset_token_expiry`, send email
- [x] π‘ βοΈ `Auth/PasswordController::reset()` β validate token, check expiry, update password
- [x] π‘ π¨ `app/Views/auth/forgot.php` β email input form
- [x] π‘ π¨ `app/Views/auth/reset.php` β new password + confirm form
- [ ] π’ π§ͺ Test: expired token, already-used token, mismatched passwords
### 2.5 User Profile
- [x] π‘ βοΈ `ProfileController.php` β show profile, update name/avatar, change password
- [x] π‘ π¨ `app/Views/profile/index.php` β profile card with avatar upload
- [x] π‘ βοΈ Handle avatar image upload to `writable/uploads/avatars/`
- [x] π‘ βοΈ `ProfileController::generateApiToken()` β create/rotate personal API token, save hashed
- [x] π’ π¨ Show/copy API token UI with regenerate button
- [ ] π’ π§ͺ Test: avatar upload size limits, password mismatch, token regeneration
### 2.6 Super Admin β User Management
- [x] π‘ βοΈ `Admin/UserController.php` β list all users, activate/deactivate, change role
- [x] π‘ π¨ `app/Views/admin/users/index.php` β paginated table with search and status filter
- [x] π’ π¨ `app/Views/admin/users/edit.php` β edit user role and active status
- [ ] π’ π§ͺ Test: deactivate user blocks login, role change reflects immediately
---
## ββββββββββββββββββββββββββββββββββββββββββ
## PHASE 3 β Workspace Management
### Estimated Time: 3β4 Days
## ββββββββββββββββββββββββββββββββββββββββββ
### 3.1 Workspace CRUD
- [x] π΄ βοΈ `WorkspaceController.php` β create, read, update, delete workspaces
- [x] π΄ βοΈ `WorkspaceModel.php` β with soft delete, slug generation, owner filter
- [x] π΄ π¨ `app/Views/workspace/index.php` β workspace list/grid with create button
- [x] π‘ π¨ `app/Views/workspace/create.php` β name, description, timezone, logo upload form
- [x] π‘ π¨ `app/Views/workspace/settings.php` β edit workspace details
- [x] π‘ βοΈ Auto-generate unique slug from workspace name on creation
- [x] π‘ βοΈ On workspace creation, auto-insert creator as `workspace_members` with role `admin`
- [x] π’ βοΈ Soft delete workspace β cascade to members, data sources, charts, dashboards
- [ ] π’ π§ͺ Test: duplicate slug, logo upload, owner-only delete restriction
### 3.2 Members & Invitations
- [x] π΄ βοΈ `WorkspaceMemberController.php` β list members, change role, remove member
- [x] π΄ βοΈ `WorkspaceInvitationController.php` β send invite email, accept invite, cancel invite
- [x] π‘ π¨ `app/Views/workspace/members.php` β members table with role dropdown and remove button
- [x] π‘ π¨ `app/Views/workspace/invite.php` β email + role form, pending invites list
- [x] π‘ βοΈ Generate secure `token` for invite, store in `workspace_invitations`, set expiry (48h)
- [x] π‘ βοΈ Public route `/invite/{token}` β if user exists log them in; else redirect to register
- [x] π’ βοΈ Prevent inviting existing members, prevent duplicate pending invites
- [ ] π’ π§ͺ Test: expired token, already-accepted token, role change enforcement
### 3.3 Workspace Context Switching
- [x] π‘ βοΈ Store `active_workspace_id` in session, set on login/switch
- [x] π‘ π¨ Workspace switcher dropdown in sidebar β list user's workspaces, highlight active
- [x] π’ βοΈ Middleware: validate user is member of active workspace on every request
- [ ] π’ π§ͺ Test: user with 0 workspaces, switching while on a dashboard page
---
## ββββββββββββββββββββββββββββββββββββββββββ
## PHASE 4 β Data Source Connections
### Estimated Time: 5β6 Days
## ββββββββββββββββββββββββββββββββββββββββββ
### 4.1 Data Source Model & Encryption
- [x] π΄ βοΈ `DataSourceModel.php` β CRUD, filter by workspace, soft delete
- [x] π΄ βοΈ `Libraries/Encrypter.php` β wrap CI4 Encryption to encrypt/decrypt credentials
- [x] π΄ βοΈ Encrypt `password`, `api_auth_value` fields before saving; decrypt on retrieval
- [ ] π’ π§ͺ Test: encrypted values are not plain text in DB, decryption returns correct value
### 4.2 Connection UI
- [x] π΄ π¨ `app/Views/datasource/index.php` β list all data sources with type icon, status badge
- [x] π΄ π¨ `app/Views/datasource/create.php` β dynamic form (type selector shows/hides fields)
- [x] π‘ π¨ Alpine.js: show MySQL/PostgreSQL fields when DB type selected; show API fields for REST API
- [x] π‘ π¨ `app/Views/datasource/edit.php` β edit form with masked password field
- [x] π‘ π¨ Connection status badge β Untested / Connected (green) / Failed (red)
- [x] π’ π¨ Delete confirmation modal
### 4.3 Connection Drivers
- [x] π΄ βοΈ `Libraries/Connectors/MySQLConnector.php` β connect via PDO, run test query `SELECT 1`
- [x] π΄ βοΈ `Libraries/Connectors/PostgreSQLConnector.php` β connect via PDO pgsql
- [x] π‘ βοΈ `Libraries/Connectors/MongoDBConnector.php` β connect via MongoDB PHP library URI
- [x] π‘ βοΈ `Libraries/Connectors/RestApiConnector.php` β cURL GET/POST with auth headers
- [x] π‘ βοΈ `Libraries/Connectors/CsvConnector.php` β parse uploaded CSV into in-memory array
- [x] π’ βοΈ `Libraries/ConnectionFactory.php` β factory to return correct connector by type
- [ ] π’ π§ͺ Test: each connector with valid/invalid credentials
### 4.4 Test Connection Endpoint
- [x] π΄ βοΈ `POST /datasource/test` (AJAX) β instantiate connector, run test, return JSON `{success, message}`
- [x] π΄ π¨ "Test Connection" button with spinner; show success/error inline below button
- [x] π‘ βοΈ Update `status` and `last_tested_at` in DB after test
- [ ] π’ π§ͺ Test: timeout handling (set cURL timeout 10s), wrong host, wrong credentials
### 4.5 Schema Browser (for Query Builder)
- [x] π‘ βοΈ `GET /datasource/{id}/schema` (AJAX) β return tables list and columns per table as JSON
- [ ] π‘ π¨ Schema sidebar in query builder β collapsible tree: Tables β Columns with types
- [x] π’ βοΈ Cache schema response for 5 minutes per data source
- [ ] π’ π§ͺ Test: DB with 100+ tables, special characters in column names
---
## ββββββββββββββββββββββββββββββββββββββββββ
## PHASE 5 β Query Builder
### Estimated Time: 5β6 Days
**Completion note:** Core MVP items are implemented (unified create/edit form, three query modes, multi-filter / multiβorder-by visual builder, variables for raw + visual, API headers in `api_params`, preview table + execution log, query cache). Open: CodeMirror editor, datasource schema tree in UI, visual aggregates + GROUP BY controls, chart/dashboard variable wiring (Phase 6), and listed tests.
---
### 5.1 Saved Query Model
- [x] π΄ βοΈ `SavedQueryModel.php` β CRUD, filter by workspace and data source, soft delete
- [ ] π’ π§ͺ Test: save and retrieve query with complex JSON config
### 5.2 Visual Query Builder (No-Code)
- [x] π΄ π¨ Visual mode in `app/Views/query/_form.php` (create/edit) β table name input + comma-separated columns *(no separate `visual.php`; schema dropdown / per-column checkboxes not implemented)*
- [ ] π΄ π¨ Schema-driven table dropdown + column checkboxes with optional aliases
- [x] π‘ π¨ Filter builder β add/remove filter rows: field, operator (=, !=, >, <, LIKE, IS NULL, IS NOT NULL), value
- [ ] π‘ π¨ Aggregate row β apply COUNT/SUM/AVG/MIN/MAX to numeric columns
- [ ] π‘ π¨ GROUP BY selector β multi-select *(backend `QueryBuilder::toSQL()` supports `group_by[]`; no UI yet)*
- [x] π‘ π¨ ORDER BY β multiple sort columns, each with ASC/DESC + add/remove rows
- [x] π‘ π¨ LIMIT input β max rows (default 500); supports `{{variable}}` in visual fields
- [x] π‘ βοΈ `QueryBuilder::toSQL()` β convert visual config JSON to safe parameterized SQL
- [x] π’ βοΈ Prevent destructive keywords: block `DROP`, `DELETE`, `UPDATE`, `INSERT`, `TRUNCATE` in generated SQL
- [ ] π’ π§ͺ Test: multi-filter query, aggregate with group by, null filter
### 5.3 Raw SQL Mode
- [ ] π΄ π¨ CodeMirror 6 editor integration β SQL syntax highlighting, auto-complete (table/column names) *(currently plain textarea in `_form.php`)*
- [x] π΄ π¨ Mode selector β Raw SQL / Visual builder / API query
- [x] π‘ βοΈ SQL safety check before execution β regex/parse to block DDL/DML mutations
- [x] π‘ βοΈ `QueryController::execute()` β run sanitized SQL on the selected data source, return JSON results
- [x] π’ βοΈ Enforce query timeout β kill query after 30 seconds
- [ ] π’ π§ͺ Test: malicious SQL injection attempt, timeout simulation, empty result set
### 5.4 Query Variables
- [x] π΄ βοΈ `Libraries/QueryVariableParser.php` β scan query string for `{{ var_name }}` pattern using regex, return list of variable names
- [x] π΄ βοΈ `Libraries/QueryVariableResolver.php` β resolve system variables (`{{today}}`, `{{now}}`, etc.) and substitute user values via PDO bindings; `resolveTemplateString()` for visual builder identifiers/literals
- [x] π΄ π¨ Variable panel β Raw SQL + Visual builder: detect / configure / test values; hidden for API mode on create/edit
- [x] π΄ π¨ Variable config form per detected variable:
- Label (display name shown to end user)
- Type: `text` / `number` / `date` / `date_range` / `select` / `multi_select`
- Default value
- For `select`/`multi_select`: options list (comma-separated or from another query)
- Required toggle
- [x] π‘ βοΈ Persist variable definitions in `query_variables` table (per `saved_query_id`) on save/update *(dashboard/chart `display_config` integration pending Phase 6)*
- [ ] π‘ π¨ Dashboard view β render a variable input widget per variable above each chart that has variables defined:
- `text` β ``
- `number` β ``
- `date` β date picker
- `date_range` β dual date range picker
- `select` β dropdown
- `multi_select` β multi-select dropdown with checkboxes
- [ ] π‘ π¨ On variable value change β re-fetch chart data AJAX with new values, re-render chart without page reload
- [ ] π‘ βοΈ Global dashboard filter β if multiple charts share a variable with the same name, a single widget controls all of them simultaneously
- [x] π‘ βοΈ Resolve built-in system variables server-side before query execution (no user input needed for these)
- [x] π’ π¨ Variable widget in Query Builder preview β show input fields for each detected variable so creator can test values before saving
- [x] π’ βοΈ `multi_select` in raw SQL β `QueryVariableResolver::resolve()` expands array values to multiple `?` placeholders (use inside `IN ({{var}})` in SQL)
- [ ] π’ βοΈ Public shared dashboards β variable widgets still visible and functional for anonymous viewers
- [ ] π’ π§ͺ Test: SQL injection attempt via variable value is blocked by PDO binding; date variable resolves correctly; missing required variable shows validation error; multi-select generates correct `IN` clause
### 5.5 API Query Builder
- [x] π‘ π¨ API query form β endpoint URL, JSON path, grouped sections (headers + field map with add/remove rows)
- [x] π‘ π¨ HTTP headers β repeatable rows; persisted in `saved_queries.api_params` as JSON `{ "headers": [...] }`
- [x] π‘ π¨ JSON path input β e.g. `data.results` to extract nested array
- [x] π‘ π¨ Field map β source JSON key β column alias rows
- [x] π‘ βοΈ `Libraries/ApiConnector::fetch()` β variable substitution on URL, headers, extract by JSON path
- [ ] π’ π§ͺ Test: nested JSON path, missing field graceful fallback, invalid URL
### 5.6 Query Preview & Result Table
- [x] π΄ π¨ "Run Query" button β AJAX call, show spinner while loading
- [x] π΄ π¨ Result preview table β first 100 rows, dynamic columns, sortable headers, pagination, CSV download, execution log tab
- [x] π‘ π¨ Row count badge, execution time badge
- [x] π‘ π¨ Save flow β name/description/meta on same form as builder; POST to store/update `saved_queries`
- [x] π’ π¨ Empty state β centered βNo rows returnedβ message in preview *(illustration asset optional)*
- [ ] π’ π§ͺ Test: 0 rows, 1000+ rows truncated, columns with special characters
### 5.7 Query Cache
- [x] π‘ βοΈ After execution, store result JSON in `query_cache` with MD5 cache key and TTL
- [x] π‘ βοΈ On next execution, check `query_cache` first; serve from cache if not expired
- [x] π’ βοΈ CI4 Cron task: `DeleteExpiredQueryCache` β run every 30 minutes to purge expired rows
- [ ] π’ π§ͺ Test: cache hit serves faster, cache miss goes to DB, expired cache re-fetches
---
## ββββββββββββββββββββββββββββββββββββββββββ
## PHASE 6 β Chart Builder
### Estimated Time: 6β7 Days
**Completion note:** Core MVP is in place: `ChartModel`, `SavedQueryRunner`, `ChartRenderer`, `ChartController`, multi-step builder (saved query first β no separate data-source step), ApexCharts live preview (`preview-query` / `preview-render`), `POST /chart/{id}/data` with CSRF refresh in JSON, chart list with Edit / Duplicate / Delete. Open: `public_token` generation, saved-query modal vs dropdown (dropdown implemented), unsaved-changes guard, table pagination/sort, legend position + custom hex + X-axis date format in UI, client `setInterval` refresh + spinner, search/filter, listed tests.
---
### 6.1 Chart Model
- [x] π΄ βοΈ `ChartModel.php` β CRUD, filter by workspace, soft delete
- [ ] π‘ βοΈ Public token generator (`is_public` / `public_token`) β pending (sharing phase)
- [ ] π’ π§ͺ Test: create chart with JSON display_config, retrieve and parse correctly
### 6.2 Chart Builder UI β Step Flow
- [x] π΄ π¨ Multi-step chart builder UI (Step 1: Saved query β Type β Fields β Style β Preview & save)
- [x] π΄ π¨ Step 1 β Choose saved query + run preview *(data source implied by query; no separate source step)*
- [x] π΄ π¨ Step 2 β Chart type selector grid (12 types with icons)
- [x] π‘ π¨ Step 3 β Field mapping: X-axis, Y-axis, Group By, Value (+ combo second metric); columns from preview
- [x] π‘ π¨ Step 4 β Display settings: title, subtitle, palette presets, legend toggle, data label toggle, Y-axis min/max, number format, grid / smooth / stacked / horizontal bar
- [x] π‘ π¨ Step navigation β Back / Next / Save; validation before advancing
- [x] π‘ π¨ Live preview β ApexCharts with real query data (`chart/preview-render`)
- [ ] π’ π¨ "Use Saved Query" button β modal to pick query *(dropdown on Step 1 covers pick-from-list)*
- [ ] π’ π¨ Unsaved changes warning on browser back/close
### 6.3 Chart Rendering Engine
- [x] π΄ βοΈ `Libraries/ChartRenderer.php` β chart config + rows β Apex-compatible options (`buildPayload`)
- [x] π΄ βοΈ Renderer: `bar`, `line`, `area`, `pie`, `donut`
- [x] π‘ βοΈ Renderer: `scatter`, `kpi_card`, `funnel`, `gauge`, `heatmap`, `combo`
- [ ] π‘ βοΈ `table` β paginated HTML table with sort *(basic truncated table only)*
- [x] π’ βοΈ Number formatter β currency (βΉ/$), percentage, decimals, K/M/B (server + client)
- [ ] π’ π§ͺ Test: each chart type with realistic data, empty data, single-row data
### 6.4 Chart Display Settings
- [x] π‘ π¨ Color palette presets (Ocean, Forest, Sunset, Mono) *(custom hex input β open)*
- [ ] π‘ π¨ Legend position selector (top/bottom/left/right/none) in builder UI
- [x] π‘ π¨ Refresh interval dropdown (Manual / 1 min / 5 min / 15 min / 1 hr) *(1 day option β open)*
- [ ] π’ π¨ Date format selector for time-series X-axis (dd/MM, MMM dd, MMM yyyy)
- [x] π’ π¨ Stacked bar/area toggle
### 6.5 Chart Auto-Refresh
- [ ] π‘ π¨ JavaScript: `refresh_interval > 0` β `setInterval` re-fetch
- [x] π‘ βοΈ `POST /chart/{id}/data` β re-run query, JSON render payload *(POST + `variables_json` for CSRF/vars; spec listed GET)*
- [ ] π’ π¨ Refresh spinner overlay while loading
- [ ] π’ π§ͺ Test: 1-minute refresh updates data without full page reload
### 6.6 Chart List & Management
- [x] π‘ π¨ `app/Views/chart/index.php` β card grid, type, last updated, data source + query name
- [x] π‘ π¨ Chart card actions β Edit, Duplicate, Delete *(Add to Dashboard / Share β Phase 7+)*
- [x] π‘ βοΈ `ChartController::duplicate()` β clone with name suffix `(copy)`
- [ ] π’ π¨ Search and filter charts by type or data source
- [ ] π’ π§ͺ Test: duplicate preserves all config, delete removes from all dashboards
---
## ββββββββββββββββββββββββββββββββββββββββββ
## PHASE 7 β Dashboard Builder
### Estimated Time: 6β7 Days
## ββββββββββββββββββββββββββββββββββββββββββ
### 7.1 Dashboard Model
- [ ] π΄ βοΈ `DashboardModel.php` β CRUD, filter by workspace, soft delete, public token generator
- [ ] π΄ βοΈ `DashboardWidgetModel.php` β CRUD widgets per dashboard, store grid position
- [ ] π’ π§ͺ Test: save layout_config JSON and retrieve widget positions correctly
### 7.2 Dashboard List
- [ ] π΄ π¨ `app/Views/dashboard/index.php` β card grid of dashboards with pinned section at top
- [ ] π‘ π¨ Dashboard card β name, description, chart count, last updated, share status badge
- [ ] π‘ π¨ Create Dashboard button β modal with name + description input
- [ ] π’ π¨ Pin/Unpin dashboard toggle
- [ ] π’ π§ͺ Test: 0 dashboards empty state, pinned order preserved on reload
### 7.3 Dashboard View Mode
- [ ] π΄ π¨ `app/Views/dashboard/view.php` β render all widgets in their grid positions
- [ ] π΄ π¨ Render chart widgets: fetch data via AJAX, render ApexCharts
- [ ] π‘ π¨ Render text widgets β parse Markdown to HTML using `marked.js`
- [ ] π‘ π¨ Render image widgets β `
` with configurable object-fit
- [ ] π‘ π¨ Global filter widgets β date range picker and dropdown filter
- [ ] π‘ βοΈ When global date filter changes, re-fetch all chart data with new date params injected into queries
- [ ] π’ π¨ Fullscreen button β expand dashboard to fill viewport, hide sidebar/topbar
- [ ] π’ π§ͺ Test: mixed widget types, dashboard with 20+ charts, date filter propagation
### 7.4 Dashboard Edit Mode (Drag & Drop)
- [ ] π΄ π¨ Integrate `gridstack.js` or `Muuri` for drag-and-drop grid layout
- [ ] π΄ π¨ "Edit Layout" toggle activates draggable/resizable mode on all widgets
- [ ] π‘ π¨ Resize handles on widget cards β drag corner to resize (min 1Γ1, max 4Γ3 units)
- [ ] π‘ π¨ "Add Widget" button in edit mode β opens modal to pick chart, text, image, or filter widget
- [ ] π‘ π¨ Remove widget button (β) visible only in edit mode
- [ ] π‘ βοΈ "Save Layout" β AJAX POST grid positions (x, y, w, h) per widget to `dashboard_widgets`
- [ ] π‘ π¨ "Discard Changes" β reload original layout from DB without saving
- [ ] π’ π¨ Widget title override input (optional per-widget title different from chart name)
- [ ] π’ π§ͺ Test: save layout, reload β positions preserved exactly; concurrent edit race condition
### 7.5 Dashboard Settings
- [ ] π‘ π¨ Dashboard settings panel β name, description, theme (light/dark/system), refresh interval
- [ ] π‘ βοΈ `DashboardController::updateSettings()` β update name, theme, refresh
- [ ] π’ π¨ Danger zone β delete dashboard with confirmation typing
- [ ] π’ π§ͺ Test: theme toggle persists on reload, refresh interval auto-starts
---
## ββββββββββββββββββββββββββββββββββββββββββ
## PHASE 8 β Alerts & Notifications
### Estimated Time: 4β5 Days
## ββββββββββββββββββββββββββββββββββββββββββ
### 8.1 Alert Model & CRUD
- [ ] π΄ βοΈ `AlertModel.php` β CRUD, filter by workspace, soft delete
- [ ] π΄ βοΈ `AlertHistoryModel.php` β insert triggered log, fetch recent history per alert
- [ ] π‘ π¨ `app/Views/alert/index.php` β list alerts with status (OK/Triggered/Muted), last triggered time
- [ ] π‘ π¨ `app/Views/alert/create.php` β form: chart selector, metric field, condition, threshold, channels
- [ ] π‘ π¨ `app/Views/alert/edit.php` β edit + mute/unmute toggle
- [ ] π’ π§ͺ Test: create alert with all fields, edit threshold, delete alert
### 8.2 Alert Engine (Background Check)
- [ ] π΄ βοΈ `Libraries/AlertEngine.php` β load active alerts, run chart query, compare value to threshold
- [ ] π΄ βοΈ CI4 Cron (`php spark alert:check`) β runs every minute via system cron job
- [ ] π‘ βοΈ `AlertEngine::evaluate()` β conditions: `gt`, `lt`, `eq`, `gte`, `lte`
- [ ] π‘ βοΈ Skip alert if `is_muted_until` is in the future
- [ ] π‘ βοΈ On trigger: send Email via CI4 Email library, send Slack message via Webhook HTTP POST
- [ ] π‘ βοΈ Log result to `alert_history` β value, channels notified, status (sent/failed/muted)
- [ ] π’ βοΈ Prevent duplicate notifications β if same alert triggered within last 5 minutes, skip
- [ ] π’ π§ͺ Test: threshold breach triggers notification, muted alert is skipped, failed webhook logs error
### 8.3 Alert Notifications UI
- [ ] π‘ π¨ Alerts sidebar panel (as in POC) β show recent triggered alerts with current value
- [ ] π‘ π¨ Alert history sub-page β paginated log of all past triggers per alert
- [ ] π‘ π¨ "Mute for" button β snooze alert for 1h / 4h / 24h
- [ ] π’ π¨ Red badge count on sidebar Alerts nav item for active triggered alerts
- [ ] π’ π§ͺ Test: snooze clears badge, alert re-triggers after snooze expires
---
## ββββββββββββββββββββββββββββββββββββββββββ
## PHASE 9 β Sharing & Embedding
### Estimated Time: 3β4 Days
## ββββββββββββββββββββββββββββββββββββββββββ
### 9.1 Public Share Links
- [ ] π΄ βοΈ `SharedLinkModel.php` β create token, find by token, increment view count
- [ ] π΄ βοΈ `SharingController::generate()` β create record in `shared_links`, return public URL
- [ ] π΄ βοΈ Public route `GET /share/{token}` β no auth required, load dashboard/chart view-only
- [ ] π‘ π¨ Share modal in dashboard/chart view β show public URL with copy button, QR code
- [ ] π‘ π¨ Optional password field β bcrypt hash stored, prompt on public page if set
- [ ] π‘ π¨ Optional expiry date picker β after expiry show "This link has expired" page
- [ ] π‘ βοΈ `SharingController::revoke()` β set `is_active = 0`, invalidate link
- [ ] π’ π¨ View count display in share modal ("Viewed 42 times")
- [ ] π’ π§ͺ Test: password protection, expired link, revoked link, view count increment
### 9.2 iFrame Embed
- [ ] π‘ π¨ Embed tab in share modal β show iframe HTML snippet with correct URL
- [ ] π‘ βοΈ Public share route: set `X-Frame-Options: ALLOWALL` header for embed URLs
- [ ] π‘ π¨ Embed view β stripped layout (no sidebar/topbar), chart/dashboard only
- [ ] π’ π¨ Embed size presets (640Γ480, 800Γ600, 1200Γ800, custom)
- [ ] π’ π§ͺ Test: embed renders in external `