From fdccda93e15c2f0147dff16ea4d5007c794979d7 Mon Sep 17 00:00:00 2001 From: Gowtham M Date: Mon, 30 Mar 2026 09:51:33 +0530 Subject: [PATCH] Initial commit --- .cursorrules | 51 + .gitignore | 126 + LICENSE | 22 + README.md | 541 ++++ TASKS.md | 621 ++++ app/.htaccess | 6 + app/Commands/AlertCheck.php | 21 + app/Commands/DeleteExpiredQueryCache.php | 23 + app/Commands/PurgeOldAuditLogs.php | 32 + app/Common.php | 15 + app/Config/App.php | 202 ++ app/Config/Audit.php | 13 + app/Config/Autoload.php | 92 + app/Config/Boot/development.php | 34 + app/Config/Boot/production.php | 25 + app/Config/Boot/testing.php | 38 + app/Config/CURLRequest.php | 20 + app/Config/Cache.php | 162 ++ app/Config/Constants.php | 79 + app/Config/ContentSecurityPolicy.php | 176 ++ app/Config/Cookie.php | 107 + app/Config/Cors.php | 105 + app/Config/Database.php | 204 ++ app/Config/DocTypes.php | 43 + app/Config/Email.php | 121 + app/Config/Encryption.php | 92 + app/Config/Events.php | 55 + app/Config/Exceptions.php | 106 + app/Config/Feature.php | 37 + app/Config/Filters.php | 124 + app/Config/ForeignCharacters.php | 12 + app/Config/Format.php | 64 + app/Config/Generators.php | 44 + app/Config/Honeypot.php | 42 + app/Config/Images.php | 31 + app/Config/Kint.php | 63 + app/Config/Logger.php | 151 + app/Config/Migrations.php | 50 + app/Config/Mimes.php | 534 ++++ app/Config/Modules.php | 82 + app/Config/Optimize.php | 30 + app/Config/Pager.php | 38 + app/Config/Paths.php | 78 + app/Config/Publisher.php | 28 + app/Config/Routes.php | 159 ++ app/Config/Routing.php | 140 + app/Config/Security.php | 86 + app/Config/Services.php | 32 + app/Config/Session.php | 127 + app/Config/Toolbar.php | 122 + app/Config/UserAgents.php | 252 ++ app/Config/Validation.php | 44 + app/Config/View.php | 62 + app/Controllers/Admin/SettingsController.php | 62 + app/Controllers/Admin/UserController.php | 95 + app/Controllers/Alert/AlertController.php | 232 ++ app/Controllers/Audit/AuditController.php | 135 + app/Controllers/Auth/LoginController.php | 117 + app/Controllers/Auth/PasswordController.php | 98 + app/Controllers/Auth/RegisterController.php | 80 + app/Controllers/BaseController.php | 96 + app/Controllers/Chart/ChartController.php | 689 +++++ .../Dashboard/DashboardController.php | 497 ++++ .../DataSource/DataSourceController.php | 724 +++++ app/Controllers/Home.php | 11 + app/Controllers/ProfileController.php | 93 + app/Controllers/Query/QueryController.php | 1026 +++++++ .../Settings/SettingsController.php | 231 ++ app/Controllers/Share/LinkController.php | 184 ++ app/Controllers/Share/PublicController.php | 309 ++ .../Workspace/WorkspaceController.php | 284 ++ .../WorkspaceInvitationController.php | 150 + .../Workspace/WorkspaceMemberController.php | 94 + app/Database/Migrations/.gitkeep | 0 .../2026-03-26-083636_CreateUsersTable.php | 97 + ...03-28-140000_ExtendChartsChartTypeEnum.php | 26 + ...03-28-160000_AddThemePreferenceToUsers.php | 26 + ...03-29-120000_DashboardWidgetsFinerGrid.php | 30 + ...130000_RevertDashboardWidgetsFinerGrid.php | 29 + app/Database/Seeds/.gitkeep | 0 app/Database/Seeds/InitialSeeder.php | 74 + app/Filters/.gitkeep | 0 app/Filters/ActiveWorkspaceFilter.php | 68 + app/Filters/ApiAuthFilter.php | 35 + app/Filters/AuthFilter.php | 24 + app/Filters/RoleFilter.php | 34 + app/Filters/WorkspaceAdminFilter.php | 44 + app/Helpers/.gitkeep | 0 app/Language/.gitkeep | 0 app/Language/en/Validation.php | 4 + app/Libraries/.gitkeep | 0 app/Libraries/AlertEngine.php | 306 ++ app/Libraries/ApiConnector.php | 78 + app/Libraries/AuditLogger.php | 121 + app/Libraries/ChartRenderer.php | 767 +++++ app/Libraries/ConnectionFactory.php | 25 + app/Libraries/Connectors/CsvConnector.php | 33 + app/Libraries/Connectors/MongoDBConnector.php | 33 + app/Libraries/Connectors/MySQLConnector.php | 36 + .../Connectors/PostgreSQLConnector.php | 36 + app/Libraries/Connectors/RestApiConnector.php | 48 + app/Libraries/Encrypter.php | 42 + app/Libraries/QueryBuilder.php | 124 + app/Libraries/QueryExecutor.php | 96 + app/Libraries/QueryVariableParser.php | 27 + app/Libraries/QueryVariableResolver.php | 102 + app/Libraries/SavedQueryRunner.php | 314 ++ app/Models/.gitkeep | 0 app/Models/AlertHistoryModel.php | 101 + app/Models/AlertModel.php | 71 + app/Models/AuditLogModel.php | 80 + app/Models/ChartExportModel.php | 51 + app/Models/ChartModel.php | 83 + app/Models/DashboardModel.php | 89 + app/Models/DashboardWidgetModel.php | 64 + app/Models/DataSourceModel.php | 128 + app/Models/QueryCacheModel.php | 42 + app/Models/QueryVariableModel.php | 76 + app/Models/SavedQueryModel.php | 58 + app/Models/SettingsModel.php | 83 + app/Models/SharedLinkModel.php | 84 + app/Models/UserModel.php | 90 + app/Models/WorkspaceInvitationModel.php | 68 + app/Models/WorkspaceMemberModel.php | 65 + app/Models/WorkspaceModel.php | 100 + app/ThirdParty/.gitkeep | 0 app/Views/Pagers/cb_full.php | 43 + app/Views/admin/settings.php | 51 + app/Views/admin/users/edit.php | 55 + app/Views/admin/users/index.php | 91 + app/Views/alert/create.php | 85 + app/Views/alert/edit.php | 89 + app/Views/alert/history.php | 66 + app/Views/alert/index.php | 136 + app/Views/audit/index.php | 191 ++ app/Views/auth/forgot.php | 33 + app/Views/auth/login.php | 44 + app/Views/auth/register.php | 41 + app/Views/auth/reset.php | 29 + app/Views/chart/builder.php | 404 +++ app/Views/chart/builder_scripts.php | 430 +++ app/Views/chart/index.php | 60 + app/Views/dashboard/_card.php | 41 + app/Views/dashboard/_view_scripts.php | 565 ++++ app/Views/dashboard/index.php | 94 + app/Views/dashboard/settings.php | 111 + app/Views/dashboard/view.php | 150 + app/Views/datasource/_form.php | 119 + app/Views/datasource/_proof_content.php | 91 + app/Views/datasource/create.php | 47 + app/Views/datasource/edit.php | 49 + app/Views/datasource/index.php | 91 + app/Views/datasource/show.php | 56 + app/Views/errors/cli/error_404.php | 7 + app/Views/errors/cli/error_exception.php | 65 + app/Views/errors/cli/production.php | 5 + app/Views/errors/html/debug.css | 194 ++ app/Views/errors/html/debug.js | 116 + app/Views/errors/html/error_400.php | 84 + app/Views/errors/html/error_404.php | 84 + app/Views/errors/html/error_exception.php | 429 +++ app/Views/errors/html/production.php | 25 + app/Views/layouts/auth.php | 22 + app/Views/layouts/embed.php | 24 + app/Views/layouts/main.php | 167 ++ app/Views/profile/index.php | 94 + app/Views/query/_form.php | 505 ++++ app/Views/query/_query_scripts.php | 480 ++++ app/Views/query/create.php | 50 + app/Views/query/edit.php | 60 + app/Views/query/index.php | 71 + app/Views/query/show.php | 76 + app/Views/settings/notifications.php | 74 + app/Views/settings/workspace.php | 129 + app/Views/share/chart_view.php | 0 app/Views/share/dashboard_view.php | 64 + app/Views/share/expired.php | 7 + app/Views/share/gone.php | 7 + app/Views/share/modal.php | 247 ++ app/Views/share/password.php | 18 + app/Views/share/revoked.php | 7 + app/Views/welcome_message.php | 331 +++ app/Views/workspace/create.php | 72 + app/Views/workspace/index.php | 66 + app/Views/workspace/invite.php | 89 + app/Views/workspace/members.php | 73 + app/Views/workspace/settings.php | 92 + app/index.html | 11 + builds | 125 + chartboard.sql | 624 ++++ composer.json | 44 + composer.lock | 2534 +++++++++++++++++ env | 69 + phpunit.xml.dist | 63 + preload.php | 112 + public/.htaccess | 49 + public/assets/css/chart-builder.css | 218 ++ public/assets/css/dashboard.css | 377 +++ public/assets/css/query-builder.css | 363 +++ public/assets/css/variables.css | 1448 ++++++++++ public/assets/images/logo-mark.svg | 6 + public/assets/js/app.js | 122 + public/favicon.ico | Bin 0 -> 5430 bytes public/index.php | 59 + public/robots.txt | 2 + .../ui-sample-html-asset/PHASE 2/login.html | 204 ++ .../PHASE 2/register.html | 191 ++ .../ui-sample-html-asset/PHASE 2/reset.html | 172 ++ .../PHASE 3/workspace-module.html | 1058 +++++++ spark | 87 + tests/.htaccess | 6 + .../2020-02-22-222222_example_migration.php | 37 + .../_support/Database/Seeds/ExampleSeeder.php | 41 + tests/_support/Libraries/ConfigReader.php | 19 + tests/_support/Models/ExampleModel.php | 24 + tests/database/ExampleDatabaseTest.php | 46 + tests/index.html | 11 + tests/session/ExampleSessionTest.php | 17 + tests/unit/HealthTest.php | 49 + writable/.htaccess | 6 + writable/cache/index.html | 11 + writable/debugbar/index.html | 11 + writable/index.html | 11 + writable/logs/index.html | 11 + writable/session/index.html | 11 + writable/uploads/index.html | 11 + 226 files changed, 30139 insertions(+) create mode 100644 .cursorrules create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 TASKS.md create mode 100644 app/.htaccess create mode 100644 app/Commands/AlertCheck.php create mode 100644 app/Commands/DeleteExpiredQueryCache.php create mode 100644 app/Commands/PurgeOldAuditLogs.php create mode 100644 app/Common.php create mode 100644 app/Config/App.php create mode 100644 app/Config/Audit.php create mode 100644 app/Config/Autoload.php create mode 100644 app/Config/Boot/development.php create mode 100644 app/Config/Boot/production.php create mode 100644 app/Config/Boot/testing.php create mode 100644 app/Config/CURLRequest.php create mode 100644 app/Config/Cache.php create mode 100644 app/Config/Constants.php create mode 100644 app/Config/ContentSecurityPolicy.php create mode 100644 app/Config/Cookie.php create mode 100644 app/Config/Cors.php create mode 100644 app/Config/Database.php create mode 100644 app/Config/DocTypes.php create mode 100644 app/Config/Email.php create mode 100644 app/Config/Encryption.php create mode 100644 app/Config/Events.php create mode 100644 app/Config/Exceptions.php create mode 100644 app/Config/Feature.php create mode 100644 app/Config/Filters.php create mode 100644 app/Config/ForeignCharacters.php create mode 100644 app/Config/Format.php create mode 100644 app/Config/Generators.php create mode 100644 app/Config/Honeypot.php create mode 100644 app/Config/Images.php create mode 100644 app/Config/Kint.php create mode 100644 app/Config/Logger.php create mode 100644 app/Config/Migrations.php create mode 100644 app/Config/Mimes.php create mode 100644 app/Config/Modules.php create mode 100644 app/Config/Optimize.php create mode 100644 app/Config/Pager.php create mode 100644 app/Config/Paths.php create mode 100644 app/Config/Publisher.php create mode 100644 app/Config/Routes.php create mode 100644 app/Config/Routing.php create mode 100644 app/Config/Security.php create mode 100644 app/Config/Services.php create mode 100644 app/Config/Session.php create mode 100644 app/Config/Toolbar.php create mode 100644 app/Config/UserAgents.php create mode 100644 app/Config/Validation.php create mode 100644 app/Config/View.php create mode 100644 app/Controllers/Admin/SettingsController.php create mode 100644 app/Controllers/Admin/UserController.php create mode 100644 app/Controllers/Alert/AlertController.php create mode 100644 app/Controllers/Audit/AuditController.php create mode 100644 app/Controllers/Auth/LoginController.php create mode 100644 app/Controllers/Auth/PasswordController.php create mode 100644 app/Controllers/Auth/RegisterController.php create mode 100644 app/Controllers/BaseController.php create mode 100644 app/Controllers/Chart/ChartController.php create mode 100644 app/Controllers/Dashboard/DashboardController.php create mode 100644 app/Controllers/DataSource/DataSourceController.php create mode 100644 app/Controllers/Home.php create mode 100644 app/Controllers/ProfileController.php create mode 100644 app/Controllers/Query/QueryController.php create mode 100644 app/Controllers/Settings/SettingsController.php create mode 100644 app/Controllers/Share/LinkController.php create mode 100644 app/Controllers/Share/PublicController.php create mode 100644 app/Controllers/Workspace/WorkspaceController.php create mode 100644 app/Controllers/Workspace/WorkspaceInvitationController.php create mode 100644 app/Controllers/Workspace/WorkspaceMemberController.php create mode 100644 app/Database/Migrations/.gitkeep create mode 100644 app/Database/Migrations/2026-03-26-083636_CreateUsersTable.php create mode 100644 app/Database/Migrations/2026-03-28-140000_ExtendChartsChartTypeEnum.php create mode 100644 app/Database/Migrations/2026-03-28-160000_AddThemePreferenceToUsers.php create mode 100644 app/Database/Migrations/2026-03-29-120000_DashboardWidgetsFinerGrid.php create mode 100644 app/Database/Migrations/2026-03-29-130000_RevertDashboardWidgetsFinerGrid.php create mode 100644 app/Database/Seeds/.gitkeep create mode 100644 app/Database/Seeds/InitialSeeder.php create mode 100644 app/Filters/.gitkeep create mode 100644 app/Filters/ActiveWorkspaceFilter.php create mode 100644 app/Filters/ApiAuthFilter.php create mode 100644 app/Filters/AuthFilter.php create mode 100644 app/Filters/RoleFilter.php create mode 100644 app/Filters/WorkspaceAdminFilter.php create mode 100644 app/Helpers/.gitkeep create mode 100644 app/Language/.gitkeep create mode 100644 app/Language/en/Validation.php create mode 100644 app/Libraries/.gitkeep create mode 100644 app/Libraries/AlertEngine.php create mode 100644 app/Libraries/ApiConnector.php create mode 100644 app/Libraries/AuditLogger.php create mode 100644 app/Libraries/ChartRenderer.php create mode 100644 app/Libraries/ConnectionFactory.php create mode 100644 app/Libraries/Connectors/CsvConnector.php create mode 100644 app/Libraries/Connectors/MongoDBConnector.php create mode 100644 app/Libraries/Connectors/MySQLConnector.php create mode 100644 app/Libraries/Connectors/PostgreSQLConnector.php create mode 100644 app/Libraries/Connectors/RestApiConnector.php create mode 100644 app/Libraries/Encrypter.php create mode 100644 app/Libraries/QueryBuilder.php create mode 100644 app/Libraries/QueryExecutor.php create mode 100644 app/Libraries/QueryVariableParser.php create mode 100644 app/Libraries/QueryVariableResolver.php create mode 100644 app/Libraries/SavedQueryRunner.php create mode 100644 app/Models/.gitkeep create mode 100644 app/Models/AlertHistoryModel.php create mode 100644 app/Models/AlertModel.php create mode 100644 app/Models/AuditLogModel.php create mode 100644 app/Models/ChartExportModel.php create mode 100644 app/Models/ChartModel.php create mode 100644 app/Models/DashboardModel.php create mode 100644 app/Models/DashboardWidgetModel.php create mode 100644 app/Models/DataSourceModel.php create mode 100644 app/Models/QueryCacheModel.php create mode 100644 app/Models/QueryVariableModel.php create mode 100644 app/Models/SavedQueryModel.php create mode 100644 app/Models/SettingsModel.php create mode 100644 app/Models/SharedLinkModel.php create mode 100644 app/Models/UserModel.php create mode 100644 app/Models/WorkspaceInvitationModel.php create mode 100644 app/Models/WorkspaceMemberModel.php create mode 100644 app/Models/WorkspaceModel.php create mode 100644 app/ThirdParty/.gitkeep create mode 100644 app/Views/Pagers/cb_full.php create mode 100644 app/Views/admin/settings.php create mode 100644 app/Views/admin/users/edit.php create mode 100644 app/Views/admin/users/index.php create mode 100644 app/Views/alert/create.php create mode 100644 app/Views/alert/edit.php create mode 100644 app/Views/alert/history.php create mode 100644 app/Views/alert/index.php create mode 100644 app/Views/audit/index.php create mode 100644 app/Views/auth/forgot.php create mode 100644 app/Views/auth/login.php create mode 100644 app/Views/auth/register.php create mode 100644 app/Views/auth/reset.php create mode 100644 app/Views/chart/builder.php create mode 100644 app/Views/chart/builder_scripts.php create mode 100644 app/Views/chart/index.php create mode 100644 app/Views/dashboard/_card.php create mode 100644 app/Views/dashboard/_view_scripts.php create mode 100644 app/Views/dashboard/index.php create mode 100644 app/Views/dashboard/settings.php create mode 100644 app/Views/dashboard/view.php create mode 100644 app/Views/datasource/_form.php create mode 100644 app/Views/datasource/_proof_content.php create mode 100644 app/Views/datasource/create.php create mode 100644 app/Views/datasource/edit.php create mode 100644 app/Views/datasource/index.php create mode 100644 app/Views/datasource/show.php create mode 100644 app/Views/errors/cli/error_404.php create mode 100644 app/Views/errors/cli/error_exception.php create mode 100644 app/Views/errors/cli/production.php create mode 100644 app/Views/errors/html/debug.css create mode 100644 app/Views/errors/html/debug.js create mode 100644 app/Views/errors/html/error_400.php create mode 100644 app/Views/errors/html/error_404.php create mode 100644 app/Views/errors/html/error_exception.php create mode 100644 app/Views/errors/html/production.php create mode 100644 app/Views/layouts/auth.php create mode 100644 app/Views/layouts/embed.php create mode 100644 app/Views/layouts/main.php create mode 100644 app/Views/profile/index.php create mode 100644 app/Views/query/_form.php create mode 100644 app/Views/query/_query_scripts.php create mode 100644 app/Views/query/create.php create mode 100644 app/Views/query/edit.php create mode 100644 app/Views/query/index.php create mode 100644 app/Views/query/show.php create mode 100644 app/Views/settings/notifications.php create mode 100644 app/Views/settings/workspace.php create mode 100644 app/Views/share/chart_view.php create mode 100644 app/Views/share/dashboard_view.php create mode 100644 app/Views/share/expired.php create mode 100644 app/Views/share/gone.php create mode 100644 app/Views/share/modal.php create mode 100644 app/Views/share/password.php create mode 100644 app/Views/share/revoked.php create mode 100644 app/Views/welcome_message.php create mode 100644 app/Views/workspace/create.php create mode 100644 app/Views/workspace/index.php create mode 100644 app/Views/workspace/invite.php create mode 100644 app/Views/workspace/members.php create mode 100644 app/Views/workspace/settings.php create mode 100644 app/index.html create mode 100755 builds create mode 100644 chartboard.sql create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 env create mode 100644 phpunit.xml.dist create mode 100644 preload.php create mode 100644 public/.htaccess create mode 100644 public/assets/css/chart-builder.css create mode 100644 public/assets/css/dashboard.css create mode 100644 public/assets/css/query-builder.css create mode 100644 public/assets/css/variables.css create mode 100644 public/assets/images/logo-mark.svg create mode 100644 public/assets/js/app.js create mode 100644 public/favicon.ico create mode 100644 public/index.php create mode 100644 public/robots.txt create mode 100644 public/ui-sample-html-asset/PHASE 2/login.html create mode 100644 public/ui-sample-html-asset/PHASE 2/register.html create mode 100644 public/ui-sample-html-asset/PHASE 2/reset.html create mode 100644 public/ui-sample-html-asset/PHASE 3/workspace-module.html create mode 100755 spark create mode 100644 tests/.htaccess create mode 100644 tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php create mode 100644 tests/_support/Database/Seeds/ExampleSeeder.php create mode 100644 tests/_support/Libraries/ConfigReader.php create mode 100644 tests/_support/Models/ExampleModel.php create mode 100644 tests/database/ExampleDatabaseTest.php create mode 100644 tests/index.html create mode 100644 tests/session/ExampleSessionTest.php create mode 100644 tests/unit/HealthTest.php create mode 100755 writable/.htaccess create mode 100755 writable/cache/index.html create mode 100755 writable/debugbar/index.html create mode 100755 writable/index.html create mode 100755 writable/logs/index.html create mode 100755 writable/session/index.html create mode 100755 writable/uploads/index.html diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..6444af4 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,51 @@ +## CodeIgniter 4 Project Rules + +### Project Context +You are an expert PHP developer specializing in CodeIgniter 4(latest version). This is an Chart board. + +### Coding Standards +- **Framework:** CodeIgniter 4.x +- **Architecture:** MVC (Models in `app/Models`, Controllers in `app/Controllers`, Views in `app/Views`). +- **Database:** Use CI4 Query Builder or Entities for database interactions. No raw SQL unless specified. +- **Naming:** + - Controllers: PascalCase (e.g., `UserController.php`) + - Models: PascalCase (e.g., `UserModel.php`) + - Views: snake_case (e.g., `add_user.php`) + +### Best Practices +- **Routes:** Always check `app/Config/Routes.php` before suggesting new URLs. Use named routes where possible. +- **Validation:** Use CI4's built-in validation service in Controllers or Model `$validationRules`. +- **Security:** Always use `csrf_field()` in forms and ensure data is escaped using the Query Builder. +- **Entities:** Prefer using Entities (`app/Entities`) for mapping database rows to objects to keep business logic out of Models. + +### Common Commands +- When asked to create a file, suggest using PHP Spark: `php spark make:controller Name`, `php spark make:model Name`, `php spark make:migration Name`. + +### envirnment file +- keep base_url , database credentials , jwt secret , other secret ... inside .env file + +### view +- dont't rewrite header , footer code in every view file keep that seperatly . +- user base_url from .env file +- Refer the UI wire frame files inside (/public/ui-sample-html-asset/) folder + +### SECURITY focus +1 SQL Injection +2 HTML XSS script iframe injuction +3 csrf +4 Stored Cross Site Scripting (XSS) +5 Role based Access Control +6 Improper Input Validation +7 Handle OPTIONS Method +8 Basic CI4 validation for all form +9 Input sanitizer (get,post,put) + + +### DB connectivity details +database.default.hostname = localhost +database.default.database = chartboard +database.default.username = root +database.default.password = +database.default.DBDriver = MySQLi +# database.sql file attached here in same location + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87e86b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,126 @@ +#------------------------- +# Operating Specific Junk Files +#------------------------- + +# OS X +.DS_Store +.AppleDouble +.LSOverride + +# OS X Thumbnails +._* + +# Windows image file caches +Thumbs.db +ehthumbs.db +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Linux +*~ + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +#------------------------- +# Environment Files +#------------------------- +# These should never be under version control, +# as it poses a security risk. +.env +.vagrant +Vagrantfile + +#------------------------- +# Temporary Files +#------------------------- +writable/cache/* +!writable/cache/index.html + +writable/logs/* +!writable/logs/index.html + +writable/session/* +!writable/session/index.html + +writable/uploads/* +!writable/uploads/index.html + +writable/debugbar/* +!writable/debugbar/index.html + +php_errors.log + +#------------------------- +# User Guide Temp Files +#------------------------- +user_guide_src/build/* +user_guide_src/cilexer/build/* +user_guide_src/cilexer/dist/* +user_guide_src/cilexer/pycilexer.egg-info/* + +#------------------------- +# Test Files +#------------------------- +tests/coverage* + +# Don't save phpunit under version control. +phpunit + +#------------------------- +# Composer +#------------------------- +vendor/ + +#------------------------- +# IDE / Development Files +#------------------------- + +# Modules Testing +_modules/* + +# phpenv local config +.php-version + +# Jetbrains editors (PHPStorm, etc) +.idea/ +*.iml + +# NetBeans +/nbproject/ +/build/ +/nbbuild/ +/dist/ +/nbdist/ +/nbactions.xml +/nb-configuration.xml +/.nb-gradle/ + +# Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +*.sublime-workspace +*.sublime-project +.phpintel +/api/ + +# Visual Studio Code +.vscode/ + +/results/ +/phpunit*.xml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..24728f6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2019 British Columbia Institute of Technology +Copyright (c) 2019-present CodeIgniter Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..13c6acf --- /dev/null +++ b/README.md @@ -0,0 +1,541 @@ +# πŸ“Š Chart-Board + +> A self-hosted, open-source data visualization and dashboard platform built with CodeIgniter 4 (CI4). Connect your databases and APIs, build stunning dashboards, and share insights with your team β€” all from a clean, modern interface. + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Features](#features) +3. [Tech Stack](#tech-stack) +4. [System Requirements](#system-requirements) +5. [Installation](#installation) +6. [Configuration](#configuration) +7. [Project Structure](#project-structure) +8. [UI typography (shell alignment)](#ui-typography-shell-alignment) +9. [Modules & Functionality](#modules--functionality) + - [Authentication & User Management](#1-authentication--user-management) + - [Workspace Management](#2-workspace-management) + - [Data Source Connections](#3-data-source-connections) + - [Query Builder](#4-query-builder) + - [Chart Builder](#5-chart-builder) + - [Dashboard Builder](#6-dashboard-builder) + - [API Data Source Support](#7-api-data-source-support) + - [Alerts & Notifications](#8-alerts--notifications) + - [Sharing & Embedding](#9-sharing--embedding) + - [Audit Logs](#10-audit-logs) +10. [Supported Chart Types](#supported-chart-types) +11. [Supported Data Sources](#supported-data-sources) +12. [REST API Reference](#rest-api-reference) +13. [Roles & Permissions](#roles--permissions) +14. [Security Considerations](#security-considerations) +15. [Roadmap](#roadmap) +16. [Contributing](#contributing) +17. [License](#license) + +--- + +## Overview + +**Chart-Board** is a lightweight, powerful, and extensible analytics dashboard platform. It is designed for developers, analysts, and teams who need to visualize data from multiple sources β€” databases, REST APIs, CSV files β€” without writing front-end code from scratch. + +Inspired by tools like Metabase and Chartbrew, Chart-Board is tailored for teams who prefer a self-hosted solution with full control over their data and infrastructure, built on the familiar and lightweight **CodeIgniter 4** PHP framework. + +--- + +## Features + +- πŸ”Œ **Multi-source Connections** β€” MySQL, PostgreSQL, MongoDB, REST APIs, CSV uploads +- 🧱 **Drag-and-Drop Dashboard Builder** β€” Arrange, resize, and compose charts freely +- πŸ“ˆ **Rich Chart Types** β€” Bar, Line, Pie, Donut, Area, Scatter, Table, KPI Cards, Funnel, Gauge +- πŸ” **Visual Query Builder** β€” Build queries without SQL knowledge; raw SQL mode also supported +- πŸ‘₯ **Team Workspaces** β€” Organize dashboards by team or project +- πŸ” **Role-Based Access Control** β€” Admin, Editor, Viewer roles with fine-grained permissions +- πŸ”” **Alerts & Notifications** β€” Threshold-based alerts via Email and Slack Webhook +- πŸ”— **Public Sharing & Embedding** β€” Share dashboards via public URL or embed via iframe +- πŸ• **Scheduled Refresh** β€” Auto-refresh dashboards at set intervals +- πŸ“‹ **Audit Logs** β€” Track all user actions across workspaces +- πŸŒ™ **Dark Mode** β€” Built-in dark/light theme toggle +- πŸ“€ **Export** β€” Export charts as PNG/SVG; export data as CSV/Excel + +--- + +## Tech Stack + +| Layer | Technology | +|--------------|--------------------------------------| +| Backend | PHP 8.1+, CodeIgniter 4 | +| Frontend | HTML5, CSS3, Bootstrap 5, Alpine.js | +| Charts | ApexCharts.js / Chart.js | +| Database | MySQL 8.0+ | +| Query Engine | Custom CI4 Query Builder + Raw SQL | +| Auth | CI4 Session-based + API Token Auth | +| Cache | CI4 File/Redis Cache | +| Scheduler | CI4 Task Scheduler (Cron) | +| Storage | Local / AWS S3 (for exports/uploads) | + +--- + +## System Requirements + +- PHP >= 8.1 with extensions: `intl`, `mbstring`, `curl`, `pdo`, `pdo_mysql`, `json`, `xml` +- MySQL >= 8.0 +- Composer >= 2.x +- Node.js >= 18.x (for front-end asset compilation, optional) +- Web Server: Apache 2.4+ or Nginx 1.18+ +- Minimum 512MB RAM (1GB+ recommended for production) +- Disk Space: 500MB minimum + +--- + +## Installation + +```bash +# 1. Clone the repository +git clone https://github.com/yourorg/chart-board.git +cd chart-board + +# 2. Install PHP dependencies +composer install + +# 3. Copy and configure environment file +cp env .env +# Edit .env with your database credentials and app settings + +# 4. Run database migrations +php spark migrate + +# 5. Seed initial data (admin user, default roles) +php spark db:seed InitialSeeder + +# 6. (Optional) Install front-end dependencies +npm install && npm run build + +# 7. Set folder permissions +chmod -R 777 writable/ + +# 8. Start the development server +php spark serve +``` + +Visit `http://localhost:8080` to access Chart-Board. + +Default admin credentials (change after first login): +- **Email:** `admin@chartboard.local` +- **Password:** `Admin@1234` + +--- + +## Configuration + +Key settings in `.env`: + +```ini +# Application +app.baseURL = 'http://localhost:8080' +app.name = 'Chart-Board' + +# Database +database.default.hostname = localhost +database.default.database = chartboard +database.default.username = root +database.default.password = secret +database.default.DBDriver = MySQLi + +# Cache (file or redis) +cache.handler = file + +# Mail (for alerts) +email.fromEmail = noreply@chartboard.local +email.fromName = Chart-Board +email.SMTPHost = smtp.mailtrap.io +email.SMTPPort = 587 + +# Encryption key (generate with: php spark key:generate) +encryption.key = hex2bin:your_generated_key_here +``` + +--- + +## Project Structure + +``` +chart-board/ +β”œβ”€β”€ app/ +β”‚ β”œβ”€β”€ Config/ # CI4 configuration files +β”‚ β”œβ”€β”€ Controllers/ +β”‚ β”‚ β”œβ”€β”€ Auth/ # Login, Register, Password Reset +β”‚ β”‚ β”œβ”€β”€ Api/ # REST API controllers +β”‚ β”‚ β”œβ”€β”€ Dashboard/ # Dashboard CRUD +β”‚ β”‚ β”œβ”€β”€ Chart/ # Chart CRUD +β”‚ β”‚ β”œβ”€β”€ DataSource/ # Connection management +β”‚ β”‚ β”œβ”€β”€ Query/ # Query builder & executor +β”‚ β”‚ β”œβ”€β”€ Workspace/ # Workspace management +β”‚ β”‚ └── Admin/ # Admin panel +β”‚ β”œβ”€β”€ Models/ # CI4 Model classes +β”‚ β”œβ”€β”€ Views/ +β”‚ β”‚ β”œβ”€β”€ auth/ +β”‚ β”‚ β”œβ”€β”€ dashboard/ +β”‚ β”‚ β”œβ”€β”€ chart/ +β”‚ β”‚ β”œβ”€β”€ datasource/ +β”‚ β”‚ β”œβ”€β”€ workspace/ +β”‚ β”‚ └── layouts/ +β”‚ β”œβ”€β”€ Libraries/ +β”‚ β”‚ β”œβ”€β”€ QueryEngine.php # Runs queries against data sources +β”‚ β”‚ β”œβ”€β”€ ChartRenderer.php # Formats data for chart types +β”‚ β”‚ β”œβ”€β”€ ApiConnector.php # REST API data source handler +β”‚ β”‚ └── AlertEngine.php # Threshold checking & notifications +β”‚ β”œβ”€β”€ Filters/ # Auth, RBAC, API token filters +β”‚ └── Database/ +β”‚ β”œβ”€β”€ Migrations/ # All migration files +β”‚ └── Seeds/ # Seeders +β”œβ”€β”€ public/ # Web root +β”‚ β”œβ”€β”€ assets/ +β”‚ β”‚ β”œβ”€β”€ css/ +β”‚ β”‚ β”œβ”€β”€ js/ +β”‚ β”‚ └── images/ +β”œβ”€β”€ writable/ # Logs, cache, uploads +β”œβ”€β”€ tests/ # PHPUnit tests +β”œβ”€β”€ .env +β”œβ”€β”€ composer.json +└── README.md +``` + +--- + +## UI typography (shell alignment) + +The main chrome uses a compact type scale so navigation stays scannable: + +| Token | Variable | Size | Where it appears | +|-------|-----------|------|------------------| +| Top navigation links | `--cb-font-topnav` | **14px** | Header main menu (`.cb-top-main-link`) | +| Sidebar & dense UI | `--cb-font-sidebar` | **13px** | Left menu (`.cb-nav-link`), default body text in main content | + +These variables live in `public/assets/css/variables.css`. **All authenticated screens** wrapped in `
` (workspaces, data sources, queries list/view, dashboard, profile, admin users, etc.) inherit this scale: page body at **13px**, full-size inputs/buttons and subsection titles (`h2.h5`, `.h6`) at **14px**, matching the shell. Login/register/forgot-password use `.cb-auth-body` on the auth card for the same rules. + +The query builder adds `public/assets/css/query-builder.css` under `.qb-root` (max-width + preview/SQL-specific styles only; base type comes from `.cb-page`). + +Page titles use Bootstrap’s `h1` + `.h4` pattern with **font-weight 700** (not `display-*` sizes). + +--- + +## Modules & Functionality + +### 1. Authentication & User Management + +- Registration with email verification +- Login with session-based authentication +- Password reset via email token +- API Token generation for programmatic access +- Profile management (name, avatar, password change) +- Super Admin panel to manage all users + +### 2. Workspace Management + +A **Workspace** is the top-level organizational unit (similar to a team or project). + +- Create and manage multiple workspaces +- Invite users to a workspace with a specific role (Admin / Editor / Viewer) +- Each workspace has its own data sources, charts, and dashboards +- Workspace-level settings: name, logo, timezone, default refresh interval + +### 3. Data Source Connections + +Connect to external data sources. All credentials are encrypted at rest. + +**Supported types:** + +| Type | Details | +|--------------|--------------------------------------| +| MySQL | Host, Port, DB name, User, Password | +| PostgreSQL | Host, Port, DB name, User, Password | +| MongoDB | Connection URI | +| REST API | URL, Method, Headers, Auth (Bearer / Basic / API Key) | +| CSV Upload | Upload CSV files as static datasets | + +- Test connection before saving +- Multiple data sources per workspace +- Connection health status indicator + +### 4. Query Builder + +Two modes for fetching data from a data source: + +**Visual Builder (No-Code):** +- Select table β†’ choose columns β†’ apply filters β†’ set grouping & sorting +- Filter conditions: equals, not equals, contains, greater than, less than, between, is null +- Aggregate functions: COUNT, SUM, AVG, MIN, MAX +- Preview results in a table before saving + +**Raw SQL Mode:** +- Write custom SQL with syntax highlighting (CodeMirror) +- Schema browser sidebar (tables & columns) +- Query result preview (limited to 100 rows) +- Save queries for reuse + +**For API Sources:** +- Define endpoint URL (supports dynamic date variables like `{{today}}`, `{{last_30_days}}`) +- Map JSON response path to extract data array +- Define field aliases for chart use + +### Query Variables + +Query Variables allow chart creators to define named placeholders inside SQL or API queries that end users can fill in at dashboard view time β€” without touching the query itself. + +**Syntax:** Use double curly braces `{{ variable_name }}` anywhere in a SQL query. +```sql +SELECT DATE(created_at) AS date, SUM(amount) AS revenue +FROM orders +WHERE + created_at BETWEEN '{{ start_date }}' AND '{{ end_date }}' + AND status = '{{ order_status }}' + AND region = '{{ region }}' +GROUP BY DATE(created_at) +``` + +**Variable Types:** + +| Type | Input Widget | Example | +|------|-------------|---------| +| `text` | Free text input | customer name, SKU | +| `number` | Number input | threshold, limit | +| `date` | Date picker | start date, end date | +| `date_range` | Dual date picker | from β†’ to | +| `select` | Dropdown (options defined by creator) | status, region, category | +| `multi_select` | Multi-select dropdown | multiple statuses | + +**How it works:** + +1. Creator writes query with `{{ variable_name }}` placeholders +2. Chart-Board auto-detects all variables and prompts the creator to configure each one (type, label, default value, options for dropdowns) +3. Variable config is saved in `charts.display_config` as JSON +4. On dashboard view, users see a filter widget per variable above the chart +5. On change, Chart-Board re-runs the query with substituted safe-parameterized values (never raw string injection) + +**Built-in System Variables (no config needed):** + +| Variable | Resolves To | +|----------|------------| +| `{{ today }}` | Current date `YYYY-MM-DD` | +| `{{ now }}` | Current datetime `YYYY-MM-DD HH:MM:SS` | +| `{{ yesterday }}` | Yesterday's date | +| `{{ last_7_days_start }}` | 7 days ago | +| `{{ last_30_days_start }}` | 30 days ago | +| `{{ last_90_days_start }}` | 90 days ago | +| `{{ this_month_start }}` | First day of current month | +| `{{ this_year_start }}` | First day of current year | +| `{{ current_user_id }}` | Logged-in user's ID | +| `{{ current_workspace_id }}` | Active workspace ID | + +**Security:** All user-supplied variable values are passed as PDO prepared statement bindings β€” never interpolated directly into SQL strings. + +### 5. Chart Builder + +Build charts from a saved query or direct query. + +**Steps:** +1. Select Data Source + enter/select query +2. Choose Chart Type +3. Map X-axis, Y-axis, group-by fields +4. Configure labels, colors, legend, tooltips +5. Set refresh interval (manual / 1 min / 5 min / 15 min / 1 hour / 1 day) +6. Save chart with a name and description + +**Chart Settings:** +- Title & subtitle +- Color palette (preset or custom) +- Show/hide legend +- Show/hide data labels +- Custom Y-axis min/max +- Number formatting (currency, percentage, decimal places) +- Date format for time-series X-axis + +### 6. Dashboard Builder + +A Dashboard is a collection of charts arranged in a grid layout. + +- Drag-and-drop chart placement +- Resize charts (1Γ—1 to 4Γ—2 grid units) +- Add text/markdown widgets (for annotations and section titles) +- Add image widgets (logo, banner) +- Add filter widgets (date range picker, dropdown) that apply across all charts +- Dashboard-level refresh interval +- Fullscreen mode + +### 7. API Data Source Support + +A key differentiator β€” pull data directly from REST APIs: + +- GET / POST support +- Auth modes: None, Bearer Token, Basic Auth, API Key (header or query param) +- Static or dynamic headers +- JSON path extraction (`data.results[*].value`) +- Pagination support (offset/cursor-based) +- Schedule automatic sync (cache API response for N minutes) +- Supports webhook-push mode (Chart-Board provides a unique URL to receive data) + +### 8. Alerts & Notifications + +Define threshold-based alerts on any chart metric. + +- UI: **Alerts** in the top nav (`/alert`) β€” create, edit, mute (1h / 4h / 24h), history, delete +- Conditions: `gt`, `lt`, `eq`, `gte`, `lte` on the first row of the chart’s query (numeric column) +- Check interval: per-alert minutes (`check_interval`); cron runs `php spark alert:check` (intended every minute) +- Channels: Email (comma-separated addresses) and Slack incoming webhook +- Duplicate notifications suppressed if the same alert fired within the last 5 minutes +- History stored in `alert_history`; recent triggers shown on the alerts index; badge on **Alerts** for sends in the last hour + +### 9. Sharing & Embedding + +- **Public link** β€” `GET /share/{token}` (no auth). Optional password (`POST /share/{token}/unlock`) and expiry. Records in `shared_links`; view count incremented once per session per link. +- **Create / revoke** β€” Authenticated: `POST /sharing/generate`, `POST /sharing/revoke/{id}`, list: `GET /sharing/links?type=dashboard|chart&resource_id=…`. Share modal on dashboard view, chart list, and chart editor (QR via external API in modal). +- **Embed** β€” `?embed=1` on the same URL; response drops `X-Frame-Options` and sets CSP `frame-ancestors *`. Embed tab in the share modal with size presets and iframe HTML. +- **Chart data for public dashboards** β€” `POST /share/{token}/chart/{id}/data` (same variable JSON as authenticated charts; no CSRF when token is valid). +- **Export** β€” Chart editor: PNG (Apex `dataURI` + FileSaver.js, client-side). Server: `GET /chart/{id}/export?format=csv|excel` (PhpSpreadsheet for `.xlsx`); logged in `chart_exports` for CSV/Excel. + +### 10. Audit Logs + +Track all significant user actions: + +- User login / logout +- Data source created / updated / deleted +- Chart created / updated / deleted +- Dashboard shared / unshared +- User role changes +- Alert triggered + +Logs are viewable by workspace admins and filterable by user, action type, and date range. + +--- + +## Supported Chart Types + +| Chart Type | Description | +|---------------|--------------------------------------------------| +| Bar Chart | Vertical or horizontal bar comparison | +| Line Chart | Trend over time | +| Area Chart | Filled line chart for volume trends | +| Pie Chart | Proportional distribution | +| Donut Chart | Pie with center label (good for KPIs) | +| Scatter Plot | Correlation between two numeric variables | +| Data Table | Paginated tabular display of query results | +| KPI Card | Single metric with optional trend indicator | +| Funnel Chart | Step-by-step conversion visualization | +| Gauge Chart | Progress toward a goal (0–100%) | +| Heatmap | Matrix of values colored by intensity | +| Combo Chart | Bar + Line overlaid on same axes | + +--- + +## Supported Data Sources + +| Source | Connection Type | Status | +|-------------|---------------------|----------| +| MySQL | Direct TCP | βœ… v1.0 | +| PostgreSQL | Direct TCP | βœ… v1.0 | +| MongoDB | URI | βœ… v1.0 | +| REST API | HTTP/HTTPS | βœ… v1.0 | +| CSV Upload | File Upload | βœ… v1.0 | +| SQLite | File Path | πŸ”œ v1.1 | +| BigQuery | OAuth / Service Key | πŸ”œ v1.2 | +| Snowflake | JDBC-style DSN | πŸ”œ v1.2 | +| Google Sheets | OAuth | πŸ”œ v1.2 | + +--- + +## REST API Reference + +Chart-Board exposes a REST API for programmatic access (authenticated via API token). + +**Base URL:** `https://yourdomain.com/api/v1` + +**Authentication Header:** +``` +Authorization: Bearer +``` + +| Method | Endpoint | Description | +|--------|--------------------------------------|-------------------------------| +| GET | `/workspaces` | List all workspaces | +| POST | `/workspaces` | Create a workspace | +| GET | `/workspaces/{id}/datasources` | List data sources | +| POST | `/workspaces/{id}/datasources` | Add a data source | +| GET | `/workspaces/{id}/charts` | List charts | +| POST | `/workspaces/{id}/charts` | Create a chart | +| GET | `/workspaces/{id}/dashboards` | List dashboards | +| POST | `/workspaces/{id}/dashboards` | Create a dashboard | +| GET | `/charts/{id}/data` | Fetch chart data (JSON) | +| POST | `/datasources/{id}/query` | Run a query | +| GET | `/alerts` | List alerts | + +--- + +## Roles & Permissions + +| Permission | Super Admin | Workspace Admin | Editor | Viewer | +|-------------------------|:-----------:|:---------------:|:------:|:------:| +| Manage users (global) | βœ… | ❌ | ❌ | ❌ | +| Manage workspaces | βœ… | βœ… | ❌ | ❌ | +| Invite members | βœ… | βœ… | ❌ | ❌ | +| Manage data sources | βœ… | βœ… | βœ… | ❌ | +| Create/edit charts | βœ… | βœ… | βœ… | ❌ | +| Create/edit dashboards | βœ… | βœ… | βœ… | ❌ | +| View dashboards | βœ… | βœ… | βœ… | βœ… | +| Share dashboards | βœ… | βœ… | βœ… | ❌ | +| Manage alerts | βœ… | βœ… | βœ… | ❌ | +| View audit logs | βœ… | βœ… | ❌ | ❌ | + +--- + +## Security Considerations + +- All data source credentials are encrypted using CI4's Encryption library (AES-256) +- SQL queries run in read-only mode on connected databases (use a dedicated read-only DB user) +- CSRF protection enabled on all forms +- XSS filtering on all user inputs +- Rate limiting on API endpoints (60 req/min per token) +- Public dashboard links support optional password and expiry +- HTTP-only, SameSite=Strict session cookies +- Content Security Policy (CSP) headers configured + +--- + +## Roadmap + +| Version | Planned Features | +|---------|-------------------------------------------------------------------------| +| v1.0 | Core dashboard, MySQL/PostgreSQL/REST API sources, basic chart types | +| v1.1 | SQLite support, improved mobile view, CSV export, chart comments | +| v1.2 | BigQuery, Snowflake, Google Sheets connectors | +| v1.3 | AI-powered natural language query ("Show me sales last month") | +| v1.4 | White-label mode, multi-tenant SaaS mode | +| v2.0 | Real-time streaming data (WebSocket), collaborative dashboard editing | + +--- + +## Contributing + +Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +```bash +# Run tests +php spark test + +# Check code style +vendor/bin/phpcs --standard=PSR12 app/ + +# Fix code style +vendor/bin/phpcbf --standard=PSR12 app/ +``` + +--- + +## License + +Chart-Board is open-source software licensed under the [MIT License](LICENSE). + +--- + +*Built with ❀️ using CodeIgniter 4* diff --git a/TASKS.md b/TASKS.md new file mode 100644 index 0000000..cb38a82 --- /dev/null +++ b/TASKS.md @@ -0,0 +1,621 @@ +# πŸ“‹ 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 `'; + const ta = document.getElementById('cbEmbedCode'); + if (ta) ta.value = code; + } + + async function loadLinks() { + const type = document.getElementById('cbShareType')?.value || 'chart'; + const rid = document.getElementById('cbShareResourceId')?.value || ''; + const wrap = document.getElementById('cbShareLinksWrap'); + const err = document.getElementById('cbShareErr'); + if (!wrap || !rid) return; + err.classList.add('d-none'); + wrap.innerHTML = '

Loading…

'; + try { + const r = await fetch(urlList + '?type=' + encodeURIComponent(type) + '&resource_id=' + encodeURIComponent(rid), { + credentials: 'same-origin', + headers: { 'X-Requested-With': 'XMLHttpRequest' }, + }); + const j = await r.json(); + if (!j.success || !j.links) { + wrap.innerHTML = '

No links yet.

'; + return; + } + if (j.links.length === 0) { + wrap.innerHTML = '

No active links. Generate one above.

'; + setEmbedSnippet(''); + return; + } + let html = '
    '; + j.links.forEach((L) => { + const qr = 'https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=' + encodeURIComponent(L.url); + html += '
  • ' + + '
    ' + + '
    ' + + '
    ' + + '' + + '
    ' + + '
    Views: ' + (L.view_count ?? 0) + + (L.expires_at ? ' Β· Expires: ' + L.expires_at : '') + + (L.has_password ? ' Β· Password protected' : '') + '
    ' + + '
    ' + + '
    ' + + '
    QR code
  • '; + }); + html += '
'; + wrap.innerHTML = html; + setEmbedSnippet(j.links[0].url); + wrap.querySelectorAll('.cb-copy-url').forEach((btn) => { + btn.addEventListener('click', () => { + const inp = btn.closest('.input-group')?.querySelector('input'); + if (!inp) return; + inp.select(); + document.execCommand('copy'); + }); + }); + wrap.querySelectorAll('.cb-revoke-form').forEach((f) => { + f.addEventListener('submit', async (ev) => { + ev.preventDefault(); + const id = f.getAttribute('data-id'); + const csrf = getCsrf(); + const body = new URLSearchParams(); + body.append(csrf.name, csrf.hash); + const res = await fetch(urlRevoke + id, { + method: 'POST', + credentials: 'same-origin', + headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' }, + body, + }); + const jj = await res.json(); + if (jj.csrf) { + modal.setAttribute('data-csrf-name', jj.csrf.name); + const hi = modal.querySelector('.cb-share-csrf'); + if (hi) { hi.name = jj.csrf.name; hi.value = jj.csrf.hash; } + } + loadLinks(); + }); + }); + } catch (e) { + wrap.innerHTML = '

Could not load links.

'; + } + } + + modal.addEventListener('show.bs.modal', (ev) => { + const t = ev.relatedTarget; + const type = t?.getAttribute('data-share-type') || 'chart'; + const rid = t?.getAttribute('data-resource-id') || ''; + const st = document.getElementById('cbShareType'); + const sr = document.getElementById('cbShareResourceId'); + if (st) st.value = type; + if (sr) sr.value = rid; + loadLinks(); + }); + + document.getElementById('cbShareGenerateForm')?.addEventListener('submit', async (ev) => { + ev.preventDefault(); + const err = document.getElementById('cbShareErr'); + err.classList.add('d-none'); + const form = ev.target; + const fd = new FormData(form); + try { + const r = await fetch(urlGen, { + method: 'POST', + credentials: 'same-origin', + headers: { 'X-Requested-With': 'XMLHttpRequest' }, + body: new URLSearchParams(fd), + }); + const j = await r.json(); + if (j.csrf) { + const hi = modal.querySelector('.cb-share-csrf'); + if (hi) { hi.name = j.csrf.name; hi.value = j.csrf.hash; } + } + if (!j.success) { + err.textContent = j.message || (j.errors ? Object.values(j.errors).flat().join(' ') : 'Failed'); + err.classList.remove('d-none'); + return; + } + if (j.csrf) { + const hi = modal.querySelector('.cb-share-csrf'); + if (hi) { hi.name = j.csrf.name; hi.value = j.csrf.hash; } + modal.setAttribute('data-csrf-name', j.csrf.name); + } + form.querySelector('[name="password"]').value = ''; + form.querySelector('[name="expires_at"]').value = ''; + loadLinks(); + } catch (e) { + err.textContent = 'Request failed.'; + err.classList.remove('d-none'); + } + }); + + document.getElementById('cbEmbedPreset')?.addEventListener('change', () => { + const c = document.getElementById('cbEmbedCustomDims'); + if (c) c.classList.toggle('d-none', document.getElementById('cbEmbedPreset').value !== 'custom'); + const first = document.querySelector('#cbShareLinksWrap input[readonly]'); + setEmbedSnippet(first ? first.value : ''); + }); + document.getElementById('cbEmbedW')?.addEventListener('input', () => { + const first = document.querySelector('#cbShareLinksWrap input[readonly]'); + setEmbedSnippet(first ? first.value : ''); + }); + document.getElementById('cbEmbedH')?.addEventListener('input', () => { + const first = document.querySelector('#cbShareLinksWrap input[readonly]'); + setEmbedSnippet(first ? first.value : ''); + }); + + document.getElementById('cbEmbedCopy')?.addEventListener('click', () => { + const ta = document.getElementById('cbEmbedCode'); + if (!ta) return; + ta.select(); + document.execCommand('copy'); + }); +})(); + diff --git a/app/Views/share/password.php b/app/Views/share/password.php new file mode 100644 index 0000000..ece9dfe --- /dev/null +++ b/app/Views/share/password.php @@ -0,0 +1,18 @@ +extend('layouts/embed') ?> +section('content') ?> +
+

Password required

+ getFlashdata('error')): ?> +
getFlashdata('error')) ?>
+ +
+ + +
+ + +
+ +
+
+endSection() ?> diff --git a/app/Views/share/revoked.php b/app/Views/share/revoked.php new file mode 100644 index 0000000..e67ac8f --- /dev/null +++ b/app/Views/share/revoked.php @@ -0,0 +1,7 @@ +extend('layouts/embed') ?> +section('content') ?> +
+

Link revoked

+

The owner has turned off this share link.

+
+endSection() ?> diff --git a/app/Views/welcome_message.php b/app/Views/welcome_message.php new file mode 100644 index 0000000..c18eca3 --- /dev/null +++ b/app/Views/welcome_message.php @@ -0,0 +1,331 @@ + + + + + Welcome to CodeIgniter 4! + + + + + + + + + + + +
+ + + +
+ +

Welcome to CodeIgniter

+ +

The small framework with powerful features

+ +
+ +
+ + + +
+ +

About this page

+ +

The page you are looking at is being generated dynamically by CodeIgniter.

+ +

If you would like to edit this page you will find it located at:

+ +
app/Views/welcome_message.php
+ +

The corresponding controller for this page can be found at:

+ +
app/Controllers/Home.php
+ +
+ +
+ +
+ +

Go further

+ +

+ + Learn +

+ +

The User Guide contains an introduction, tutorial, a number of "how to" + guides, and then reference documentation for the components that make up + the framework. Check the User Guide !

+ +

+ + Discuss +

+ +

CodeIgniter is a community-developed open source project, with several + venues for the community members to gather and exchange ideas. View all + the threads on CodeIgniter's forum, or chat on Slack !

+ +

+ + Contribute +

+ +

CodeIgniter is a community driven project and accepts contributions + of code and documentation from the community. Why not + + join us ?

+ +
+ +
+ + + +
+
+ +

Page rendered in {elapsed_time} seconds using {memory_usage} MB of memory.

+ +

Environment:

+ +
+ +
+ +

© CodeIgniter Foundation. CodeIgniter is open source project released under the MIT + open source licence.

+ +
+ +
+ + + + + + + + + diff --git a/app/Views/workspace/create.php b/app/Views/workspace/create.php new file mode 100644 index 0000000..b852053 --- /dev/null +++ b/app/Views/workspace/create.php @@ -0,0 +1,72 @@ +extend('layouts/main') ?> + +section('content') ?> +
+
+

Create Workspace

+

Set up a new isolated data environment for your team.

+
+ Cancel +
+ +
+ getFlashdata('errors')): ?> +
+
    + getFlashdata('errors') as $error): ?> +
  • + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + Cancel +
+
+
+
+
+
+endSection() ?> diff --git a/app/Views/workspace/index.php b/app/Views/workspace/index.php new file mode 100644 index 0000000..03426ab --- /dev/null +++ b/app/Views/workspace/index.php @@ -0,0 +1,66 @@ +extend('layouts/main') ?> + +section('content') ?> +
+
+

Workspaces

+

Organize your data assets, dashboards, and team access across isolated workspace environments.

+
+ +  New Workspace +
+ +
+getFlashdata('success')): ?> +
getFlashdata('success')) ?>
+ +getFlashdata('error')): ?> +
getFlashdata('error')) ?>
+ + +
+
Total Workspaces
+
Total Members
+
Data Sources
+
Dashboards
+
+ +
+
+ search + +
+ + + +
+ +
+ +
+
+
+
+
+ account_tree +
+ + + +
+

+
chartboard.io/
+

+ +
+
+ +
+
+endSection() ?> diff --git a/app/Views/workspace/invite.php b/app/Views/workspace/invite.php new file mode 100644 index 0000000..ea6899e --- /dev/null +++ b/app/Views/workspace/invite.php @@ -0,0 +1,89 @@ +extend('layouts/main') ?> + +section('content') ?> +
+
+

Workspace Invitations

+

Send invites and manage pending links.

+
+ Back to Members +
+ +
+getFlashdata('success')): ?> +
getFlashdata('success')) ?>
+ +getFlashdata('error')): ?> +
getFlashdata('error')) ?>
+ + +
+

Send Invitation

+
+ +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+
+

Pending Invitations

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
EmailRoleExpiresActions
No pending invites.
+
+
+ + +
+
+ + +
+
+
+
+
+
+endSection() ?> diff --git a/app/Views/workspace/members.php b/app/Views/workspace/members.php new file mode 100644 index 0000000..7c4723c --- /dev/null +++ b/app/Views/workspace/members.php @@ -0,0 +1,73 @@ +extend('layouts/main') ?> + +section('content') ?> +
+
+

Workspace Members

+

Roles and access for people in this workspace.

+
+ +
+ +
+getFlashdata('success')): ?> +
getFlashdata('success')) ?>
+ +getFlashdata('error')): ?> +
getFlashdata('error')) ?>
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameEmailRoleActions
No members yet. Invite someone to collaborate.
+
+
+
+ + + +
+
+
+
+ + +
+
+
+
+
+
+endSection() ?> diff --git a/app/Views/workspace/settings.php b/app/Views/workspace/settings.php new file mode 100644 index 0000000..c60d6ca --- /dev/null +++ b/app/Views/workspace/settings.php @@ -0,0 +1,92 @@ +extend('layouts/main') ?> + +section('content') ?> +
+
+

Workspace Settings

+

Name, slug, defaults, and status for this workspace.

+
+ Back +
+ +
+ getFlashdata('success')): ?> +
getFlashdata('success')) ?>
+ + getFlashdata('error')): ?> +
getFlashdata('error')) ?>
+ + getFlashdata('errors')): ?> +
+
    + getFlashdata('errors') as $error): ?> +
  • + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + + +
+
+
+ + Manage Members + Invitations +
+
+
+
+
+ +
+

Danger Zone

+

Only the workspace owner can delete this workspace.

+
+ + +
+
+
+endSection() ?> diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..69df4e1 --- /dev/null +++ b/app/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/builds b/builds new file mode 100755 index 0000000..cc2ca08 --- /dev/null +++ b/builds @@ -0,0 +1,125 @@ +#!/usr/bin/env php + 'vcs', + 'url' => GITHUB_URL, + ]; + } + + $array['require']['codeigniter4/codeigniter4'] = 'dev-develop'; + unset($array['require']['codeigniter4/framework']); + } else { + unset($array['minimum-stability']); + + if (isset($array['repositories'])) { + foreach ($array['repositories'] as $i => $repository) { + if ($repository['url'] === GITHUB_URL) { + unset($array['repositories'][$i]); + break; + } + } + + if (empty($array['repositories'])) { + unset($array['repositories']); + } + } + + $array['require']['codeigniter4/framework'] = LATEST_RELEASE; + unset($array['require']['codeigniter4/codeigniter4']); + } + + file_put_contents($file, json_encode($array, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL); + + $modified[] = $file; + } else { + echo 'Warning: Unable to decode composer.json! Skipping...' . PHP_EOL; + } + } else { + echo 'Warning: Unable to read composer.json! Skipping...' . PHP_EOL; + } +} + +$files = [ + __DIR__ . DIRECTORY_SEPARATOR . 'app/Config/Paths.php', + __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml.dist', + __DIR__ . DIRECTORY_SEPARATOR . 'phpunit.xml', +]; + +foreach ($files as $file) { + if (is_file($file)) { + $contents = file_get_contents($file); + + if ($dev) { + $contents = str_replace('vendor/codeigniter4/framework', 'vendor/codeigniter4/codeigniter4', $contents); + } else { + $contents = str_replace('vendor/codeigniter4/codeigniter4', 'vendor/codeigniter4/framework', $contents); + } + + file_put_contents($file, $contents); + + $modified[] = $file; + } +} + +if ($modified === []) { + echo 'No files modified.' . PHP_EOL; +} else { + echo 'The following files were modified:' . PHP_EOL; + + foreach ($modified as $file) { + echo " * {$file}" . PHP_EOL; + } + + echo 'Run `composer update` to sync changes with your vendor folder.' . PHP_EOL; +} diff --git a/chartboard.sql b/chartboard.sql new file mode 100644 index 0000000..ad88174 --- /dev/null +++ b/chartboard.sql @@ -0,0 +1,624 @@ +-- ============================================================ +-- Chart-Board β€” MySQL Database Schema +-- Version : 1.0.0 +-- Engine : InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; +SET SQL_MODE = 'NO_ENGINE_SUBSTITUTION'; + +CREATE DATABASE IF NOT EXISTS `chartboard` + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +USE `chartboard`; + + +-- ============================================================ +-- TABLE: users +-- Stores all registered user accounts +-- ============================================================ +CREATE TABLE `users` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(150) NOT NULL, + `email` VARCHAR(255) NOT NULL UNIQUE, + `password` VARCHAR(255) NOT NULL COMMENT 'bcrypt hashed', + `avatar` VARCHAR(500) NULL DEFAULT NULL COMMENT 'URL or file path', + `role` ENUM('superadmin','user') NOT NULL DEFAULT 'user', + `email_verified` TINYINT(1) NOT NULL DEFAULT 0, + `verify_token` VARCHAR(255) NULL DEFAULT NULL, + `reset_token` VARCHAR(255) NULL DEFAULT NULL, + `reset_token_expiry` DATETIME NULL DEFAULT NULL, + `api_token` VARCHAR(255) NULL DEFAULT NULL UNIQUE COMMENT 'Personal API token', + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `theme_preference` VARCHAR(20) NOT NULL DEFAULT 'light' COMMENT 'UI theme: light, dark, system', + `last_login_at` DATETIME NULL DEFAULT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` DATETIME NULL DEFAULT NULL COMMENT 'Soft delete', + PRIMARY KEY (`id`), + INDEX `idx_users_email` (`email`), + INDEX `idx_users_api_token` (`api_token`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='All registered Chart-Board users'; + + +-- ============================================================ +-- TABLE: workspaces +-- Top-level container for data sources, charts, dashboards +-- ============================================================ +CREATE TABLE `workspaces` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `name` VARCHAR(150) NOT NULL, + `slug` VARCHAR(160) NOT NULL UNIQUE COMMENT 'URL-friendly identifier', + `description` TEXT NULL DEFAULT NULL, + `logo` VARCHAR(500) NULL DEFAULT NULL, + `timezone` VARCHAR(80) NOT NULL DEFAULT 'UTC', + `default_refresh` INT UNSIGNED NOT NULL DEFAULT 300 COMMENT 'Default refresh in seconds', + `owner_id` INT UNSIGNED NOT NULL, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` DATETIME NULL DEFAULT NULL, + PRIMARY KEY (`id`), + INDEX `idx_workspaces_owner` (`owner_id`), + CONSTRAINT `fk_workspaces_owner` + FOREIGN KEY (`owner_id`) REFERENCES `users` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Workspace β€” team or project level grouping'; + + +-- ============================================================ +-- TABLE: workspace_members +-- Many-to-many: users ↔ workspaces with role +-- ============================================================ +CREATE TABLE `workspace_members` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `workspace_id` INT UNSIGNED NOT NULL, + `user_id` INT UNSIGNED NOT NULL, + `role` ENUM('admin','editor','viewer') NOT NULL DEFAULT 'viewer', + `invited_by` INT UNSIGNED NULL DEFAULT NULL, + `joined_at` DATETIME NULL DEFAULT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uq_workspace_member` (`workspace_id`, `user_id`), + CONSTRAINT `fk_wm_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_wm_user` + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_wm_invited_by` + FOREIGN KEY (`invited_by`) REFERENCES `users` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Workspace membership and roles'; + + +-- ============================================================ +-- TABLE: workspace_invitations +-- Pending email invitations to join a workspace +-- ============================================================ +CREATE TABLE `workspace_invitations` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `workspace_id` INT UNSIGNED NOT NULL, + `invited_by` INT UNSIGNED NOT NULL, + `email` VARCHAR(255) NOT NULL, + `role` ENUM('admin','editor','viewer') NOT NULL DEFAULT 'viewer', + `token` VARCHAR(255) NOT NULL UNIQUE, + `accepted` TINYINT(1) NOT NULL DEFAULT 0, + `expires_at` DATETIME NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `idx_invitations_token` (`token`), + CONSTRAINT `fk_inv_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_inv_invited_by` + FOREIGN KEY (`invited_by`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Pending workspace invitations via email'; + + +-- ============================================================ +-- TABLE: data_sources +-- Connection configurations for databases, APIs, CSV files +-- ============================================================ +CREATE TABLE `data_sources` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `workspace_id` INT UNSIGNED NOT NULL, + `name` VARCHAR(150) NOT NULL, + `type` ENUM('mysql','postgresql','mongodb','rest_api','csv') NOT NULL, + `host` VARCHAR(255) NULL DEFAULT NULL, + `port` SMALLINT UNSIGNED NULL DEFAULT NULL, + `database_name` VARCHAR(150) NULL DEFAULT NULL, + `username` VARCHAR(150) NULL DEFAULT NULL, + `password` TEXT NULL DEFAULT NULL COMMENT 'AES-256 encrypted', + `connection_uri` TEXT NULL DEFAULT NULL COMMENT 'For MongoDB or DSN-style', + `ssl_enabled` TINYINT(1) NOT NULL DEFAULT 0, + `ssl_ca` TEXT NULL DEFAULT NULL, + -- REST API fields + `api_base_url` VARCHAR(500) NULL DEFAULT NULL, + `api_method` ENUM('GET','POST') NULL DEFAULT 'GET', + `api_auth_type` ENUM('none','bearer','basic','api_key') NULL DEFAULT 'none', + `api_auth_value` TEXT NULL DEFAULT NULL COMMENT 'Encrypted token/password', + `api_headers` JSON NULL DEFAULT NULL COMMENT 'Static headers as key-value JSON', + -- CSV fields + `csv_file_path` VARCHAR(500) NULL DEFAULT NULL, + `csv_delimiter` CHAR(1) NULL DEFAULT ',', + -- Status + `last_tested_at` DATETIME NULL DEFAULT NULL, + `status` ENUM('untested','connected','failed') NOT NULL DEFAULT 'untested', + `error_message` TEXT NULL DEFAULT NULL, + `created_by` INT UNSIGNED NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` DATETIME NULL DEFAULT NULL, + PRIMARY KEY (`id`), + INDEX `idx_ds_workspace` (`workspace_id`), + CONSTRAINT `fk_ds_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_ds_created_by` + FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Data source connection configurations'; + + +-- ============================================================ +-- TABLE: saved_queries +-- Reusable queries (SQL or API-mapped) per data source +-- ============================================================ +CREATE TABLE `saved_queries` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `workspace_id` INT UNSIGNED NOT NULL, + `data_source_id` INT UNSIGNED NOT NULL, + `name` VARCHAR(150) NOT NULL, + `description` TEXT NULL DEFAULT NULL, + `query_type` ENUM('visual','raw_sql','api') NOT NULL DEFAULT 'raw_sql', + `raw_sql` TEXT NULL DEFAULT NULL, + `visual_config` JSON NULL DEFAULT NULL COMMENT 'Visual builder settings (table, filters, columns)', + `api_endpoint` VARCHAR(500) NULL DEFAULT NULL COMMENT 'Override base URL for specific query', + `api_params` JSON NULL DEFAULT NULL COMMENT 'Query params / body', + `response_path` VARCHAR(255) NULL DEFAULT NULL COMMENT 'JSON path to data array e.g. data.results', + `field_map` JSON NULL DEFAULT NULL COMMENT 'Map response fields to aliases', + `cache_ttl` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Cache result for N seconds (0 = no cache)', + `created_by` INT UNSIGNED NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` DATETIME NULL DEFAULT NULL, + PRIMARY KEY (`id`), + INDEX `idx_sq_workspace` (`workspace_id`), + INDEX `idx_sq_datasource` (`data_source_id`), + CONSTRAINT `fk_sq_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_sq_datasource` + FOREIGN KEY (`data_source_id`) REFERENCES `data_sources` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_sq_created_by` + FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Reusable saved queries for charts'; + + +-- ============================================================ +-- TABLE: query_variables +-- Variable definitions detected/configured per saved query +-- ============================================================ +CREATE TABLE `query_variables` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `saved_query_id` INT UNSIGNED NOT NULL, + `workspace_id` INT UNSIGNED NOT NULL, + `name` VARCHAR(100) NOT NULL COMMENT 'Variable token name without braces', + `label` VARCHAR(120) NULL DEFAULT NULL COMMENT 'Display label for form widgets', + `type` ENUM('text','number','date','date_range','select','multi_select') NOT NULL DEFAULT 'text', + `default_value` TEXT NULL DEFAULT NULL, + `options_json` JSON NULL DEFAULT NULL COMMENT 'Option list for select/multi_select', + `is_required` TINYINT(1) NOT NULL DEFAULT 0, + `sort_order` SMALLINT UNSIGNED NOT NULL DEFAULT 1, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uq_qv_query_name` (`saved_query_id`, `name`), + INDEX `idx_qv_workspace` (`workspace_id`), + INDEX `idx_qv_query` (`saved_query_id`), + CONSTRAINT `fk_qv_saved_query` + FOREIGN KEY (`saved_query_id`) REFERENCES `saved_queries` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_qv_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Variable definitions for parameterized saved queries'; + + +-- ============================================================ +-- TABLE: charts +-- Individual chart definitions +-- ============================================================ +CREATE TABLE `charts` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `workspace_id` INT UNSIGNED NOT NULL, + `data_source_id` INT UNSIGNED NOT NULL, + `saved_query_id` INT UNSIGNED NULL DEFAULT NULL, + `name` VARCHAR(200) NOT NULL, + `description` TEXT NULL DEFAULT NULL, + `chart_type` ENUM( + 'bar','line','area','pie','donut', + 'scatter','table','kpi_card','funnel', + 'gauge','heatmap','combo', + 'spline','stepline','radar','bubble','polar_area' + ) NOT NULL, + `query_type` ENUM('visual','raw_sql','api') NOT NULL DEFAULT 'raw_sql', + `raw_sql` TEXT NULL DEFAULT NULL, + `visual_config` JSON NULL DEFAULT NULL, + `api_endpoint` VARCHAR(500) NULL DEFAULT NULL, + `api_params` JSON NULL DEFAULT NULL, + `response_path` VARCHAR(255) NULL DEFAULT NULL, + `field_map` JSON NULL DEFAULT NULL, + -- Axis & field mapping + `x_field` VARCHAR(150) NULL DEFAULT NULL, + `y_field` VARCHAR(150) NULL DEFAULT NULL, + `group_field` VARCHAR(150) NULL DEFAULT NULL, + `value_field` VARCHAR(150) NULL DEFAULT NULL COMMENT 'Used for KPI/Gauge', + -- Display settings + `display_config` JSON NULL DEFAULT NULL COMMENT 'Colors, labels, legend, formatting options', + `refresh_interval` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Auto-refresh in seconds (0 = manual)', + `cache_ttl` INT UNSIGNED NOT NULL DEFAULT 300 COMMENT 'Cache result TTL in seconds', + `is_public` TINYINT(1) NOT NULL DEFAULT 0, + `public_token` VARCHAR(100) NULL DEFAULT NULL UNIQUE, + `created_by` INT UNSIGNED NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` DATETIME NULL DEFAULT NULL, + PRIMARY KEY (`id`), + INDEX `idx_charts_workspace` (`workspace_id`), + INDEX `idx_charts_datasource` (`data_source_id`), + INDEX `idx_charts_public` (`public_token`), + CONSTRAINT `fk_charts_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_charts_datasource` + FOREIGN KEY (`data_source_id`) REFERENCES `data_sources` (`id`) ON DELETE RESTRICT, + CONSTRAINT `fk_charts_query` + FOREIGN KEY (`saved_query_id`) REFERENCES `saved_queries` (`id`) ON DELETE SET NULL, + CONSTRAINT `fk_charts_created_by` + FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Chart definitions with query and display configuration'; + + +-- ============================================================ +-- TABLE: dashboards +-- Dashboard containers that hold multiple charts +-- ============================================================ +CREATE TABLE `dashboards` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `workspace_id` INT UNSIGNED NOT NULL, + `name` VARCHAR(200) NOT NULL, + `description` TEXT NULL DEFAULT NULL, + `slug` VARCHAR(220) NOT NULL, + `layout_config` JSON NULL DEFAULT NULL COMMENT 'Grid layout positions for all widgets', + `filters_config` JSON NULL DEFAULT NULL COMMENT 'Global filter widget definitions', + `refresh_interval` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Dashboard-level refresh (0 = manual)', + `theme` ENUM('light','dark','system') NOT NULL DEFAULT 'system', + `is_public` TINYINT(1) NOT NULL DEFAULT 0, + `public_token` VARCHAR(100) NULL DEFAULT NULL UNIQUE, + `public_password` VARCHAR(255) NULL DEFAULT NULL COMMENT 'Optional bcrypt hashed password for public link', + `public_expires_at` DATETIME NULL DEFAULT NULL, + `is_pinned` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Pinned dashboards appear first', + `created_by` INT UNSIGNED NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` DATETIME NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uq_dashboard_slug` (`workspace_id`, `slug`), + INDEX `idx_dashboards_workspace` (`workspace_id`), + INDEX `idx_dashboards_public` (`public_token`), + CONSTRAINT `fk_dashboards_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_dashboards_created_by` + FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Dashboard containers'; + + +-- ============================================================ +-- TABLE: dashboard_widgets +-- Each widget on a dashboard (chart, text, image, filter) +-- ============================================================ +CREATE TABLE `dashboard_widgets` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `dashboard_id` INT UNSIGNED NOT NULL, + `chart_id` INT UNSIGNED NULL DEFAULT NULL COMMENT 'NULL for non-chart widgets', + `widget_type` ENUM('chart','text','image','filter_date','filter_dropdown') NOT NULL DEFAULT 'chart', + `title` VARCHAR(200) NULL DEFAULT NULL COMMENT 'Widget-level title override', + -- Grid position (react-grid-layout style) + `grid_x` TINYINT UNSIGNED NOT NULL DEFAULT 0, + `grid_y` TINYINT UNSIGNED NOT NULL DEFAULT 0, + `grid_w` TINYINT UNSIGNED NOT NULL DEFAULT 2 COMMENT 'Width in grid units', + `grid_h` TINYINT UNSIGNED NOT NULL DEFAULT 2 COMMENT 'Height in grid units', + -- Content for non-chart widgets + `content` TEXT NULL DEFAULT NULL COMMENT 'Markdown for text widgets, URL for image widgets', + `widget_config` JSON NULL DEFAULT NULL COMMENT 'Extra config (filter field bindings, image fit, etc.)', + `sort_order` INT UNSIGNED NOT NULL DEFAULT 0, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `idx_dw_dashboard` (`dashboard_id`), + INDEX `idx_dw_chart` (`chart_id`), + CONSTRAINT `fk_dw_dashboard` + FOREIGN KEY (`dashboard_id`) REFERENCES `dashboards` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_dw_chart` + FOREIGN KEY (`chart_id`) REFERENCES `charts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Widgets placed on a dashboard'; + + +-- ============================================================ +-- TABLE: alerts +-- Threshold-based alert definitions on chart metrics +-- ============================================================ +CREATE TABLE `alerts` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `workspace_id` INT UNSIGNED NOT NULL, + `chart_id` INT UNSIGNED NOT NULL, + `name` VARCHAR(200) NOT NULL, + `metric_field` VARCHAR(150) NOT NULL COMMENT 'Which data field to watch', + `condition` ENUM('gt','lt','eq','gte','lte') NOT NULL COMMENT 'greater than, less than, etc.', + `threshold` DECIMAL(20,4) NOT NULL, + `check_interval` SMALLINT UNSIGNED NOT NULL DEFAULT 15 COMMENT 'Check every N minutes', + -- Notification + `notify_email` TINYINT(1) NOT NULL DEFAULT 0, + `email_addresses` TEXT NULL DEFAULT NULL COMMENT 'Comma-separated list of emails', + `notify_slack` TINYINT(1) NOT NULL DEFAULT 0, + `slack_webhook` VARCHAR(500) NULL DEFAULT NULL, + -- State + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `is_muted_until` DATETIME NULL DEFAULT NULL, + `last_checked_at` DATETIME NULL DEFAULT NULL, + `last_triggered_at` DATETIME NULL DEFAULT NULL, + `created_by` INT UNSIGNED NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted_at` DATETIME NULL DEFAULT NULL, + PRIMARY KEY (`id`), + INDEX `idx_alerts_workspace` (`workspace_id`), + INDEX `idx_alerts_chart` (`chart_id`), + CONSTRAINT `fk_alerts_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_alerts_chart` + FOREIGN KEY (`chart_id`) REFERENCES `charts` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_alerts_created_by` + FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Alert rules defined on chart metrics'; + + +-- ============================================================ +-- TABLE: alert_history +-- Log of every time an alert was triggered +-- ============================================================ +CREATE TABLE `alert_history` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `alert_id` INT UNSIGNED NOT NULL, + `triggered_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `metric_value` DECIMAL(20,4) NOT NULL COMMENT 'Value that triggered the alert', + `channels_notified` JSON NULL DEFAULT NULL COMMENT 'Which channels were notified', + `status` ENUM('sent','failed','muted') NOT NULL DEFAULT 'sent', + `error_message` TEXT NULL DEFAULT NULL, + PRIMARY KEY (`id`), + INDEX `idx_ah_alert` (`alert_id`), + CONSTRAINT `fk_ah_alert` + FOREIGN KEY (`alert_id`) REFERENCES `alerts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='History of triggered alert events'; + + +-- ============================================================ +-- TABLE: chart_exports +-- Log of chart/dashboard exports and downloads +-- ============================================================ +CREATE TABLE `chart_exports` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `workspace_id` INT UNSIGNED NOT NULL, + `chart_id` INT UNSIGNED NULL DEFAULT NULL, + `dashboard_id` INT UNSIGNED NULL DEFAULT NULL, + `export_type` ENUM('png','svg','csv','excel','pdf') NOT NULL, + `file_path` VARCHAR(500) NULL DEFAULT NULL, + `file_size` INT UNSIGNED NULL DEFAULT NULL COMMENT 'File size in bytes', + `exported_by` INT UNSIGNED NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `idx_ce_workspace` (`workspace_id`), + CONSTRAINT `fk_ce_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_ce_exported_by` + FOREIGN KEY (`exported_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Log of chart and dashboard export requests'; + + +-- ============================================================ +-- TABLE: shared_links +-- Public share links for dashboards and charts +-- ============================================================ +CREATE TABLE `shared_links` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `token` VARCHAR(100) NOT NULL UNIQUE, + `type` ENUM('dashboard','chart') NOT NULL, + `resource_id` INT UNSIGNED NOT NULL, + `workspace_id` INT UNSIGNED NOT NULL, + `password_hash` VARCHAR(255) NULL DEFAULT NULL, + `expires_at` DATETIME NULL DEFAULT NULL, + `view_count` INT UNSIGNED NOT NULL DEFAULT 0, + `is_active` TINYINT(1) NOT NULL DEFAULT 1, + `created_by` INT UNSIGNED NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `idx_sl_token` (`token`), + CONSTRAINT `fk_sl_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_sl_created_by` + FOREIGN KEY (`created_by`) REFERENCES `users` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Public sharing links for dashboards and charts'; + + +-- ============================================================ +-- TABLE: audit_logs +-- Immutable record of significant user actions +-- ============================================================ +CREATE TABLE `audit_logs` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `workspace_id` INT UNSIGNED NULL DEFAULT NULL COMMENT 'NULL for super-admin actions', + `user_id` INT UNSIGNED NULL DEFAULT NULL COMMENT 'NULL for system actions', + `action` VARCHAR(100) NOT NULL COMMENT 'e.g. dashboard.created, user.login', + `resource_type` VARCHAR(50) NULL DEFAULT NULL COMMENT 'e.g. dashboard, chart, data_source', + `resource_id` INT UNSIGNED NULL DEFAULT NULL, + `old_value` JSON NULL DEFAULT NULL COMMENT 'State before the action', + `new_value` JSON NULL DEFAULT NULL COMMENT 'State after the action', + `ip_address` VARCHAR(45) NULL DEFAULT NULL COMMENT 'IPv4 or IPv6', + `user_agent` VARCHAR(500) NULL DEFAULT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `idx_al_workspace` (`workspace_id`), + INDEX `idx_al_user` (`user_id`), + INDEX `idx_al_action` (`action`), + INDEX `idx_al_created_at` (`created_at`), + CONSTRAINT `fk_al_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE SET NULL, + CONSTRAINT `fk_al_user` + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Immutable audit trail of all user actions'; + + +-- ============================================================ +-- TABLE: user_sessions +-- Tracks active login sessions (for multi-device management) +-- ============================================================ +CREATE TABLE `user_sessions` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` INT UNSIGNED NOT NULL, + `session_token` VARCHAR(255) NOT NULL UNIQUE, + `ip_address` VARCHAR(45) NULL DEFAULT NULL, + `user_agent` VARCHAR(500) NULL DEFAULT NULL, + `last_active` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `expires_at` DATETIME NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `idx_us_user` (`user_id`), + INDEX `idx_us_token` (`session_token`), + CONSTRAINT `fk_us_user` + FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Active user login sessions'; + + +-- ============================================================ +-- TABLE: tags +-- Tagging system for charts and dashboards +-- ============================================================ +CREATE TABLE `tags` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `workspace_id` INT UNSIGNED NOT NULL, + `name` VARCHAR(80) NOT NULL, + `color` VARCHAR(7) NOT NULL DEFAULT '#6366f1' COMMENT 'Hex color code', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uq_tag_name` (`workspace_id`, `name`), + CONSTRAINT `fk_tags_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Tags for organizing charts and dashboards'; + + +-- ============================================================ +-- TABLE: taggables +-- Polymorphic tag assignments (chart or dashboard) +-- ============================================================ +CREATE TABLE `taggables` ( + `tag_id` INT UNSIGNED NOT NULL, + `taggable_id` INT UNSIGNED NOT NULL, + `taggable_type` ENUM('chart','dashboard') NOT NULL, + PRIMARY KEY (`tag_id`, `taggable_id`, `taggable_type`), + CONSTRAINT `fk_taggables_tag` + FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Polymorphic join table for tagging'; + + +-- ============================================================ +-- TABLE: query_cache +-- Stores cached query results to reduce DB load +-- ============================================================ +CREATE TABLE `query_cache` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `cache_key` VARCHAR(255) NOT NULL UNIQUE COMMENT 'MD5 hash of datasource+query', + `data_source_id` INT UNSIGNED NOT NULL, + `result_data` LONGTEXT NOT NULL COMMENT 'JSON encoded query result', + `row_count` INT UNSIGNED NOT NULL DEFAULT 0, + `expires_at` DATETIME NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + INDEX `idx_qc_cache_key` (`cache_key`), + INDEX `idx_qc_expires_at` (`expires_at`), + CONSTRAINT `fk_qc_datasource` + FOREIGN KEY (`data_source_id`) REFERENCES `data_sources` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Cached query results with TTL'; + + +-- ============================================================ +-- TABLE: settings +-- Key-value application settings (per workspace or global) +-- ============================================================ +CREATE TABLE `settings` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `workspace_id` INT UNSIGNED NULL DEFAULT NULL COMMENT 'NULL = global/system setting', + `key` VARCHAR(150) NOT NULL, + `value` TEXT NULL DEFAULT NULL, + `type` ENUM('string','integer','boolean','json') NOT NULL DEFAULT 'string', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uq_setting` (`workspace_id`, `key`), + CONSTRAINT `fk_settings_workspace` + FOREIGN KEY (`workspace_id`) REFERENCES `workspaces` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + COMMENT='Key-value settings store'; + + +-- ============================================================ +-- INITIAL SEED DATA +-- ============================================================ + +-- Default Super Admin +INSERT INTO `users` (`name`, `email`, `password`, `role`, `email_verified`, `is_active`) +VALUES ( + 'Super Admin', + 'admin@chartboard.local', + '$2y$12$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', -- Admin@1234 + 'superadmin', + 1, + 1 +); + +-- Default Workspace +INSERT INTO `workspaces` (`name`, `slug`, `description`, `owner_id`) +VALUES ('Default Workspace', 'default-workspace', 'The default Chart-Board workspace', 1); + +-- Add super admin as workspace admin +INSERT INTO `workspace_members` (`workspace_id`, `user_id`, `role`, `joined_at`) +VALUES (1, 1, 'admin', NOW()); + +-- Global Settings +INSERT INTO `settings` (`workspace_id`, `key`, `value`, `type`) VALUES +(NULL, 'app_name', 'Chart-Board', 'string'), +(NULL, 'app_version', '1.0.0', 'string'), +(NULL, 'allow_registration', '1', 'boolean'), +(NULL, 'max_workspaces', '10', 'integer'), +(NULL, 'default_theme', 'light', 'string'), +(NULL, 'smtp_configured', '0', 'boolean'); + + +SET FOREIGN_KEY_CHECKS = 1; + +-- ============================================================ +-- End of chart-board schema +-- ============================================================ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..a608342 --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "name": "codeigniter4/appstarter", + "description": "CodeIgniter4 starter app", + "license": "MIT", + "type": "project", + "homepage": "https://codeigniter.com", + "support": { + "forum": "https://forum.codeigniter.com/", + "source": "https://github.com/codeigniter4/CodeIgniter4", + "slack": "https://codeigniterchat.slack.com" + }, + "require": { + "php": "^8.1", + "codeigniter4/framework": "^4.0", + "phpoffice/phpspreadsheet": "^5.5" + }, + "require-dev": { + "fakerphp/faker": "^1.9", + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^10.5.16" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Config\\": "app/Config/" + }, + "exclude-from-classmap": [ + "**/Database/Migrations/**" + ] + }, + "autoload-dev": { + "psr-4": { + "Tests\\Support\\": "tests/_support" + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + }, + "scripts": { + "test": "phpunit" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..01378e6 --- /dev/null +++ b/composer.lock @@ -0,0 +1,2534 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "33df5fc713c9a9454c5380111d6aac0d", + "packages": [ + { + "name": "codeigniter4/framework", + "version": "v4.6.5", + "source": { + "type": "git", + "url": "https://github.com/codeigniter4/framework.git", + "reference": "116e0919590a412c09d2b9e4f6b8addda18224d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/codeigniter4/framework/zipball/116e0919590a412c09d2b9e4f6b8addda18224d8", + "reference": "116e0919590a412c09d2b9e4f6b8addda18224d8", + "shasum": "" + }, + "require": { + "ext-intl": "*", + "ext-mbstring": "*", + "laminas/laminas-escaper": "^2.17", + "php": "^8.1", + "psr/log": "^3.0" + }, + "require-dev": { + "codeigniter/coding-standard": "^1.7", + "fakerphp/faker": "^1.24", + "friendsofphp/php-cs-fixer": "^3.47.1", + "kint-php/kint": "^6.1", + "mikey179/vfsstream": "^1.6.12", + "nexusphp/cs-config": "^3.6", + "phpunit/phpunit": "^10.5.16 || ^11.2", + "predis/predis": "^3.0" + }, + "suggest": { + "ext-curl": "If you use CURLRequest class", + "ext-dom": "If you use TestResponse", + "ext-exif": "If you run Image class tests", + "ext-fileinfo": "Improves mime type detection for files", + "ext-gd": "If you use Image class GDHandler", + "ext-imagick": "If you use Image class ImageMagickHandler", + "ext-libxml": "If you use TestResponse", + "ext-memcache": "If you use Cache class MemcachedHandler with Memcache", + "ext-memcached": "If you use Cache class MemcachedHandler with Memcached", + "ext-mysqli": "If you use MySQL", + "ext-oci8": "If you use Oracle Database", + "ext-pgsql": "If you use PostgreSQL", + "ext-readline": "Improves CLI::input() usability", + "ext-redis": "If you use Cache class RedisHandler", + "ext-simplexml": "If you format XML", + "ext-sodium": "If you use Encryption SodiumHandler", + "ext-sqlite3": "If you use SQLite3", + "ext-sqlsrv": "If you use SQL Server", + "ext-xdebug": "If you use CIUnitTestCase::assertHeaderEmitted()" + }, + "type": "project", + "autoload": { + "psr-4": { + "CodeIgniter\\": "system/" + }, + "exclude-from-classmap": [ + "**/Database/Migrations/**" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The CodeIgniter framework v4", + "homepage": "https://codeigniter.com", + "support": { + "forum": "https://forum.codeigniter.com/", + "slack": "https://codeigniterchat.slack.com", + "source": "https://github.com/codeigniter4/CodeIgniter4" + }, + "time": "2026-02-01T17:59:34+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "laminas/laminas-escaper", + "version": "2.17.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-escaper.git", + "reference": "df1ef9503299a8e3920079a16263b578eaf7c3ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/df1ef9503299a8e3920079a16263b578eaf7c3ba", + "reference": "df1ef9503299a8e3920079a16263b578eaf7c3ba", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-mbstring": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "conflict": { + "zendframework/zend-escaper": "*" + }, + "require-dev": { + "infection/infection": "^0.29.8", + "laminas/laminas-coding-standard": "~3.0.1", + "phpunit/phpunit": "^10.5.45", + "psalm/plugin-phpunit": "^0.19.2", + "vimeo/psalm": "^6.6.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Escaper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Securely and safely escape HTML, HTML attributes, JavaScript, CSS, and URLs", + "homepage": "https://laminas.dev", + "keywords": [ + "escaper", + "laminas" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-escaper/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-escaper/issues", + "rss": "https://github.com/laminas/laminas-escaper/releases.atom", + "source": "https://github.com/laminas/laminas-escaper" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-05-06T19:29:36+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "6187e9cc4493da94b9b63eb2315821552015fca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6187e9cc4493da94b9b63eb2315821552015fca9", + "reference": "6187e9cc4493da94b9b63eb2315821552015fca9", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.1" + }, + "require-dev": { + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.16", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^10.0", + "vimeo/psalm": "^5.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan MΓ€nnchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "AndrΓ‘s KolesΓ‘r", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2024-10-10T12:33:01+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "5.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "eecd31b885a1c8192f12738130f85bbc6e8906ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eecd31b885a1c8192f12738130f85bbc6e8906ba", + "reference": "eecd31b885a1c8192f12738130f85bbc6e8906ba", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-filter": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^8.1", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^2.0 || ^3.0", + "ext-intl": "*", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.5", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1 || ^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + }, + { + "name": "Owen Leibman" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.5.0" + }, + "time": "2026-03-01T00:58:56+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "FranΓ§ois Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "mikey179/vfsstream", + "version": "v1.6.12", + "source": { + "type": "git", + "url": "https://github.com/bovigo/vfsStream.git", + "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bovigo/vfsStream/zipball/fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "reference": "fe695ec993e0a55c3abdda10a9364eb31c6f1bf0", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5||^8.5||^9.6", + "yoast/phpunit-polyfills": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Frank Kleine", + "homepage": "http://frankkleine.de/", + "role": "Developer" + } + ], + "description": "Virtual file system to mock the real file system in unit tests.", + "homepage": "http://vfs.bovigo.org/", + "support": { + "issues": "https://github.com/bovigo/vfsStream/issues", + "source": "https://github.com/bovigo/vfsStream/tree/master", + "wiki": "https://github.com/bovigo/vfsStream/wiki" + }, + "time": "2024-08-29T18:43:31+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.63", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "33198268dad71e926626b618f3ec3966661e4d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:48:37+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": [], + "plugin-api-version": "2.2.0" +} diff --git a/env b/env new file mode 100644 index 0000000..f359ec2 --- /dev/null +++ b/env @@ -0,0 +1,69 @@ +#-------------------------------------------------------------------- +# Example Environment Configuration file +# +# This file can be used as a starting point for your own +# custom .env files, and contains most of the possible settings +# available in a default install. +# +# By default, all of the settings are commented out. If you want +# to override the setting, you must un-comment it by removing the '#' +# at the beginning of the line. +#-------------------------------------------------------------------- + +#-------------------------------------------------------------------- +# ENVIRONMENT +#-------------------------------------------------------------------- + +# CI_ENVIRONMENT = production + +#-------------------------------------------------------------------- +# APP +#-------------------------------------------------------------------- + +# app.baseURL = '' +# If you have trouble with `.`, you could also use `_`. +# app_baseURL = '' +# app.forceGlobalSecureRequests = false +# app.CSPEnabled = false + +#-------------------------------------------------------------------- +# DATABASE +#-------------------------------------------------------------------- + +# database.default.hostname = localhost +# database.default.database = ci4 +# database.default.username = root +# database.default.password = root +# database.default.DBDriver = MySQLi +# database.default.DBPrefix = +# database.default.port = 3306 + +# If you use MySQLi as tests, first update the values of Config\Database::$tests. +# database.tests.hostname = localhost +# database.tests.database = ci4_test +# database.tests.username = root +# database.tests.password = root +# database.tests.DBDriver = MySQLi +# database.tests.DBPrefix = +# database.tests.charset = utf8mb4 +# database.tests.DBCollat = utf8mb4_general_ci +# database.tests.port = 3306 + +#-------------------------------------------------------------------- +# ENCRYPTION +#-------------------------------------------------------------------- + +# encryption.key = + +#-------------------------------------------------------------------- +# SESSION +#-------------------------------------------------------------------- + +# session.driver = 'CodeIgniter\Session\Handlers\FileHandler' +# session.savePath = null + +#-------------------------------------------------------------------- +# LOGGER +#-------------------------------------------------------------------- + +# logger.threshold = 4 diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..b408a99 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,63 @@ + + + + + + + + + + + + + ./tests + + + + + + + + + + ./app + + + ./app/Views + ./app/Config/Routes.php + + + + + + + + + + + + + + + diff --git a/preload.php b/preload.php new file mode 100644 index 0000000..288b30b --- /dev/null +++ b/preload.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use CodeIgniter\Boot; +use Config\Paths; + +/* + *--------------------------------------------------------------- + * Sample file for Preloading + *--------------------------------------------------------------- + * See https://www.php.net/manual/en/opcache.preloading.php + * + * How to Use: + * 0. Copy this file to your project root folder. + * 1. Set the $paths property of the preload class below. + * 2. Set opcache.preload in php.ini. + * php.ini: + * opcache.preload=/path/to/preload.php + */ + +// Load the paths config file +require __DIR__ . '/app/Config/Paths.php'; + +// Path to the front controller +define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR); + +class preload +{ + /** + * @var array Paths to preload. + */ + private array $paths = [ + [ + 'include' => __DIR__ . '/vendor/codeigniter4/framework/system', // Change this path if using manual installation + 'exclude' => [ + // Not needed if you don't use them. + '/system/Database/OCI8/', + '/system/Database/Postgre/', + '/system/Database/SQLite3/', + '/system/Database/SQLSRV/', + // Not needed for web apps. + '/system/Database/Seeder.php', + '/system/Test/', + '/system/CLI/', + '/system/Commands/', + '/system/Publisher/', + '/system/ComposerScripts.php', + // Not Class/Function files. + '/system/Config/Routes.php', + '/system/Language/', + '/system/bootstrap.php', + '/system/util_bootstrap.php', + '/system/rewrite.php', + '/Views/', + // Errors occur. + '/system/ThirdParty/', + ], + ], + ]; + + public function __construct() + { + $this->loadAutoloader(); + } + + private function loadAutoloader(): void + { + $paths = new Paths(); + require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'Boot.php'; + + Boot::preload($paths); + } + + /** + * Load PHP files. + */ + public function load(): void + { + foreach ($this->paths as $path) { + $directory = new RecursiveDirectoryIterator($path['include']); + $fullTree = new RecursiveIteratorIterator($directory); + $phpFiles = new RegexIterator( + $fullTree, + '/.+((? $file) { + foreach ($path['exclude'] as $exclude) { + if (str_contains($file[0], $exclude)) { + continue 2; + } + } + + require_once $file[0]; + // Uncomment only for debugging (to inspect which files are included). + // Never use this in production - preload scripts must not generate output. + // echo 'Loaded: ' . $file[0] . "\n"; + } + } + } +} + +(new preload())->load(); diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..abac3cb --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,49 @@ +# Disable directory browsing +Options -Indexes + +# ---------------------------------------------------------------------- +# Rewrite engine +# ---------------------------------------------------------------------- + +# Turning on the rewrite engine is necessary for the following rules and features. +# FollowSymLinks must be enabled for this to work. + + Options +FollowSymlinks + RewriteEngine On + + # If you installed CodeIgniter in a subfolder, you will need to + # change the following line to match the subfolder you need. + # http://httpd.apache.org/docs/current/mod/mod_rewrite.html#rewritebase + # RewriteBase / + + # Redirect Trailing Slashes... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Rewrite "www.example.com -> example.com" + RewriteCond %{HTTPS} !=on + RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L] + + # Checks to see if the user is attempting to access a valid file, + # such as an image or css document, if this isn't true it sends the + # request to the front controller, index.php + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^([\s\S]*)$ index.php/$1 [L,NC,QSA] + + # Ensure Authorization header is passed along + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + + + # If we don't have mod_rewrite installed, all 404's + # can be sent to index.php, and everything works as normal. + ErrorDocument 404 index.php + + +# Disable server signature start +ServerSignature Off +# Disable server signature end diff --git a/public/assets/css/chart-builder.css b/public/assets/css/chart-builder.css new file mode 100644 index 0000000..98bb846 --- /dev/null +++ b/public/assets/css/chart-builder.css @@ -0,0 +1,218 @@ +/* Chart builder β€” Phase 6 (aligned with Chart-Board tokens) */ +:root { + --cb-cb-surface: #f8fafc; + --cb-cb-elevated: #ffffff; + --cb-cb-muted: #64748b; + --cb-cb-border: rgba(148, 163, 184, 0.35); + --cb-cb-ink: #0f172a; + --cb-cb-accent: #0f172a; +} + +.cb-chart-builder { + font-family: Inter, system-ui, sans-serif; + color: var(--cb-cb-ink); +} + +.cb-cb-stepbar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem 0.25rem; + padding: 0.75rem 0 1.25rem; + border-bottom: 1px solid var(--cb-cb-border); + margin-bottom: 1.5rem; +} + +.cb-cb-pill { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0.85rem; + border-radius: 0.5rem; + font-size: 0.8125rem; + font-weight: 600; + color: var(--cb-cb-muted); + background: #f1f5f9; + border: none; + cursor: pointer; + transition: background 0.15s, color 0.15s; +} + +.cb-cb-pill:hover { + color: var(--cb-cb-ink); + background: #e2e8f0; +} + +.cb-cb-pill.active { + background: var(--cb-cb-ink); + color: #fff; +} + +.cb-cb-pill.done { + background: #ecfdf5; + color: #047857; +} + +.cb-cb-pill-num { + width: 1.35rem; + height: 1.35rem; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 0.65rem; + background: rgba(255, 255, 255, 0.25); +} + +.cb-cb-pill:not(.active) .cb-cb-pill-num { + background: #e2e8f0; + color: #475569; +} + +.cb-cb-pill.done .cb-cb-pill-num { + background: #a7f3d0; + color: #065f46; +} + +.cb-cb-connector { + width: 1.25rem; + height: 2px; + background: var(--cb-cb-border); + flex-shrink: 0; +} + +.cb-cb-card { + background: var(--cb-cb-elevated); + border-radius: 0.75rem; + border: 1px solid var(--cb-cb-border); + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); +} + +.cb-cb-card-h { + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--cb-cb-border); + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} + +.cb-cb-card-b { + padding: 1.25rem; +} + +.cb-cb-type-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr)); + gap: 0.5rem; +} + +.cb-cb-type { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + padding: 0.75rem 0.35rem; + border-radius: 0.5rem; + border: 2px solid transparent; + background: #f8fafc; + cursor: pointer; + font-size: 0.65rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--cb-cb-muted); + transition: border-color 0.15s, background 0.15s, color 0.15s; +} + +.cb-cb-type:hover { + background: #e0e7ff; + color: #3730a3; +} + +.cb-cb-type.selected { + border-color: var(--cb-cb-accent); + background: #0f172a; + color: #fff; +} + +.cb-cb-type-ico { + font-size: 1.25rem; + line-height: 1; +} + +.cb-cb-preview-host { + min-height: 320px; + background: linear-gradient(180deg, #f8fafc 0%, #fff 100%); + border-radius: 0.5rem; + border: 1px dashed var(--cb-cb-border); +} + +.cb-cb-hint { + display: flex; + gap: 0.75rem; + padding: 0.9rem 1rem; + border-radius: 0.5rem; + background: #0f172a; + color: #e2e8f0; + font-size: 0.8125rem; + line-height: 1.5; +} + +.cb-cb-fm-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); + gap: 0.75rem; + align-items: center; + padding: 0.65rem 0; + border-bottom: 1px solid #f1f5f9; +} + +.cb-cb-fm-row:last-child { + border-bottom: none; +} + +.cb-cb-kpi-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1rem; +} + +.cb-cb-kpi { + text-align: center; + padding: 1.25rem; + background: #f8fafc; + border-radius: 0.65rem; + border: 1px solid var(--cb-cb-border); +} + +.cb-cb-kpi-val { + font-size: 1.65rem; + font-weight: 800; + letter-spacing: -0.02em; + color: var(--cb-cb-ink); +} + +.cb-cb-kpi-lbl { + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--cb-cb-muted); + margin-top: 0.35rem; +} + +.cb-cb-color-swatch { + width: 2.25rem; + height: 2.25rem; + padding: 0.15rem; + border-radius: 0.35rem; + border: 1px solid var(--cb-cb-border); + cursor: pointer; +} + +@media (max-width: 768px) { + .cb-cb-fm-row { + grid-template-columns: 1fr; + } +} diff --git a/public/assets/css/dashboard.css b/public/assets/css/dashboard.css new file mode 100644 index 0000000..c6e5373 --- /dev/null +++ b/public/assets/css/dashboard.css @@ -0,0 +1,377 @@ +/* Dashboard module β€” list + builder shell */ +.cb-dash-btn-primary { + background: #fff; + color: #0f172a; + font-weight: 700; + border: none; + border-radius: 12px; + padding: 10px 18px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); +} + +.cb-dash-btn-primary:hover { + background: #f1f5f9; + color: #0f172a; +} + +.cb-dash-section-label { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; + color: #9aa0b4; + margin-bottom: 14px; +} + +.cb-dash-empty { + background: #fff; + border-radius: 20px; + border: 1px dashed #cbd5e1; +} + +.cb-dash-empty-icon { + width: 72px; + height: 72px; + border-radius: 18px; + background: linear-gradient(145deg, #e0e7ff, #f8fafc); + display: flex; + align-items: center; + justify-content: center; + color: #4338ca; +} + +.cb-dash-empty-icon .material-symbols-outlined { + font-size: 36px; +} + +.cb-dash-card { + position: relative; + background: #fff; + border-radius: 16px; + border: 1px solid #e8ecf1; + overflow: hidden; + transition: box-shadow 0.2s ease, transform 0.2s ease; +} + +.cb-dash-card:hover { + box-shadow: 0 14px 40px rgba(15, 23, 42, 0.08); + transform: translateY(-2px); +} + +.cb-dash-card-accent { + height: 4px; + background: linear-gradient(90deg, #7c3aed, #ec4899); +} + +.cb-dash-card-body { + padding: 20px 20px 18px; +} + +.cb-dash-list-page .row { + --bs-gutter-x: 16px; + --bs-gutter-y: 16px; +} + +.cb-dash-card-body .btn-dark { + background: #1a1d23; + border-color: #1a1d23; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + padding: 6px 14px; +} + +.cb-dash-card-body .btn-outline-secondary { + border-color: #e2e4e9; + color: #5a6072; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + padding: 6px 14px; +} + +.cb-dash-card-title { + font-weight: 800; + font-size: 1.05rem; + color: #0f172a; + letter-spacing: -0.02em; +} + +.cb-dash-card-title:hover { + color: #4338ca; +} + +.text-truncate-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.cb-dash-badge { + display: inline-flex; + align-items: center; + padding: 2px 9px; + border-radius: 20px; + font-size: 11px; + font-weight: 600; + background: #eff4ff; + color: #1a56db; +} + +.cb-dash-badge-live { + background: #ecfdf5; + color: #059669; +} + +.cb-dash-badge-muted { + background: #f3f4f6; + color: #6b7280; +} + +.cb-dash-badge-shared { + background: #ecfdf5; + color: #059669; + border: 1px solid #a7f3d0; +} + +.cb-dash-meta-row { + gap: 8px !important; +} + +.cb-dash-modal { + border-radius: 16px; +} + +.cb-dash-pin { + color: #64748b; +} + +.cb-dash-pin:hover { + color: #4338ca; + background: #eef2ff !important; +} + +/* β€”β€” Builder canvas β€”β€” */ +.cb-dash-shell { + /* Bleed horizontally into page gutters only; keep top margin β‰₯0 so toolbar clears the fixed topbar. */ + margin-top: 0; + margin-bottom: 0; + margin-left: calc(-1 * var(--page-padding-x)); + margin-right: calc(-1 * var(--page-padding-x)); + padding: calc(var(--page-padding-top) + 10px) var(--page-padding-x) 32px; + min-height: calc(100vh - var(--topbar-height)); +} + +.cb-dash-shell.cb-theme-dark { + background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%); + color: #e2e8f0; +} + +.cb-dash-shell.cb-theme-dark .cb-dash-toolbar { + background: rgba(30, 41, 59, 0.85); + border-color: rgba(148, 163, 184, 0.2); +} + +.cb-dash-shell.cb-theme-dark .cb-dash-widget-card { + background: rgba(30, 41, 59, 0.9); + border-color: rgba(148, 163, 184, 0.15); + color: #e2e8f0; +} + +.cb-dash-toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + padding: 14px 18px; + background: rgba(255, 255, 255, 0.92); + backdrop-filter: blur(10px); + border: 1px solid #e5e7eb; + border-radius: 16px; + margin-bottom: 18px; + box-shadow: 0 4px 24px rgba(15, 23, 42, 0.06); +} + +.cb-dash-toolbar h1 { + font-size: 1.15rem; + font-weight: 800; + letter-spacing: -0.02em; + margin: 0; +} + +.cb-dash-toolbar .cb-dash-meta { + font-size: 12px; + color: #64748b; +} + +.cb-dash-shell.cb-theme-dark .cb-dash-toolbar .cb-dash-meta { + color: #94a3b8; +} + +.cb-dash-grid-wrap { + min-height: 400px; +} + +/* Stretch widgets to the full GridStack cell so paired cards align visually */ +.cb-dash-grid-wrap .grid-stack-item > .grid-stack-item-content { + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.cb-dash-grid-wrap .cb-dash-widget-mount { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; +} + +.grid-stack-item-content { + overflow: hidden; +} + +.cb-dash-widget-card { + height: 100%; + background: #fff; + border: 1px solid #e8ecf1; + border-radius: 14px; + box-shadow: 0 4px 18px rgba(15, 23, 42, 0.05); + display: flex; + flex-direction: column; +} + +.cb-dash-widget-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 10px 14px; + border-bottom: 1px solid #f1f5f9; + font-size: 13px; + font-weight: 700; + color: #0f172a; +} + +.cb-dash-shell.cb-theme-dark .cb-dash-widget-head { + border-bottom-color: rgba(148, 163, 184, 0.15); + color: #f1f5f9; +} + +.cb-dash-widget-body { + flex: 1 1 auto; + min-height: 0; + position: relative; + padding: 8px 10px 12px; + display: flex; + flex-direction: column; +} + +.cb-dash-widget-body .cb-dash-chart-inner { + flex: 1 1 auto; + min-height: 180px; + height: auto; + display: flex; + flex-direction: column; +} + +.cb-dash-widget-body .cb-dash-chart-inner > div[id^="cb-chart-"] { + flex: 1 1 auto; + min-height: 180px; + width: 100%; +} + +.cb-dash-widget-remove { + border: none; + background: transparent; + color: #94a3b8; + padding: 2px; + line-height: 1; + border-radius: 6px; +} + +.cb-dash-widget-remove:hover { + background: #fef2f2; + color: #dc2626; +} + +.cb-dash-edit-hint { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #6366f1; +} + +.cb-dash-prose { + font-size: 13px; + line-height: 1.55; + color: #334155; + padding: 4px 6px; +} + +.cb-dash-shell.cb-theme-dark .cb-dash-prose { + color: #cbd5e1; +} + +.cb-dash-prose h1, .cb-dash-prose h2, .cb-dash-prose h3 { + font-size: 1rem; + font-weight: 800; + margin: 0.5em 0 0.25em; +} + +.cb-dash-img { + width: 100%; + height: 100%; + min-height: 140px; + object-fit: cover; + border-radius: 10px; +} + +.cb-dash-filter-label { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #64748b; + margin-bottom: 6px; +} + +.cb-dash-loading { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.65); + font-size: 12px; + font-weight: 600; + color: #64748b; + z-index: 2; +} + +.cb-dash-shell.cb-theme-dark .cb-dash-loading { + background: rgba(15, 23, 42, 0.55); + color: #94a3b8; +} + +/* Fullscreen: toggled on body */ +body.cb-dash-fs .cb-topbar { + display: none !important; +} + +body.cb-dash-fs .cb-content { + margin-left: 0 !important; + padding-top: 0 !important; +} + +body.cb-dash-fs .cb-dash-shell { + min-height: 100vh; + margin: 0; + padding: 16px; +} + +.grid-stack.cb-dash-editing .grid-stack-item { + cursor: grab; +} diff --git a/public/assets/css/query-builder.css b/public/assets/css/query-builder.css new file mode 100644 index 0000000..1ba9e47 --- /dev/null +++ b/public/assets/css/query-builder.css @@ -0,0 +1,363 @@ +/* Query builder β€” edit/create (typography from .cb-page in variables.css) */ +.qb-root { + max-width: 1200px; + margin: 0 auto; +} + +.qb-text-link { + color: #64748b; + font-size: var(--cb-font-topnav, 14px); + font-weight: 600; + text-decoration: none; +} + +.qb-text-link:hover { + color: #0f172a; + text-decoration: underline; +} + +.qb-flash { + border-radius: 8px; + padding: 10px 14px; + font-size: var(--cb-font-topnav, 14px); + font-weight: 600; +} + +.qb-flash-success { + background: #ecfdf5; + border: 1px solid #a7f3d0; + color: #065f46; +} + +.qb-flash-icon { + font-size: 20px !important; + color: #059669; +} + +.qb-meta-panel .qb-label-caps, +.qb-label-caps { + display: block; + font-size: 10px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #94a3b8; + margin-bottom: 6px; +} + +.qb-label-caps-tiny { + display: block; + font-size: 9px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #94a3b8; + margin-bottom: 4px; +} + +.qb-vars-heading { + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #64748b; +} + +.qb-add-from-sql { + font-size: 11px !important; + font-weight: 700 !important; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #2563eb !important; +} + +.qb-vars-column { + background: #fafbfc !important; + border-color: #e5e7eb !important; +} + +.qb-var-card { + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 12px; + padding: 14px; + margin-bottom: 12px; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); +} + +.qb-var-card:last-child { + margin-bottom: 0; +} + +.qb-var-title { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: var(--cb-font-topnav, 14px); + font-weight: 700; + color: #0f172a; +} + +.qb-var-details summary { + cursor: pointer; + list-style: none; +} + +.qb-var-details summary::-webkit-details-marker { + display: none; +} + +.qb-var-details summary::before { + content: "β–Έ "; + font-size: 10px; + color: #94a3b8; +} + +.qb-var-details[open] summary::before { + content: "β–Ύ "; +} + +/* Dark SQL IDE */ +.qb-sql-stack { + border-radius: 10px; + overflow: auto; + border: 1px solid #1e293b; + box-shadow: 0 4px 24px rgba(15, 23, 42, 0.12); +} + +.qb-sql-ide-head { + background: #1e293b; + color: #94a3b8; + font-size: 10px; + font-weight: 800; + letter-spacing: 0.12em; + text-transform: uppercase; + padding: 8px 14px; + border-bottom: 1px solid #334155; +} + +.qb-sql-ide-body { + background: #0f172a; +} + +/* Light text on dark field; Bootstrap .form-control:focus sets body color (higher specificity) β€” keep contrast in all states */ +.cb-page textarea.form-control.qb-sql-textarea-dark, +.qb-sql-textarea-dark { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: var(--cb-font-topnav, 14px); + line-height: 1.55; + color: #e2e8f0 !important; + -webkit-text-fill-color: #e2e8f0; + caret-color: #f1f5f9; + background: #0f172a !important; + border: none !important; + border-radius: 0 !important; + min-height: 280px; + resize: vertical; + padding: 14px 16px; + pointer-events: auto; + -webkit-user-select: text; + user-select: text; +} + +.qb-sql-textarea-dark::placeholder { + color: #64748b !important; + -webkit-text-fill-color: #64748b; +} + +.cb-page textarea.form-control.qb-sql-textarea-dark:focus, +.cb-page textarea.form-control.qb-sql-textarea-dark:focus-visible, +.qb-sql-textarea-dark:focus, +.qb-sql-textarea-dark:focus-visible { + color: #e2e8f0 !important; + -webkit-text-fill-color: #e2e8f0; + box-shadow: none !important; + outline: 2px solid rgba(59, 130, 246, 0.45); + outline-offset: -2px; +} + +.qb-sql-textarea-dark::selection { + background: rgba(59, 130, 246, 0.35); + color: #f8fafc; +} + +.qb-sql-footnote { + font-size: var(--cb-font-sidebar, 13px); +} + +.qb-sql-footnote code { + color: #7c3aed; + font-size: inherit; +} + +/* Runner strip */ +.qb-runner-strip { + border-top: 1px solid #e5e7eb; + background: #f8fafc; +} + +.qb-runner-strip--dark { + background: #1e293b; + border-top-color: #334155; +} + +.qb-runner-strip--dark .qb-runner-meta { + color: rgba(248, 250, 252, 0.55) !important; +} + +.qb-runner-strip--dark .btn-secondary { + background: #334155; + border-color: #475569; + color: #f1f5f9; +} + +.qb-runner-strip--dark .btn-secondary:hover { + background: #475569; + border-color: #64748b; + color: #fff; +} + +.qb-conn-status { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #64748b; +} + +.qb-runner-strip--dark .qb-conn-status { + color: rgba(248, 250, 252, 0.75); +} + +.qb-conn-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: #cbd5e1; + flex-shrink: 0; +} + +.qb-conn-dot--on { + background: #22c55e; + box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.35); +} + +/* Preview */ +.qb-preview-title { + font-size: var(--cb-font-topnav, 14px); + font-weight: 700; + letter-spacing: -0.01em; + color: #0f172a; +} + +.qb-preview-thead th { + font-size: 10px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #64748b; + background: #f8fafc !important; + border-bottom: 1px solid #e5e7eb; +} + +.qb-preview-td { + font-size: var(--cb-font-sidebar, 13px); + vertical-align: middle; +} + +.qb-preview-td--strong { + font-weight: 700; + color: #0f172a; +} + +.qb-cell-badge { + display: inline-block; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; + padding: 4px 10px; + border-radius: 999px; + background: #e0f2fe; + color: #0369a1; + border: 1px solid #bae6fd; +} + +.qb-preview-tabs .nav-link { + font-size: var(--cb-font-sidebar, 13px); + font-weight: 700; + color: #64748b; + border: none; + border-bottom: 2px solid transparent; + border-radius: 0; +} + +.qb-preview-tabs .nav-link.active { + color: #0f172a; + border-bottom-color: #0f172a; + background: transparent; +} + +.qb-tab-panel { + display: block; +} + +.qb-tab-panel.qb-tab-panel--hidden { + display: none !important; +} + +.qb-preview-meta { + font-size: var(--cb-font-sidebar, 13px); + color: #64748b; +} + +.qb-preview-table-wrap { + max-height: 420px; + overflow: auto; +} + +.qb-preview-table tbody tr:nth-child(even) { + background: #fafafa; +} + +.qb-pagination-bar { + border-top: 1px solid #e5e7eb; + padding: 12px 14px; + background: #fff; + font-size: var(--cb-font-sidebar, 13px); +} + +.qb-page-num--active { + background: #0f172a !important; + border-color: #0f172a !important; + color: #fff !important; + font-weight: 700; + min-width: 34px; +} + +.qb-page-indicator { + color: #0f172a; +} + +.qb-preview-card--fullscreen { + position: fixed; + inset: 52px 12px 12px 12px; + z-index: 1030; + max-height: none !important; + display: flex; + flex-direction: column; +} + +.qb-preview-card--fullscreen .qb-preview-table-wrap { + max-height: none; + flex: 1; + min-height: 0; +} + +@media (max-width: 991.98px) { + .qb-vars-column { + border-right: none !important; + border-bottom: 1px solid #e5e7eb; + } +} diff --git a/public/assets/css/variables.css b/public/assets/css/variables.css new file mode 100644 index 0000000..3b2199a --- /dev/null +++ b/public/assets/css/variables.css @@ -0,0 +1,1448 @@ +/* Chart Board β€” shell + shared UI tokens (see CHARTBOARD_UI_IMPROVEMENT spec) */ +:root { + --card-bg: #ffffff; + --card-border: 1px solid #e2e4e9; + --card-radius: 10px; + --card-padding: 20px 24px; + --page-padding-x: 28px; + --page-padding-top: 28px; + --table-header-bg: #f9fafb; + --table-row-divider: #f0f1f3; + --text-primary: #1a1d23; + --text-secondary: #5a6072; + --text-muted: #9aa0b4; + --accent-blue: #1a56db; + --accent-blue-lt: #eff4ff; + --success-green: #059669; + --success-green-lt: #ecfdf5; + --success-green-border: #a7f3d0; + --danger-red: #dc2626; + --surface-bg: #f4f5f7; + --divider-color: #e2e4e9; + --topbar-height: 52px; + + /* Legacy aliases used elsewhere in the app */ + --cb-black: var(--text-primary); + --cb-text: var(--text-primary); + --cb-muted: var(--text-muted); + --cb-bg: var(--surface-bg); + --cb-panel: var(--card-bg); + --cb-line: #e2e4e9; + --cb-soft: #f4f5f7; + --cb-font-topnav: 14px; + --cb-font-sidebar: 13px; +} + +body.cb-body { + margin: 0; + background: var(--cb-bg); + color: var(--cb-text); + font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; +} + +.cb-auth-shell { + font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; +} + +/* β€”β€” Top bar (full width, no sidebar) β€”β€” */ +.cb-topbar { + position: fixed; + top: 0; + left: 0; + right: 0; + background: #fff; + border-bottom: 1px solid var(--cb-line); + z-index: 1040; +} + +.cb-topbar-row { + min-height: var(--topbar-height); +} + +.cb-brand-unit { + flex-shrink: 0; +} + +.cb-brand-text { + font-size: 13px; + font-weight: 700; + letter-spacing: 0.02em; + color: var(--text-primary); +} + +.cb-topbar-divider { + width: 1px; + height: 24px; + background: var(--divider-color); + margin: 0 8px; + flex-shrink: 0; +} + +@media (min-width: 768px) { + .cb-topbar-divider { + margin: 0 16px; + } +} + +.cb-top-nav-scroll { + overflow-x: auto; + overflow-y: visible; + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + padding-right: 2px; +} + +.cb-top-main-nav { + gap: 2px; + flex-wrap: nowrap; +} + +.cb-top-main-link, +a.cb-top-main-link { + color: var(--text-secondary); + text-decoration: none; + font-size: var(--cb-font-topnav); + font-weight: 400; + padding: 6px 12px; + border-radius: 7px; + white-space: nowrap; + border: none; + box-shadow: none; +} + +button.cb-top-main-link { + background: transparent; + font-family: inherit; + line-height: 1.25; + cursor: pointer; + display: inline-flex; + align-items: center; +} + +.cb-top-main-link:hover { + color: var(--text-primary); + background: #f4f5f7; +} + +.cb-top-main-link.active { + color: var(--accent-blue); + font-weight: 600; + background: var(--accent-blue-lt); +} + +.cb-top-main-link.active:hover { + color: var(--accent-blue); + background: #e0e8fc; +} + +.cb-top-nav-dropdown .cb-top-main-link.dropdown-toggle::after { + margin-left: 0.2em; + vertical-align: 0.12em; + border-top-color: currentColor; +} + +.cb-top-nav-dropdown .cb-top-main-link.dropdown-toggle.active::after { + border-top-color: var(--accent-blue); +} + +.cb-top-nav-badge { + font-size: 0.65rem; + vertical-align: middle; +} + +.cb-topbar-actions { + gap: 10px; +} + +@media (min-width: 768px) { + .cb-topbar-actions { + gap: 12px; + } +} + +.cb-topbar-action-icon { + font-size: 20px; + cursor: default; +} + +.cb-top-dropdown-menu { + min-width: 232px; + border-radius: 10px; + border: 1px solid var(--divider-color); + margin-top: 6px !important; + padding-top: 6px; + padding-bottom: 6px; +} + +.cb-top-dropdown-item { + font-size: 13px; + padding: 8px 14px; + border-radius: 6px; + margin: 2px 8px; + color: var(--text-primary); +} + +.cb-top-dropdown-item:hover, +.cb-top-dropdown-item:focus { + background: #f4f5f7; + color: var(--text-primary); +} + +.cb-top-dropdown-item.active, +.cb-top-dropdown-item:active { + background: var(--accent-blue-lt); + color: var(--accent-blue); + font-weight: 600; +} + +.cb-top-dropdown-icon { + font-size: 18px; + width: 22px; + flex-shrink: 0; + opacity: 0.72; +} + +.cb-top-dropdown-item.active .cb-top-dropdown-icon { + opacity: 1; +} + +.cb-top-ws-wrap { + max-width: min(200px, 38vw); + min-width: 0; +} + +.cb-top-ws-select { + width: 100%; + max-width: 100%; + font-size: 12px; + font-weight: 600; + padding: 5px 26px 5px 10px; + border-radius: 7px; + border: 1px solid var(--divider-color); + background-color: #fff; + color: var(--text-primary); + cursor: pointer; +} + +.cb-top-ws-select:focus { + border-color: var(--accent-blue); + box-shadow: 0 0 0 3px rgba(26, 86, 219, 0.12); + outline: none; +} + +.cb-user-avatar { + width: 28px; + height: 28px; + font-size: 10px; + font-weight: 700; + border: 2px solid var(--accent-blue); + flex-shrink: 0; +} + +.cb-user-avatar.dropdown-toggle::after { + display: none; +} + +.cb-content { + margin-left: 0; + padding-top: var(--topbar-height); +} + +/* β€”β€” Page shell β€”β€” */ +.cb-page { + padding: 0; + font-size: var(--cb-font-sidebar); + color: var(--cb-text); +} + +.cb-page-header { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + padding: var(--page-padding-top) var(--page-padding-x) 20px; +} + +.cb-page-header--center { + align-items: center; +} + +.cb-page-title { + font-size: 1.75rem; + font-weight: 700; + color: var(--text-primary); + letter-spacing: -0.02em; + margin: 0; + line-height: 1.2; +} + +.cb-page-subtitle { + font-size: 13px; + color: var(--text-secondary); + margin-top: 4px; + margin-bottom: 0; + max-width: 640px; +} + +.cb-page-primary-btn.btn, +a.cb-page-primary-btn { + background: #1a1d23; + border-color: #1a1d23; + color: #fff; + border-radius: 7px; + padding: 9px 18px; + font-size: 13px; + font-weight: 600; +} + +.cb-page-primary-btn.btn:hover, +a.cb-page-primary-btn:hover { + background: #111318; + border-color: #111318; + color: #fff; + filter: brightness(0.92); +} + +.cb-page-primary-btn.btn:focus-visible { + box-shadow: 0 0 0 3px rgba(26, 86, 219, 0.2); +} + +.cb-page-body { + padding: 0 var(--page-padding-x) var(--page-padding-x); +} + +/* β€”β€” Stats + workspace list β€”β€” */ +.cb-stat-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 14px; +} + +@media (max-width: 991.98px) { + .cb-stat-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 575.98px) { + .cb-stat-grid { + grid-template-columns: 1fr; + } +} + +.cb-stat-card { + background: var(--card-bg); + border-radius: var(--card-radius); + border: var(--card-border); + padding: 20px 24px; +} + +.cb-stat-title { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.09em; + text-transform: uppercase; + color: var(--text-muted); +} + +.cb-stat-value { + margin-top: 6px; + font-size: 2rem; + font-weight: 700; + line-height: 1.15; + color: var(--text-primary); + letter-spacing: -0.02em; +} + +.cb-toolbar-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 20px; +} + +.cb-search-wrap { + position: relative; + flex: 1 1 200px; + min-width: 160px; +} + +.cb-search-input { + width: 100%; + background: #ffffff; + border: 1px solid var(--divider-color); + border-radius: 7px; + padding: 8px 14px 8px 38px; + font-size: 13px; + color: var(--text-primary); +} + +.cb-search-input:focus { + border-color: var(--accent-blue); + box-shadow: 0 0 0 3px rgba(26, 86, 219, 0.1); + outline: none; +} + +.cb-view-toggle { + border: 1px solid var(--divider-color); + border-radius: 7px; + background: #fff; + padding: 6px 14px; + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); +} + +.cb-view-toggle:hover { + background: #f4f5f7; +} + +.cb-view-toggle.active { + background: #1a1d23; + color: #fff; + border-color: #1a1d23; +} + +.cb-workspace-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +@media (max-width: 1199.98px) { + .cb-workspace-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 767.98px) { + .cb-workspace-grid { + grid-template-columns: 1fr; + } +} + +.cb-workspace-card { + border: var(--card-border); + border-radius: var(--card-radius); + overflow: hidden; + background: var(--card-bg); + transition: + box-shadow 0.2s ease, + border-color 0.2s ease; +} + +.cb-workspace-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); + border-color: #d0d3dc; + transform: none; +} + +.cb-workspace-card .top-strip { + height: 4px; + background: var(--accent-blue); + border-radius: var(--card-radius) var(--card-radius) 0 0; +} + +.cb-workspace-card-body { + padding: 20px; +} + +.cb-ws-card-icon { + width: 32px; + height: 32px; + border-radius: 8px; + background: #e8eaf6; + display: flex; + align-items: center; + justify-content: center; + color: var(--accent-blue); +} + +.cb-ws-card-icon .material-symbols-outlined { + font-size: 18px; +} + +.cb-badge-active { + background: var(--success-green-lt); + color: var(--success-green); + border: 1px solid var(--success-green-border); + border-radius: 20px; + padding: 3px 10px; + font-size: 11px; + font-weight: 700; +} + +.cb-badge-archived { + background: #f3f4f6; + color: #6b7280; + border: 1px solid #e5e7eb; + border-radius: 20px; + padding: 3px 10px; + font-size: 11px; + font-weight: 700; +} + +.cb-ws-card-title { + font-size: 15px; + font-weight: 700; + color: var(--text-primary); + margin-top: 14px; + margin-bottom: 0; +} + +.cb-ws-slug { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 12px; + color: var(--text-muted); + margin-top: 2px; +} + +.cb-ws-desc { + font-size: 13px; + color: var(--text-secondary); + margin-top: 8px; + min-height: 40px; + margin-bottom: 0; +} + +.cb-ws-card-footer { + margin-top: 16px; + padding-top: 14px; + border-top: 1px solid #f0f1f3; +} + +.cb-ws-role-badge { + font-size: 11px; + font-weight: 700; + border-radius: 5px; + padding: 3px 10px; + background: #1a1d23; + color: #fff; +} + +.cb-ws-action-btn { + border: 1px solid var(--divider-color); + background: #fff; + color: var(--text-secondary); + border-radius: 6px; + padding: 5px 12px; + font-size: 12px; + font-weight: 500; + text-decoration: none; + display: inline-block; + line-height: 1.25; +} + +.cb-ws-action-btn:hover { + background: #f9fafb; + color: var(--text-primary); +} + +.cb-ws-action-btn--primary { + border-color: #bfdbfe; + color: var(--accent-blue); +} + +.cb-ws-action-btn--primary:hover { + background: var(--accent-blue-lt); + color: var(--accent-blue); +} + +.cb-ws-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +/* β€”β€” Data tables β€”β€” */ +.cb-table-shell { + background: var(--card-bg); + border: var(--card-border); + border-radius: var(--card-radius); + overflow: hidden; +} + +/* Pagination below data tables (Bootstrap .pagination) */ +.cb-pager-wrap { + padding: 12px 16px; + border-top: 1px solid var(--table-row-divider); + background: var(--card-bg); +} + +.cb-pager-nav .pagination { + gap: 4px; +} + +.cb-pager-nav .page-link { + font-size: 13px; + padding: 6px 12px; + border-radius: 6px; + color: var(--text-secondary); + border-color: var(--divider-color); + background: var(--card-bg); +} + +.cb-pager-nav .page-item.active .page-link { + background: var(--accent-blue); + border-color: var(--accent-blue); + color: #fff; +} + +.cb-pager-nav .page-link:hover { + background: #f4f5f7; + color: var(--text-primary); + border-color: var(--divider-color); +} + +.cb-pager-nav .page-item.active .page-link:hover { + background: var(--accent-blue); + color: #fff; +} + +.cb-admin-role-pill { + display: inline-block; + font-size: 11.5px; + font-weight: 600; + text-transform: lowercase; + padding: 3px 10px; + border-radius: 999px; + background: #1a1d23; + color: #fff; +} + +.cb-audit-action-code { + font-size: 12px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + color: #9d174d; + background: #fce7f3; + padding: 3px 8px; + border-radius: 5px; + border: 1px solid #fbcfe8; + word-break: break-all; +} + +/* Full-width form blocks (same width as list / table pages inside cb-page-body) */ +.cb-form-shell { + background: var(--card-bg); + border: var(--card-border); + border-radius: var(--card-radius); + padding: 20px 24px; + margin-bottom: 14px; +} + +.cb-form-shell:last-child { + margin-bottom: 0; +} + +.cb-form-shell .form-label { + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 6px; +} + +.cb-form-shell .form-control, +.cb-form-shell .form-select { + font-size: 13px; +} + +.cb-form-shell .form-control:focus, +.cb-form-shell .form-select:focus { + border-color: var(--accent-blue); + box-shadow: 0 0 0 3px rgba(26, 86, 219, 0.1); +} + +/* Panel title above forms / tables (invitations, etc.) */ +.cb-shell-section-title { + font-size: 13px; + font-weight: 700; + letter-spacing: -0.01em; + color: var(--text-primary); + margin: 0 0 14px; +} + +.cb-table-shell-banner { + padding: 14px 16px 4px; +} + +.cb-table-shell-banner .cb-shell-section-title { + margin-bottom: 10px; +} + +.cb-role-pill { + display: inline-block; + font-size: 11.5px; + font-weight: 600; + text-transform: capitalize; + padding: 3px 10px; + border-radius: 999px; + background: #f1f5f9; + color: #475569; + border: 1px solid #e2e8f0; +} + +.cb-page-header.cb-page-header--compact { + padding-bottom: 12px; +} + +.cb-page-body.cb-page-body--form-tight { + padding-bottom: 16px; +} + +.cb-ws-danger-shell { + border-color: #fca5a5; + padding: 16px 20px; +} + +.cb-data-table.table { + margin-bottom: 0; + font-size: 13px; + color: var(--text-primary); +} + +.cb-data-table thead th { + background: var(--table-header-bg); + border-bottom: 1px solid var(--divider-color); + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + color: var(--text-muted); + padding: 11px 16px; + vertical-align: middle; +} + +.cb-data-table tbody td { + padding: 13px 16px; + vertical-align: middle; + border-bottom: 1px solid var(--table-row-divider); +} + +.cb-data-table tbody tr:last-child td { + border-bottom: none; +} + +.cb-data-table tbody tr:hover { + background: #fafafa; +} + +.cb-cell-name { + font-weight: 600; + font-size: 13px; + color: var(--text-primary); +} + +.cb-cell-sub { + font-size: 11.5px; + color: var(--text-muted); + margin-top: 2px; +} + +.cb-badge-connected { + display: inline-block; + background: var(--success-green-lt); + color: var(--success-green); + border: 1px solid var(--success-green-border); + border-radius: 20px; + padding: 3px 10px; + font-size: 11.5px; + font-weight: 600; +} + +.cb-table-actions { + display: inline-flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + align-items: center; +} + +.cb-action-btn { + border-radius: 6px; + padding: 5px 12px; + font-size: 12px; + font-weight: 500; + line-height: 1.25; + text-decoration: none; + border: 1px solid var(--divider-color); + background: #fff; + color: var(--text-secondary); + display: inline-block; +} + +.cb-action-btn:hover { + filter: brightness(0.97); +} + +.cb-action-btn--ghost { + border-color: var(--divider-color); + color: var(--text-secondary); +} + +.cb-action-btn--test { + border-color: var(--success-green-border); + color: var(--success-green); + background: #fff; +} + +.cb-action-btn--edit { + border-color: #bfdbfe; + color: var(--accent-blue); + background: #fff; +} + +.cb-action-btn--delete { + border-color: #fca5a5; + color: var(--danger-red); + background: #fff; +} + +.cb-action-btn--chart { + border-color: var(--divider-color); + color: var(--text-primary); + font-weight: 600; +} + +button.cb-action-btn { + cursor: pointer; +} + +/* Inline role editor in member / team tables */ +.cb-member-role-form { + margin: 0; + display: inline-flex; + flex-wrap: nowrap; + align-items: center; + gap: 8px; +} + +/* Bootstrap .form-select defaults to width:100%; that stacks the Update button below */ +.cb-member-role-form .form-select { + width: auto; + flex: 0 0 auto; + max-width: 168px; + min-width: 120px; + font-size: 13px; + padding-top: 5px; + padding-bottom: 5px; +} + +.cb-member-role-form .cb-action-btn { + flex-shrink: 0; +} + +.cb-data-table .cb-table-cell--inline-form { + white-space: nowrap; +} + +/* Query type badges */ +.cb-badge-type { + border-radius: 5px; + padding: 2px 8px; + font-size: 10.5px; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.cb-badge-type--sql { + background: #f3f4f6; + color: #374151; +} + +.cb-badge-type--api { + background: var(--accent-blue-lt); + color: var(--accent-blue); +} + +.cb-badge-type--visual { + background: #f5f3ff; + color: #7c3aed; +} + +/* β€”β€” Charts list β€”β€” */ +.cb-chart-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +@media (max-width: 1199.98px) { + .cb-chart-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 767.98px) { + .cb-chart-grid { + grid-template-columns: 1fr; + } +} + +.cb-chart-card { + position: relative; + background: var(--card-bg); + border: var(--card-border); + border-radius: var(--card-radius); + padding: 0; + overflow: hidden; + transition: + box-shadow 0.2s ease, + border-color 0.2s ease; +} + +.cb-chart-card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); + border-color: #d0d3dc; +} + +.cb-chart-card-accent { + display: block; + height: 3px; + width: 100%; + background: var(--accent-blue); + border-radius: var(--card-radius) var(--card-radius) 0 0; +} + +.cb-chart-card-inner { + padding: 16px 20px 20px; +} + +.cb-chart-type-badge { + background: #f3f4f6; + color: #374151; + border-radius: 5px; + padding: 3px 9px; + font-size: 10.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.cb-chart-card-name { + font-size: 15px; + font-weight: 700; + color: var(--text-primary); +} + +.cb-chart-meta { + font-size: 12px; + color: var(--text-muted); +} + +.cb-chart-card-actions { + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid #f0f1f3; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.cb-chart-btn { + border-radius: 6px; + padding: 6px 14px; + font-size: 12px; + font-weight: 500; + line-height: 1.25; +} + +.cb-chart-btn-share, +.cb-chart-btn-dup { + border: 1px solid var(--divider-color); + background: #fff; + color: var(--text-secondary); +} + +.cb-chart-btn-share:hover, +.cb-chart-btn-dup:hover { + background: #f9fafb; +} + +.cb-chart-btn-edit.btn-primary { + background: var(--accent-blue); + border-color: var(--accent-blue); + border-radius: 6px; + padding: 6px 14px; + font-size: 12px; + font-weight: 500; +} + +.cb-chart-btn-del { + border: 1px solid #fca5a5; + color: var(--danger-red); + background: #fff; + border-radius: 6px; + padding: 6px 14px; + font-size: 12px; + font-weight: 500; +} + +/* β€”β€” Alerts β€”β€” */ +.cb-alert-panel { + background: var(--card-bg); + border: var(--card-border); + border-radius: var(--card-radius); + padding: 20px; +} + +.cb-alert-panel-title { + font-size: 13px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 0; +} + +.cb-alert-empty-text { + font-size: 13px; + color: var(--text-muted); + margin-top: 12px; + margin-bottom: 0; +} + +.cb-alert-empty-panel { + background: var(--card-bg); + border: var(--card-border); + border-radius: var(--card-radius); + min-height: 280px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + padding: 32px 24px; +} + +.cb-alert-empty-icon { + font-size: 48px; + color: var(--text-muted); + opacity: 0.5; +} + +.cb-alert-empty-msg { + font-size: 14px; + color: var(--text-secondary); + margin-top: 12px; + margin-bottom: 0; +} + +.cb-alert-create-btn.btn { + margin-top: 16px; + background: #1a1d23; + border-color: #1a1d23; + color: #fff; + border-radius: 7px; + padding: 9px 20px; + font-size: 13px; + font-weight: 600; +} + +/* β€”β€” Settings: appearance (theme) β€”β€” */ +.cb-settings-section-title { + font-size: 15px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 4px; + letter-spacing: -0.01em; +} + +.cb-settings-section-hint { + font-size: 13px; + color: var(--text-secondary); + margin-bottom: 20px; + line-height: 1.45; +} + +.cb-theme-tiles { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; +} + +@media (max-width: 575.98px) { + .cb-theme-tiles { + grid-template-columns: 1fr; + } +} + +.cb-theme-tile-form { + margin: 0; + height: 100%; +} + +.cb-theme-tile { + width: 100%; + min-height: 118px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + padding: 18px 14px; + border: 1px solid var(--divider-color); + border-radius: var(--card-radius); + background: var(--card-bg); + color: var(--text-primary); + cursor: pointer; + transition: + border-color 0.15s ease, + background 0.15s ease, + box-shadow 0.15s ease; + text-align: center; + font-family: inherit; +} + +.cb-theme-tile:hover { + border-color: #d0d3dc; + background: #f9fafb; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.cb-theme-tile:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(26, 86, 219, 0.22); +} + +.cb-theme-tile.active { + border-color: var(--accent-blue); + background: var(--accent-blue-lt); + box-shadow: 0 0 0 1px var(--accent-blue); +} + +.cb-theme-tile-icon { + font-size: 30px; + line-height: 1; + color: var(--accent-blue); +} + +.cb-theme-tile.active .cb-theme-tile-icon { + color: var(--accent-blue); +} + +.cb-theme-tile-title { + font-size: 14px; + font-weight: 700; + display: block; +} + +.cb-theme-tile-desc { + font-size: 12px; + color: var(--text-muted); + font-weight: 500; + display: block; + line-height: 1.35; + max-width: 200px; +} + +.cb-theme-tile.active .cb-theme-tile-desc { + color: var(--text-secondary); +} + +/* β€”β€” Settings form β€”β€” */ +.cb-settings-form-card { + background: var(--card-bg); + border: var(--card-border); + border-radius: var(--card-radius); + padding: 28px 32px; + max-width: 760px; + margin-left: auto; + margin-right: auto; +} + +.cb-settings-form-card .form-label { + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 6px; +} + +.cb-settings-form-card .form-control, +.cb-settings-form-card .form-select, +.cb-settings-form-card textarea.form-control { + background: #ffffff; + border: 1px solid var(--divider-color); + border-radius: 7px; + padding: 9px 12px; + font-size: 13px; + color: var(--text-primary); +} + +.cb-settings-form-card textarea.form-control { + min-height: 100px; + resize: vertical; +} + +.cb-settings-form-card .form-control:focus, +.cb-settings-form-card .form-select:focus, +.cb-settings-form-card textarea.form-control:focus { + border-color: var(--accent-blue); + box-shadow: 0 0 0 3px rgba(26, 86, 219, 0.1); + outline: none; +} + +.cb-settings-back.btn { + border: 1px solid var(--divider-color); + background: #fff; + color: var(--text-secondary); + border-radius: 7px; + padding: 7px 16px; + font-size: 13px; +} + +.cb-settings-grid-3 { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; +} + +@media (max-width: 767.98px) { + .cb-settings-grid-3 { + grid-template-columns: 1fr; + } +} + +/* β€”β€” Shared card (sidebar workspace uses border token) β€”β€” */ +.cb-card { + background: var(--cb-panel); + border: var(--card-border); + border-radius: var(--card-radius); +} + +/* Legacy page headings */ +.cb-page h1.h4, +.cb-page .h4.cb-page-title-legacy { + font-weight: 700; + letter-spacing: -0.02em; + color: var(--text-primary); +} + +.cb-page .h6, +.cb-page h2.h6, +.cb-page h3.h6 { + font-size: var(--cb-font-topnav); + font-weight: 700; +} + +.cb-page h2.h5, +.cb-page .h5 { + font-size: var(--cb-font-topnav); + font-weight: 700; +} + +.cb-page .text-muted { + font-size: inherit; +} + +.cb-page .form-control:not(.form-control-sm), +.cb-page .form-select:not(.form-select-sm), +.cb-page textarea.form-control:not(.form-control-sm) { + font-size: var(--cb-font-topnav); +} + +.cb-page .btn:not(.btn-sm) { + font-size: var(--cb-font-topnav); +} + +/* Auth screens */ +.cb-auth-body { + font-size: var(--cb-font-sidebar); + color: var(--cb-text); +} + +.cb-auth-body h1.h4 { + font-weight: 700; + letter-spacing: -0.01em; + color: var(--cb-black); +} + +.cb-auth-body .form-control, +.cb-auth-body .form-select { + font-size: var(--cb-font-topnav); +} + +.cb-auth-body .btn { + font-size: var(--cb-font-topnav); +} + +/* β€”β€” Dark shell β€”β€” */ +html[data-cb-theme="dark"] { + --text-primary: #f1f5f9; + --text-secondary: #cbd5e1; + --text-muted: #94a3b8; + --card-bg: #1e293b; + --surface-bg: #0f172a; + --table-header-bg: #334155; + --table-row-divider: rgba(148, 163, 184, 0.15); + --divider-color: #334155; + --accent-blue-lt: rgba(26, 86, 219, 0.2); + --cb-black: #f1f5f9; + --cb-text: #e2e8f0; + --cb-muted: #94a3b8; + --cb-bg: #0f172a; + --cb-panel: #1e293b; + --cb-line: #334155; + --cb-soft: #1e293b; +} + +html[data-cb-theme="dark"] .cb-topbar { + background: #1e293b; + border-bottom-color: var(--cb-line); +} + +html[data-cb-theme="dark"] .cb-brand-text { + color: var(--text-primary); +} + +html[data-cb-theme="dark"] .cb-topbar-divider { + background: var(--cb-line); +} + +html[data-cb-theme="dark"] .cb-top-main-link { + color: var(--text-secondary); +} + +html[data-cb-theme="dark"] .cb-top-main-link:hover { + background: #334155; + color: #f8fafc; +} + +html[data-cb-theme="dark"] .cb-top-main-link.active { + color: #93c5fd; + background: rgba(26, 86, 219, 0.22); +} + +html[data-cb-theme="dark"] .cb-top-main-link.active:hover { + color: #bfdbfe; + background: rgba(26, 86, 219, 0.32); +} + +html[data-cb-theme="dark"] .cb-top-nav-dropdown .cb-top-main-link.dropdown-toggle.active::after { + border-top-color: #93c5fd; +} + +html[data-cb-theme="dark"] .cb-user-avatar { + border-color: var(--accent-blue); +} + +html[data-cb-theme="dark"] .cb-top-dropdown-menu { + background: var(--cb-panel); + border-color: var(--cb-line); +} + +html[data-cb-theme="dark"] .cb-top-dropdown-item { + color: var(--text-primary); +} + +html[data-cb-theme="dark"] .cb-top-dropdown-item:hover, +html[data-cb-theme="dark"] .cb-top-dropdown-item:focus { + background: #334155; + color: #f8fafc; +} + +html[data-cb-theme="dark"] .cb-top-dropdown-item.active { + background: rgba(26, 86, 219, 0.28); + color: #93c5fd; +} + +html[data-cb-theme="dark"] .cb-top-ws-select { + background: #0f172a; + border-color: var(--cb-line); + color: var(--text-primary); +} + +html[data-cb-theme="dark"] .cb-card { + border-color: var(--cb-line); +} + +html[data-cb-theme="dark"] .cb-stat-value, +html[data-cb-theme="dark"] .cb-page-title { + color: var(--text-primary); +} + +html[data-cb-theme="dark"] .cb-workspace-card, +html[data-cb-theme="dark"] .cb-table-shell, +html[data-cb-theme="dark"] .cb-form-shell, +html[data-cb-theme="dark"] .cb-chart-card, +html[data-cb-theme="dark"] .cb-alert-panel, +html[data-cb-theme="dark"] .cb-alert-empty-panel { + border-color: var(--cb-line); + background: var(--cb-panel); +} + +html[data-cb-theme="dark"] .cb-data-table thead th { + background: var(--table-header-bg); + color: var(--text-muted); + border-color: var(--cb-line); +} + +html[data-cb-theme="dark"] .cb-data-table tbody tr:hover { + background: rgba(30, 41, 59, 0.85); +} + +html[data-cb-theme="dark"] .cb-search-input, +html[data-cb-theme="dark"] .cb-action-btn, +html[data-cb-theme="dark"] .cb-ws-action-btn { + background: var(--cb-panel); + border-color: var(--cb-line); + color: var(--text-secondary); +} + +html[data-cb-theme="dark"] .cb-settings-form-card .form-control, +html[data-cb-theme="dark"] .cb-settings-form-card .form-select, +html[data-cb-theme="dark"] .cb-form-shell .form-control, +html[data-cb-theme="dark"] .cb-form-shell .form-select, +html[data-cb-theme="dark"] .cb-member-role-form .form-select { + background: #0f172a; + border-color: var(--cb-line); + color: var(--text-primary); +} + +html[data-cb-theme="dark"] .cb-role-pill { + background: rgba(148, 163, 184, 0.12); + color: var(--text-secondary); + border-color: var(--cb-line); +} + +html[data-cb-theme="dark"] .cb-pager-wrap { + background: var(--cb-panel); + border-top-color: var(--table-row-divider); +} + +html[data-cb-theme="dark"] .cb-pager-nav .page-link { + background: var(--cb-panel); + border-color: var(--cb-line); + color: var(--text-secondary); +} + +html[data-cb-theme="dark"] .cb-pager-nav .page-link:hover { + background: #334155; + color: var(--text-primary); +} + +html[data-cb-theme="dark"] .cb-admin-role-pill { + background: #334155; + color: #f1f5f9; +} + +html[data-cb-theme="dark"] .cb-audit-action-code { + color: #f9a8d4; + background: rgba(157, 23, 77, 0.25); + border-color: rgba(244, 114, 182, 0.35); +} + +html[data-cb-theme="dark"] .cb-theme-tile { + background: var(--cb-panel); +} + +html[data-cb-theme="dark"] .cb-theme-tile:hover { + background: #334155; + border-color: #475569; +} + +html[data-cb-theme="dark"] .cb-theme-tile.active { + background: rgba(26, 86, 219, 0.28); + border-color: #60a5fa; + box-shadow: 0 0 0 1px #60a5fa; +} diff --git a/public/assets/images/logo-mark.svg b/public/assets/images/logo-mark.svg new file mode 100644 index 0000000..8157fa4 --- /dev/null +++ b/public/assets/images/logo-mark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/js/app.js b/public/assets/js/app.js new file mode 100644 index 0000000..b645f79 --- /dev/null +++ b/public/assets/js/app.js @@ -0,0 +1,122 @@ +function applyChartBoardTheme() { + const el = document.documentElement; + const pref = el.getAttribute('data-cb-theme-pref') || 'light'; + let resolved = pref; + if (pref === 'system') { + resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + if (resolved !== 'dark') { + resolved = 'light'; + } + el.setAttribute('data-cb-theme', resolved); + el.setAttribute('data-bs-theme', resolved); + try { + localStorage.setItem('cb-theme-pref', pref); + } catch (e) { + /* ignore */ + } +} + +function showToast(message, type = 'success') { + // Placeholder helper for Phase 1 base setup. + console.log(`[${type}] ${message}`); +} + +document.addEventListener('DOMContentLoaded', function () { + applyChartBoardTheme(); + try { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { + if ((document.documentElement.getAttribute('data-cb-theme-pref') || '') === 'system') { + applyChartBoardTheme(); + } + }); + } catch (e) { + /* ignore */ + } + + const testButtons = document.querySelectorAll('[data-ds-test-btn]'); + testButtons.forEach(function (button) { + button.addEventListener('click', async function () { + const form = button.closest('form'); + if (!form) { + return; + } + + const resultContainer = form.querySelector('[data-ds-test-result]'); + const url = button.getAttribute('data-test-url'); + if (!url) { + return; + } + + const originalLabel = button.innerHTML; + button.disabled = true; + button.innerHTML = 'Testing...'; + + if (resultContainer) { + resultContainer.innerHTML = ''; + } + + try { + const response = await fetch(url, { + method: 'POST', + body: new FormData(form), + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json' + } + }); + + const result = await response.json(); + const isSuccess = !!result.success; + const message = result.message || (isSuccess ? 'Connection successful.' : 'Connection failed.'); + + if (resultContainer) { + resultContainer.innerHTML = '
' + message + '
'; + } + } catch (error) { + if (resultContainer) { + resultContainer.innerHTML = '
Connection test failed due to a network error.
'; + } + } finally { + button.disabled = false; + button.innerHTML = originalLabel; + } + }); + }); + + const proofRefreshButton = document.querySelector('[data-proof-refresh-btn]'); + if (proofRefreshButton) { + proofRefreshButton.addEventListener('click', async function () { + const proofUrl = proofRefreshButton.getAttribute('data-proof-url'); + const proofContainer = document.querySelector('[data-proof-content]'); + if (!proofUrl || !proofContainer) { + return; + } + + const originalLabel = proofRefreshButton.innerHTML; + proofRefreshButton.disabled = true; + proofRefreshButton.innerHTML = 'Refreshing...'; + + try { + const response = await fetch(proofUrl, { + method: 'GET', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json' + } + }); + const result = await response.json(); + if (result && result.success && typeof result.html === 'string') { + proofContainer.innerHTML = result.html; + } else { + showToast(result.message || 'Unable to refresh proof.', 'error'); + } + } catch (error) { + showToast('Unable to refresh proof due to a network error.', 'error'); + } finally { + proofRefreshButton.disabled = false; + proofRefreshButton.innerHTML = originalLabel; + } + }); + } +}); diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7ecfce214cbdbedc15d8348babeff5cd7e720488 GIT binary patch literal 5430 zcmeHL&npB`9Dn5Kuy>cuLGmAPauLohTgugsgTofC9NbV!aa0tgTofgxl%f=?Y?Sh& zWMxI9hibZ5LZ@~9&$;7J-7ai$2=7ByD3A3P1*hfQz|8$LL= zUz~w$;90HjJ=)Ss!V^NuI2Za%(z#lli~7u{+nbyOi;~<-#pGX{K<~tb_C6Lj^YSY9 zuHBEb(bRJ+>$(mjNuamQtlmY!MgdXJjDiJ*`fSvC!*oskp+}sjm(MWyz%r29BW!f*m(x|(w?cOiYFaD0y8pq z9eCfscMhGYa>i@!YdSYJ(-6XZ(W@wc#d9lm1l3S%FF!rC_waiB@V}AM)ez?!Gp$os wgQ-p&yk8A*^lZ5Jw#)Gj@LI_qy{H^P{^jt7C;ag)RHAYMwkN>;6;PJx7hHm1jsO4v literal 0 HcmV?d00001 diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..a0a20db --- /dev/null +++ b/public/index.php @@ -0,0 +1,59 @@ +systemDirectory . '/Boot.php'; + +exit(Boot::bootWeb($paths)); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..9e60f97 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/public/ui-sample-html-asset/PHASE 2/login.html b/public/ui-sample-html-asset/PHASE 2/login.html new file mode 100644 index 0000000..089e674 --- /dev/null +++ b/public/ui-sample-html-asset/PHASE 2/login.html @@ -0,0 +1,204 @@ + + + + + +Chart Board - Secure Login + + + + + + + + + + + + +
+
+Chart Board +
+ +
+
+ +
+
+
+
+ +
+ + + +
+
+

Login

+

Access your secure workspace

+
+
+ +
+ +
+
+mail +
+ +
+
+ +
+
+ +FORGOT? +
+
+
+lock +
+ +
+visibility +
+
+
+ +
+
+ +
+ +
+ +
+ +
+
+ +
+
+
+ +
+
+
+verified_user +Encrypted Session +
+
+Privacy +Terms +
+
+
+ \ No newline at end of file diff --git a/public/ui-sample-html-asset/PHASE 2/register.html b/public/ui-sample-html-asset/PHASE 2/register.html new file mode 100644 index 0000000..9b0a03d --- /dev/null +++ b/public/ui-sample-html-asset/PHASE 2/register.html @@ -0,0 +1,191 @@ + + + + + +Chart Board - Register + + + + + + + + + + +
+
+ +
+
+
+dashboard +
+
+

Chart Board

+

Sentinel Protocol

+
+
+
+

Create Account

+

+ Establish your secure access to the management environment. +

+
+
+ +
+ +
+ +
+ +person +
+
+ +
+ +
+ +alternate_email +
+
+ +
+ +
+ +
+ +lock +
+
+ +
+ +
+ +verified +
+
+
+ +
+policy +

+ By registering, you agree to the Governance Framework and data protection protocols. +

+
+ + +
+ +
+

+ Already have a secure key? + Login to Portal +

+
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/public/ui-sample-html-asset/PHASE 2/reset.html b/public/ui-sample-html-asset/PHASE 2/reset.html new file mode 100644 index 0000000..600cdbf --- /dev/null +++ b/public/ui-sample-html-asset/PHASE 2/reset.html @@ -0,0 +1,172 @@ + + + + + +Chart Board - Forgot Password + + + + + + + + + + + +
+ +
+
+
+
+
+ +
+
+
+lock_reset +
+
+

Chart Board

+
+
+ +
+ +
+
+
+

Reset your access

+

+ Enter the email associated with your identity. We'll verify your credentials and send a secure recovery link to your inbox. +

+
+
+ +
+
+ +
+
+
+mail +
+ + +
+
+ +
+ +
+
+ +
+
+info +
+

Security Verification

+

+ For your protection, recovery links expire in 30 minutes. If you don't see the email, please check your spam folder or contact your system administrator. +

+
+
+ +keyboard_backspace +Back to secure sign in + +
+
+
+ +
+
+
+
+
+
+
+ +
+

+ Β© 2024 Sentinel Identity Management Systems. Built with absolute integrity. +

+
+ \ No newline at end of file diff --git a/public/ui-sample-html-asset/PHASE 3/workspace-module.html b/public/ui-sample-html-asset/PHASE 3/workspace-module.html new file mode 100644 index 0000000..1afb9b2 --- /dev/null +++ b/public/ui-sample-html-asset/PHASE 3/workspace-module.html @@ -0,0 +1,1058 @@ + + + + + +Chart Board | Workspaces + + + + + + + + + +
+
+
+
+ bar_chart +
+ Chart Board +
+ +
+
+ + + +
+ JD +
+
+
+ + + + + +
+ + +
+ +
+
+

Organization

+

Workspaces

+

Organize your data assets, dashboards, and team access across isolated workspace environments.

+
+ +
+ + +
+
+ search + +
+
+ + +
+
+ +
+
+ + +
+
+

Total Workspaces

+

3

+
+
+

Total Members

+

14

+
+
+

Data Sources

+

7

+
+
+

Dashboards

+

22

+
+
+ + +
+ + +
+ +
+
+
+
+ account_tree +
+
+ + Active + + +
+
+

Project Alpha

+

chartboard.io/project-alpha

+

Strategic management workspace for high-level infrastructure planning and asset tracking.

+
+
+
+

68% storage used Β· 3.4 GB / 5 GB

+
+
+
JD
+
SR
+
+5
+
+
+ database 3 sources + bar_chart 12 charts +
+
+
+ EST Β· Refresh 5m + Admin +
+
+
+ + +
+
+
+
+
+ palette +
+
+ + Active + + +
+
+

Design Studio

+

chartboard.io/design-studio

+

Creative analytics for the UX/design team β€” tracking engagement metrics and user flows.

+
+
+
+

31% storage used Β· 1.5 GB / 5 GB

+
+
+
RL
+
KP
+
+2
+
+
+ database 2 sources + bar_chart 7 charts +
+
+
+ PST Β· Refresh 15m + Editor +
+
+
+ + +
+
+
+
+
+ storage +
+
+ + Archived + + +
+
+

Data Ops

+

chartboard.io/data-ops

+

Legacy ETL pipeline monitoring. Archived β€” data retained for 30 days.

+
+
+
+

Purge in 12 days Β· Data retained

+
+
+
MT
+
+
+ database 2 sources + bar_chart 3 charts +
+
+
+ UTC Β· Archived + Viewer +
+
+
+ + +
+
+ add +
+

New Workspace

+

Isolate a new team or project

+
+
+ + + +
+
+ + + + + + + + + + + +
+ + + + + + + diff --git a/spark b/spark new file mode 100755 index 0000000..e7871ea --- /dev/null +++ b/spark @@ -0,0 +1,87 @@ +#!/usr/bin/env php + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use CodeIgniter\Boot; +use Config\Paths; + +/* + * -------------------------------------------------------------------- + * CODEIGNITER COMMAND-LINE TOOLS + * -------------------------------------------------------------------- + * The main entry point into the CLI system and allows you to run + * commands and perform maintenance on your application. + */ + +/* + *--------------------------------------------------------------- + * CHECK SERVER API + *--------------------------------------------------------------- + */ + +// Refuse to run when called from php-cgi +if (str_starts_with(PHP_SAPI, 'cgi')) { + exit("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n"); +} + +/* + *--------------------------------------------------------------- + * CHECK PHP VERSION + *--------------------------------------------------------------- + */ + +$minPhpVersion = '8.1'; // If you update this, don't forget to update `public/index.php`. +if (version_compare(PHP_VERSION, $minPhpVersion, '<')) { + $message = sprintf( + 'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s', + $minPhpVersion, + PHP_VERSION, + ); + + exit($message); +} + +// We want errors to be shown when using it from the CLI. +error_reporting(E_ALL); +ini_set('display_errors', '1'); + +/* + *--------------------------------------------------------------- + * SET THE CURRENT DIRECTORY + *--------------------------------------------------------------- + */ + +// Path to the front controller +define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR); + +// Ensure the current directory is pointing to the front controller's directory +chdir(FCPATH); + +/* + *--------------------------------------------------------------- + * BOOTSTRAP THE APPLICATION + *--------------------------------------------------------------- + * This process sets up the path constants, loads and registers + * our autoloader, along with Composer's, loads our constants + * and fires up an environment-specific bootstrapping. + */ + +// LOAD OUR PATHS CONFIG FILE +// This is the line that might need to be changed, depending on your folder structure. +require FCPATH . '../app/Config/Paths.php'; +// ^^^ Change this line if you move your application folder + +$paths = new Paths(); + +// LOAD THE FRAMEWORK BOOTSTRAP FILE +require $paths->systemDirectory . '/Boot.php'; + +exit(Boot::bootSpark($paths)); diff --git a/tests/.htaccess b/tests/.htaccess new file mode 100644 index 0000000..3462048 --- /dev/null +++ b/tests/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + diff --git a/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php b/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php new file mode 100644 index 0000000..a73356d --- /dev/null +++ b/tests/_support/Database/Migrations/2020-02-22-222222_example_migration.php @@ -0,0 +1,37 @@ +forge->addField('id'); + $this->forge->addField([ + 'name' => ['type' => 'varchar', 'constraint' => 31], + 'uid' => ['type' => 'varchar', 'constraint' => 31], + 'class' => ['type' => 'varchar', 'constraint' => 63], + 'icon' => ['type' => 'varchar', 'constraint' => 31], + 'summary' => ['type' => 'varchar', 'constraint' => 255], + 'created_at' => ['type' => 'datetime', 'null' => true], + 'updated_at' => ['type' => 'datetime', 'null' => true], + 'deleted_at' => ['type' => 'datetime', 'null' => true], + ]); + + $this->forge->addKey('name'); + $this->forge->addKey('uid'); + $this->forge->addKey(['deleted_at', 'id']); + $this->forge->addKey('created_at'); + + $this->forge->createTable('factories'); + } + + public function down(): void + { + $this->forge->dropTable('factories'); + } +} diff --git a/tests/_support/Database/Seeds/ExampleSeeder.php b/tests/_support/Database/Seeds/ExampleSeeder.php new file mode 100644 index 0000000..619fc27 --- /dev/null +++ b/tests/_support/Database/Seeds/ExampleSeeder.php @@ -0,0 +1,41 @@ + 'Test Factory', + 'uid' => 'test001', + 'class' => 'Factories\Tests\NewFactory', + 'icon' => 'fas fa-puzzle-piece', + 'summary' => 'Longer sample text for testing', + ], + [ + 'name' => 'Widget Factory', + 'uid' => 'widget', + 'class' => 'Factories\Tests\WidgetPlant', + 'icon' => 'fas fa-puzzle-piece', + 'summary' => 'Create widgets in your factory', + ], + [ + 'name' => 'Evil Factory', + 'uid' => 'evil-maker', + 'class' => 'Factories\Evil\MyFactory', + 'icon' => 'fas fa-book-dead', + 'summary' => 'Abandon all hope, ye who enter here', + ], + ]; + + $builder = $this->db->table('factories'); + + foreach ($factories as $factory) { + $builder->insert($factory); + } + } +} diff --git a/tests/_support/Libraries/ConfigReader.php b/tests/_support/Libraries/ConfigReader.php new file mode 100644 index 0000000..0bb4a8f --- /dev/null +++ b/tests/_support/Libraries/ConfigReader.php @@ -0,0 +1,19 @@ +findAll(); + + // Make sure the count is as expected + $this->assertCount(3, $objects); + } + + public function testSoftDeleteLeavesRow(): void + { + $model = new ExampleModel(); + $this->setPrivateProperty($model, 'useSoftDeletes', true); + $this->setPrivateProperty($model, 'tempUseSoftDeletes', true); + + /** @var stdClass $object */ + $object = $model->first(); + $model->delete($object->id); + + // The model should no longer find it + $this->assertNull($model->find($object->id)); + + // ... but it should still be in the database + $result = $model->builder()->where('id', $object->id)->get()->getResult(); + + $this->assertCount(1, $result); + } +} diff --git a/tests/index.html b/tests/index.html new file mode 100644 index 0000000..b702fbc --- /dev/null +++ b/tests/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/tests/session/ExampleSessionTest.php b/tests/session/ExampleSessionTest.php new file mode 100644 index 0000000..33242a4 --- /dev/null +++ b/tests/session/ExampleSessionTest.php @@ -0,0 +1,17 @@ +set('logged_in', 123); + $this->assertSame(123, $session->get('logged_in')); + } +} diff --git a/tests/unit/HealthTest.php b/tests/unit/HealthTest.php new file mode 100644 index 0000000..b3e480f --- /dev/null +++ b/tests/unit/HealthTest.php @@ -0,0 +1,49 @@ +assertTrue(defined('APPPATH')); + } + + public function testBaseUrlHasBeenSet(): void + { + $validation = service('validation'); + + $env = false; + + // Check the baseURL in .env + if (is_file(HOMEPATH . '.env')) { + $env = preg_grep('/^app\.baseURL = ./', file(HOMEPATH . '.env')) !== false; + } + + if ($env) { + // BaseURL in .env is a valid URL? + // phpunit.xml.dist sets app.baseURL in $_SERVER + // So if you set app.baseURL in .env, it takes precedence + $config = new App(); + $this->assertTrue( + $validation->check($config->baseURL, 'valid_url'), + 'baseURL "' . $config->baseURL . '" in .env is not valid URL', + ); + } + + // Get the baseURL in app/Config/App.php + // You can't use Config\App, because phpunit.xml.dist sets app.baseURL + $reader = new ConfigReader(); + + // BaseURL in app/Config/App.php is a valid URL? + $this->assertTrue( + $validation->check($reader->baseURL, 'valid_url'), + 'baseURL "' . $reader->baseURL . '" in app/Config/App.php is not valid URL', + ); + } +} diff --git a/writable/.htaccess b/writable/.htaccess new file mode 100755 index 0000000..3462048 --- /dev/null +++ b/writable/.htaccess @@ -0,0 +1,6 @@ + + Require all denied + + + Deny from all + diff --git a/writable/cache/index.html b/writable/cache/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/writable/cache/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/writable/debugbar/index.html b/writable/debugbar/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/writable/debugbar/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/writable/index.html b/writable/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/writable/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/writable/logs/index.html b/writable/logs/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/writable/logs/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/writable/session/index.html b/writable/session/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/writable/session/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + + diff --git a/writable/uploads/index.html b/writable/uploads/index.html new file mode 100755 index 0000000..b702fbc --- /dev/null +++ b/writable/uploads/index.html @@ -0,0 +1,11 @@ + + + + 403 Forbidden + + + +

Directory access is forbidden.

+ + +