From 180fda0296d2f48529e811b569030a43c357ee84 Mon Sep 17 00:00:00 2001 From: Funky Waddle Date: Sun, 20 Sep 2026 00:13:41 -0500 Subject: [PATCH] Mulligan --- .decisions/app_decisions.yaml | 84 + .decisions/architecture_decisions.yaml | 103 + .decisions/auth_requirements_decisions.yaml | 53 + .decisions/components_decisions.yaml | 16 + .decisions/data_model_decisions.yaml | 54 + .decisions/deployment_decisions.yaml | 16 + .decisions/directory_structure_decisions.yaml | 15 + .decisions/features_overview_decisions.yaml | 27 + .../frontend_architecture_decisions.yaml | 38 + .decisions/glossary_decisions.yaml | 1 + .../media_manager_architecture_decisions.yaml | 21 + .decisions/performance_decisions.yaml | 15 + .decisions/plugin_architecture_decisions.yaml | 58 + .decisions/security_decisions.yaml | 13 + .decisions/tech_stack_decisions.yaml | 27 + .decisions/testing_decisions.yaml | 7 + .decisions/themes_architecture_decisions.yaml | 39 + .decisions/ui_decisions.yaml | 15 + AGENTS.md | 10 +- APP.yaml | 1410 +-- MILESTONES.yaml | 394 - _bootstrap.php | 11 - codeception.yml | 17 - composer.json | 53 - composer.lock | 10543 ---------------- config/.env.example | 13 - config/bootstrap.php | 39 - database/migrations/20260904071635_users.php | 50 - .../20260904080444_site_settings.php | 42 - database/migrations/20260905005507_roles.php | 43 - .../migrations/20260905050041_user_totp.php | 40 - .../20260905051452_user_password_reset.php | 40 - ...20260905231800_user_email_verification.php | 40 - .../migrations/20260906020100_permissions.php | 45 - .../20260906020200_role_permission.php | 44 - .../migrations/20260906020300_user_role.php | 44 - database/migrations/20260906020400_pages.php | 44 - .../20260906020500_page_versions.php | 45 - .../20260906020600_blog_settings.php | 42 - .../migrations/20260906020700_blog_posts.php | 44 - .../20260906020800_blog_post_versions.php | 46 - .../20260906020900_blog_categories.php | 39 - .../migrations/20260906021000_blog_tags.php | 37 - .../20260906021100_blog_post_categories.php | 44 - .../20260906021200_blog_post_tags.php | 44 - .../20260906021300_blog_comments.php | 44 - database/migrations/20260906021400_media.php | 46 - .../20260906021500_media_metadata.php | 45 - .../migrations/20260906021600_plugins.php | 49 - .../20260906021700_frontend_themes.php | 42 - .../20260906021800_admin_themes.php | 42 - ...260906030000_pages_add_builder_columns.php | 40 - .../20260906030100_i18n_strings.php | 43 - ...40000_media_add_folder_and_focal_point.php | 43 - .../20260906040100_plugin_migrations.php | 42 - .../20260906040200_theme_file_versions.php | 46 - .../20260906050000_pages_add_is_home.php | 43 - database/seeds/BlogCategoriesSeed.php | 37 - database/seeds/DefaultThemesSeed.php | 52 - database/seeds/RolesAndPermissionsSeed.php | 240 - public/index.php | 28 - public/maintenance.php | 57 - src/Site/Core/Config.php | 61 - src/Site/Core/Container.php | 38 - src/Site/Core/Middleware/AuthMiddleware.php | 78 - src/Site/Core/Models/Permission.php | 37 - src/Site/Core/Models/Role.php | 60 - src/Site/Core/Models/User.php | 121 - .../Core/Providers/AuthServiceProvider.php | 58 - .../Core/Providers/ConfigServiceProvider.php | 71 - .../Providers/DatabaseServiceProvider.php | 57 - .../PermissionRepositoryServiceProvider.php | 27 - .../RoleRepositoryServiceProvider.php | 27 - .../UserRepositoryServiceProvider.php | 32 - .../Core/Providers/RouterServiceProvider.php | 30 - .../Core/Providers/SessionServiceProvider.php | 32 - .../Core/Providers/TOTPServiceProvider.php | 31 - .../Repositories/PermissionRepository.php | 66 - src/Site/Core/Repositories/RoleRepository.php | 66 - src/Site/Core/Repositories/UserRepository.php | 92 - src/Site/Core/Router.php | 112 - src/Site/Core/ServiceProvider.php | 37 - src/Site/Core/Services/AuthService.php | 134 - src/Site/Core/Services/SessionService.php | 72 - src/Site/Core/Services/TOTPService.php | 73 - tests/_bootstrap.php | 11 - tests/_support/UnitTester.php | 35 - .../UnitTesterActions [conflicted].php | 2073 --- .../_support/_generated/UnitTesterActions.php | 2073 --- .../Core/AuthMiddlewareRolePermissionTest.php | 105 - tests/unit/Core/AuthServiceTest.php | 128 - tests/unit/Core/ConfigTest.php | 82 - tests/unit/Core/RouterTest.php | 42 - tests/unit/Core/ServiceProviderTest.php | 40 - tests/unit/Core/SessionServiceTest.php | 47 - tests/unit/Core/TOTPServiceTest.php | 100 - tests/unit/UnitTester.php | 16 - 97 files changed, 1140 insertions(+), 19828 deletions(-) create mode 100644 .decisions/app_decisions.yaml create mode 100644 .decisions/architecture_decisions.yaml create mode 100644 .decisions/auth_requirements_decisions.yaml create mode 100644 .decisions/components_decisions.yaml create mode 100644 .decisions/data_model_decisions.yaml create mode 100644 .decisions/deployment_decisions.yaml create mode 100644 .decisions/directory_structure_decisions.yaml create mode 100644 .decisions/features_overview_decisions.yaml create mode 100644 .decisions/frontend_architecture_decisions.yaml create mode 100644 .decisions/glossary_decisions.yaml create mode 100644 .decisions/media_manager_architecture_decisions.yaml create mode 100644 .decisions/performance_decisions.yaml create mode 100644 .decisions/plugin_architecture_decisions.yaml create mode 100644 .decisions/security_decisions.yaml create mode 100644 .decisions/tech_stack_decisions.yaml create mode 100644 .decisions/testing_decisions.yaml create mode 100644 .decisions/themes_architecture_decisions.yaml create mode 100644 .decisions/ui_decisions.yaml delete mode 100644 MILESTONES.yaml delete mode 100644 _bootstrap.php delete mode 100644 codeception.yml delete mode 100644 composer.json delete mode 100644 composer.lock delete mode 100644 config/.env.example delete mode 100644 config/bootstrap.php delete mode 100644 database/migrations/20260904071635_users.php delete mode 100644 database/migrations/20260904080444_site_settings.php delete mode 100644 database/migrations/20260905005507_roles.php delete mode 100644 database/migrations/20260905050041_user_totp.php delete mode 100644 database/migrations/20260905051452_user_password_reset.php delete mode 100644 database/migrations/20260905231800_user_email_verification.php delete mode 100644 database/migrations/20260906020100_permissions.php delete mode 100644 database/migrations/20260906020200_role_permission.php delete mode 100644 database/migrations/20260906020300_user_role.php delete mode 100644 database/migrations/20260906020400_pages.php delete mode 100644 database/migrations/20260906020500_page_versions.php delete mode 100644 database/migrations/20260906020600_blog_settings.php delete mode 100644 database/migrations/20260906020700_blog_posts.php delete mode 100644 database/migrations/20260906020800_blog_post_versions.php delete mode 100644 database/migrations/20260906020900_blog_categories.php delete mode 100644 database/migrations/20260906021000_blog_tags.php delete mode 100644 database/migrations/20260906021100_blog_post_categories.php delete mode 100644 database/migrations/20260906021200_blog_post_tags.php delete mode 100644 database/migrations/20260906021300_blog_comments.php delete mode 100644 database/migrations/20260906021400_media.php delete mode 100644 database/migrations/20260906021500_media_metadata.php delete mode 100644 database/migrations/20260906021600_plugins.php delete mode 100644 database/migrations/20260906021700_frontend_themes.php delete mode 100644 database/migrations/20260906021800_admin_themes.php delete mode 100644 database/migrations/20260906030000_pages_add_builder_columns.php delete mode 100644 database/migrations/20260906030100_i18n_strings.php delete mode 100644 database/migrations/20260906040000_media_add_folder_and_focal_point.php delete mode 100644 database/migrations/20260906040100_plugin_migrations.php delete mode 100644 database/migrations/20260906040200_theme_file_versions.php delete mode 100644 database/migrations/20260906050000_pages_add_is_home.php delete mode 100644 database/seeds/BlogCategoriesSeed.php delete mode 100644 database/seeds/DefaultThemesSeed.php delete mode 100644 database/seeds/RolesAndPermissionsSeed.php delete mode 100644 public/index.php delete mode 100644 public/maintenance.php delete mode 100644 src/Site/Core/Config.php delete mode 100644 src/Site/Core/Container.php delete mode 100644 src/Site/Core/Middleware/AuthMiddleware.php delete mode 100644 src/Site/Core/Models/Permission.php delete mode 100644 src/Site/Core/Models/Role.php delete mode 100644 src/Site/Core/Models/User.php delete mode 100644 src/Site/Core/Providers/AuthServiceProvider.php delete mode 100644 src/Site/Core/Providers/ConfigServiceProvider.php delete mode 100644 src/Site/Core/Providers/DatabaseServiceProvider.php delete mode 100644 src/Site/Core/Providers/Repositories/PermissionRepositoryServiceProvider.php delete mode 100644 src/Site/Core/Providers/Repositories/RoleRepositoryServiceProvider.php delete mode 100644 src/Site/Core/Providers/Repositories/UserRepositoryServiceProvider.php delete mode 100644 src/Site/Core/Providers/RouterServiceProvider.php delete mode 100644 src/Site/Core/Providers/SessionServiceProvider.php delete mode 100644 src/Site/Core/Providers/TOTPServiceProvider.php delete mode 100644 src/Site/Core/Repositories/PermissionRepository.php delete mode 100644 src/Site/Core/Repositories/RoleRepository.php delete mode 100644 src/Site/Core/Repositories/UserRepository.php delete mode 100644 src/Site/Core/Router.php delete mode 100644 src/Site/Core/ServiceProvider.php delete mode 100644 src/Site/Core/Services/AuthService.php delete mode 100644 src/Site/Core/Services/SessionService.php delete mode 100644 src/Site/Core/Services/TOTPService.php delete mode 100644 tests/_bootstrap.php delete mode 100644 tests/_support/UnitTester.php delete mode 100644 tests/_support/_generated/UnitTesterActions [conflicted].php delete mode 100644 tests/_support/_generated/UnitTesterActions.php delete mode 100644 tests/unit/Core/AuthMiddlewareRolePermissionTest.php delete mode 100644 tests/unit/Core/AuthServiceTest.php delete mode 100644 tests/unit/Core/ConfigTest.php delete mode 100644 tests/unit/Core/RouterTest.php delete mode 100644 tests/unit/Core/ServiceProviderTest.php delete mode 100644 tests/unit/Core/SessionServiceTest.php delete mode 100644 tests/unit/Core/TOTPServiceTest.php delete mode 100644 tests/unit/UnitTester.php diff --git a/.decisions/app_decisions.yaml b/.decisions/app_decisions.yaml new file mode 100644 index 0000000..3babdf9 --- /dev/null +++ b/.decisions/app_decisions.yaml @@ -0,0 +1,84 @@ +decisions_log: + - question: What language and minimum version should the app target? + decision: PHP 8.4+ + rationale: > + 8.4 is as far back in PHP versions as we want to support; targeting a recent + minimum keeps the codebase modern instead of carrying compatibility baggage + for older, less-supported PHP versions. + - question: Should the backend use an existing framework (e.g. Laravel, Symfony) or a custom/home-brewed codebase? + decision: Laravel 13.x + rationale: > + Leveraging Laravel 13.x provides robust routing, authentication, service container, + and ecosystem packages that accelerate development and reduce maintenance burden. + - question: Which CSS framework should the UI be built on? + decision: Fomantic UI + rationale: > + Fomantic UI is an actively maintained fork of Semantic UI (which is no longer + maintained), and semantic class names are strongly preferred over + structural/utility class names for CSS. + - question: Which JavaScript approach/framework should the frontend use? + decision: > + Vue — but used specifically via its Components feature only (similar in spirit + to how Svelte components are used), not as a full-fledged SPA framework. + rationale: > + General preference for Vue over React or Angular. Svelte would also have been an + acceptable choice for a components-only approach, but Vue was the one picked. + - question: Which tool should manage PHP dependencies? + decision: Composer + rationale: Composer is the de facto standard package manager for PHP. + - question: > + Is SiteWeaverCMS intended to be self-hosted only for v1, or should the + "Multi-tenancy ready" note under `architecture` mean a hosted/SaaS offering + is also on the roadmap? + decision: > + Self-hosted only. Multi-tenancy has been removed from consideration entirely + (see `architecture.decisions_log`), so there is no hosted/SaaS offering on the + roadmap and no tenant-isolation/billing concerns to capture. + rationale: > + Once multi-tenancy was ruled out as an architectural direction, a hosted/SaaS + offering (which would require it) is ruled out along with it. + - question: > + When the "Automatic Basic Security Audits for Plugins" check fails (eval/exec/ + remote code execution detected), what happens — is the ZIP upload rejected + outright, or is the plugin installed with an "Install - Error" status (as + implied by the Plugins List Page in `plugins_architecture`) for an Admin to + review? + decision: > + The upload is rejected outright, not installed with an "Install - Error" status. + Flow: the ZIP is unzipped, files are analyzed; if no security issues are found, + the plugin's migrations run and it is marked installed; if security issues are + found, the unzipped plugin files are immediately deleted and a + PluginSecurityException is thrown. The same audit and rejection flow applies to + Themes (throwing a ThemeSecurityException instead) — see + `plugins_architecture.decisions_log` and `themes_architecture.decisions_log`. + rationale: > + Files must be unzipped before they can be scanned, so a full "Install - Error" + list entry is unnecessary — a failed audit is treated as a hard rejection with + no trace left on disk. + - question: How will dependency injection be implemented in the app? + decision: > + Laravel's built-in service container will be used, following the framework’s + service provider pattern. This provides robust DI, lazy loading, and integration + with Eloquent, routing, and other core services. + rationale: > + Leveraging Laravel's container eliminates the need for a custom DI solution, + reduces boilerplate, and aligns with the ecosystem's conventions. + - question: What license (if any) will SiteWeaverCMS be released under (e.g. MIT, GPL, proprietary)? + decision: > + No public license for now — SiteWeaverCMS stays sole-ownership/proprietary. It + is being used to build client websites, with the clients themselves administering + the sites built for them; a public release/license may be revisited later. + rationale: > + Matches the current usage model (agency-built client sites) rather than a + publicly redistributable product, so there is no need to pick a license yet. + - question: > + Should a target audience/deployment size be stated up front, since it + influences caching, storage backend, and search decisions made later in the + spec? + decision: > + Yes — the initial target audience is self-employed business owners, small + craft makers, and small new bloggers, with room to grow toward larger sites + and capabilities over time. + rationale: > + Gives later caching/storage/search decisions a concrete initial scale to design + against, while leaving room to grow. \ No newline at end of file diff --git a/.decisions/architecture_decisions.yaml b/.decisions/architecture_decisions.yaml new file mode 100644 index 0000000..e870fe1 --- /dev/null +++ b/.decisions/architecture_decisions.yaml @@ -0,0 +1,103 @@ +decisions_log: + - question: > + Is a dedicated backend_architecture section needed (per the "e.g." placeholder + comment above)? + decision: > + No — a separate backend_architecture section is not needed. Backend detail is + already captured across the domain-specific sections (media_manager_architecture, + plugins_architecture, themes_architecture, auth_requirements), which together + provide sufficient depth on the backend. + rationale: > + Those sections already document the relevant backend concerns for their domain + (storage abstraction, migrations, Service Providers, permissions, etc.), so a + separate umbrella backend_architecture section would just duplicate them. + - question: > + Is a dedicated frontend_architecture section still needed to document how Vue + Components are organized/bundled, and how they integrate with Fomantic UI and + the templating engine? + decision: > + Yes — a `frontend_architecture` section has been added (see below), filled in + with what could be gathered from the rest of this document, with its own + `open_questions` for what couldn't be inferred. + rationale: > + Frontend integration (Vue Components + Fomantic UI + Laravel Blade templates) is enough of its own concern to warrant a dedicated section rather than + being folded into `architecture` or `ui`. + - question: > + Given the previous "Multi-tenancy ready" note, should v1's schema/routing + already carry a tenant identifier (even if unused), or should multi-tenancy be + deferred entirely to a future major version with its own migration path? + decision: > + Multi-tenancy has been removed from consideration altogether — not deferred, + not kept "ready for" — SiteWeaverCMS is a single-site, single-tenant CMS with + no planned migration path toward multi-tenancy. The "Multi-tenancy ready" note + has been removed from `notes` above, and the "Multi-tenancy" glossary term has + been removed. + rationale: > + Avoids carrying unused tenant-identifier columns/routing complexity for a + direction the project will not pursue. + - question: How will plugin and theme loading be handled? + decision: > + Plugins and themes will be loaded through service providers that register their + respective services. The plugin manager and theme manager will handle the + discovery, loading, and unloading of these components at runtime. + rationale: > + Using service providers ensures proper dependency injection for plugins and + themes, while also allowing for lazy loading and easy testing of individual + components. + - question: What approach will be used for routing? + decision: > + Laravel's routing system will handle all application and plugin routes. Plugins + can define their own route files or use Route::group to register routes, leveraging + middleware, named routes, and resource controllers provided by the framework. + rationale: > + Using Laravel’s built-in router provides a well-tested, feature-rich routing + layer that supports middleware, route caching, and easy integration with + authentication guards and API resources. + - question: How will configuration be managed? + decision: > + Configuration will be handled through a service that loads from environment + variables and .env files (if available), with support for runtime updates. + rationale: > + Using environment variables for configuration ensures secure handling of + sensitive data while allowing for easy deployment to different environments. + - question: How will services be registered? + decision: > + Services will be registered using Laravel’s service provider pattern. Core + services go into core service providers, application-level services in + AppServiceProvider, and plugin/theme-specific services into their own + service providers. + rationale: > + This follows Laravel conventions, enabling lazy loading, caching of + bindings, and integration with the framework's lifecycle events. + - question: How will caching be implemented? + decision: > + A combination of in-memory caching (for frequently accessed data) and file-based + caching (for larger content that doesn't need to be loaded on every request). + rationale: > + This hybrid approach ensures good performance for both frequently accessed + data and larger content while maintaining a reasonable memory footprint. + - question: How will security be implemented? + decision: > + Security will be implemented through a combination of input validation, + authentication, authorization, and secure storage practices. Plugins will be + subject to automated security audits during installation. + rationale: > + A layered security approach ensures that multiple attack vectors are covered, + with special attention to preventing code injection and unauthorized access. + - question: > + `in_scope` and `out_of_scope` were both still empty. What is explicitly IN scope + for v1 and what is explicitly OUT of scope for v1? + decision: > + `in_scope` and `out_of_scope` have been populated (see lists above): Page + Builder, Role Based Auth, User Management, Blog plugin, Media Manager, + Theme/Plugin Editor, i18n, Comments, Backup/Restore, and Search are in scope + for v1; multi-tenant/SaaS deployment, a plugin marketplace, and native mobile + apps are out of scope. + rationale: > + Gives a concrete, at-a-glance v1 boundary to plan milestones against instead of + inferring scope from `features_overview.notes`. + - question: What backup strategy is supported in v1? + decision: Manual trigger only via Admin UI, no automatic or scheduled backups. + rationale: > + The feature notes specify that backups are not automated; they must be triggered + manually by an admin action. \ No newline at end of file diff --git a/.decisions/auth_requirements_decisions.yaml b/.decisions/auth_requirements_decisions.yaml new file mode 100644 index 0000000..75b2263 --- /dev/null +++ b/.decisions/auth_requirements_decisions.yaml @@ -0,0 +1,53 @@ +decisions_log: + - question: What is the password-reset/forgot-password flow (self-service email link, admin-reset only, or both)? + decision: > + A logged-in user changes their own password from their Profile page (see + "Password Reset" note above); this handles a known/current password change. + rationale: > + Reuses the Profile Management capability already planned for backend users + instead of building a separate flow for a password change. + - question: > + Is there a login rate-limiting/lockout policy (e.g. temporary lockout after N + failed attempts) to complement 2FA? + decision: > + Yes — a temporary 2-hour lockout after 3 failed login attempts, which an Admin + can manually override/unlock (see "Login Lockout Policy" note above). + rationale: > + Slows down brute-force login attempts while giving the Admin an escape hatch + for a legitimately locked-out user. + - question: > + Should the public-facing User (commenter) role require email verification before + their comments are accepted/shown? + decision: > + Email verification is required after account creation, but is not a + precondition for submitting a comment (see "Email Verification" note above). + rationale: > + Confirms the commenter's email address without adding friction/delay to the + act of leaving a comment itself. + - question: Can a site have more than 1 Admin account? + decision: > + Yes — a site can have more than one Admin account (see "Multiple Admin + Accounts" note above). + rationale: > + Matches the intended usage model where the developer holds an Admin account on + every CMS instance alongside a separate Admin account created per client. + - question: > + The Profile-page password change requires being logged in already, so it + doesn't cover a user who has forgotten their password and is locked out. Is a + true "forgot password" recovery path needed (e.g. a self-service emailed + reset link) for that case, or is recovery always Admin-assisted (an Admin + manually resets/unlocks the account)? + decision: > + Yes, a self-service Forgot Password recovery path is needed. It works via an + emailed, tokenized reset link that is valid for 1 hour; the user follows the + link to set a new password directly. Successfully completing a reset also + clears/unlocks any active Login Lockout on that account (see "Login Lockout + Policy" above), since proving ownership of the account's email is treated as + sufficient to lift the lockout. + rationale: > + Covers the case the logged-in Profile-page password change can't handle (a + forgotten password while locked out), without always requiring Admin + involvement. A 1-hour token expiration limits the exposure window for a + leaked/intercepted reset link, and clearing the lockout on successful reset + avoids forcing a legitimate user to wait out the full 2-hour lockout after + already proving account ownership via email. \ No newline at end of file diff --git a/.decisions/components_decisions.yaml b/.decisions/components_decisions.yaml new file mode 100644 index 0000000..88501cf --- /dev/null +++ b/.decisions/components_decisions.yaml @@ -0,0 +1,16 @@ +decisions_log: + - question: How will core components be organized? + decision: > + Core components will be organized under the src/Site/ directory with clear + separation between bootstrap code, router logic, services, and other core + functionality. + rationale: > + This organization provides a clean separation of concerns and makes it easy + to locate specific components within the codebase. + - question: How will service components be structured? + decision: > + Service components will be organized under src/Site/Services/ with each + service having its own class implementing a clear interface. + rationale: > + This structure promotes loose coupling and makes it easy to swap out + implementations for testing or alternative backends. \ No newline at end of file diff --git a/.decisions/data_model_decisions.yaml b/.decisions/data_model_decisions.yaml new file mode 100644 index 0000000..5ce886d --- /dev/null +++ b/.decisions/data_model_decisions.yaml @@ -0,0 +1,54 @@ +decisions_log: + - question: > + `notes` here only cover Settings; no entities/columns have been sketched yet for + Users, Roles, Permissions, Pages, Posts, Media, Comments, or Content Versions + (History). Should the core schema be drafted in this section before + MILESTONES.yaml is written, given how many features depend on it? + decision: > + Yes — the core schema has been drafted above (`Database Tables`), covering + Settings, Users, Roles, Permissions, Role_Permission, User_Role, Pages, + Page_Versions, Posts, Post_Versions, Media, and Comments. + rationale: > + Gives MILESTONES.yaml concrete table/column definitions to build migrations + from, instead of inferring the schema from feature descriptions alone. + - question: > + Should a cross-cutting convention for audit/timestamp columns (`created_at`/ + `updated_at`, and who created/last edited a record) be documented once entities + are sketched out, so every migration follows the same pattern? + decision: > + Partially applied so far: Pages, Page_Versions, Posts, Post_Versions, and + Comments already follow a `created_at`/`created_by`/`updated_at`/`updated_by` + pattern. Users, Roles, Permissions, Role_Permission, User_Role, and Media do + not yet have any `created_at`/`updated_at` columns (see the new suggestion + below flagging this gap). + rationale: > + Records the convention actually in use today, rather than assuming it was + applied uniformly across every table. + - question: > + Should the suggested `created_at`/`created_by`/`updated_at`/`updated_by` + columns be added to Users, Roles, Permissions, Role_Permission, User_Role, and + Media (see `suggestions` below)? + decision: > + Yes, accepted and applied — Users, Roles, Permissions, Role_Permission, and + User_Role already carried the columns above; Media has now also been given the + same `created_at`/`created_by`/`updated_at`/`updated_by` columns, so every + table in the schema now follows the convention. `User_Totp`, `User_password_reset`, + and `User_email_verification` are purposely missing the `created_by` and `updated_by` + fields, as these are system generated and not tied to specific users. + rationale: > + Closes the last gap (Media) so audit/timestamp tracking is consistent across + every table, matching the convention already used on Pages/Page_Versions/ + Posts/Post_Versions/Comments. + - question: What database engine will be used? + decision: > + Support for MySQL/MariaDB through PDO connections. + rationale: > + Supporting multiple engines provides flexibility for different deployment + scenarios while maintaining a consistent interface through PDO. + - question: How will data migrations be handled? + decision: > + Data migrations will be implemented using Laravel Eloquent migrations, with each plugin having + its own migration files. + rationale: > + This approach allows plugins to manage their own database schema changes + while providing a centralized migration management system. \ No newline at end of file diff --git a/.decisions/deployment_decisions.yaml b/.decisions/deployment_decisions.yaml new file mode 100644 index 0000000..f333ca6 --- /dev/null +++ b/.decisions/deployment_decisions.yaml @@ -0,0 +1,16 @@ +decisions_log: + - question: What are the deployment requirements? + decision: > + The application requires PHP 8.4+, Composer, and a web server (Apache/Nginx). + Database configuration will be handled through environment variables. + rationale: > + These requirements ensure compatibility with modern PHP features while + providing flexibility in hosting environments. + - question: How will environment-specific configuration be handled? + decision: > + In Dev environment, there will be a .env file with credentials for a locally hosted Infisical instance. + In Prod environment, the Infisical credentials will come from Nginx `fastcgi_param` parameters. + All other needed environment variables (Database credentials, maintenance setting, etc) will come from the Infisical instance + rationale: > + This approach allows for easy deployment to different environments while + keeping sensitive data out of version control. \ No newline at end of file diff --git a/.decisions/directory_structure_decisions.yaml b/.decisions/directory_structure_decisions.yaml new file mode 100644 index 0000000..9eae509 --- /dev/null +++ b/.decisions/directory_structure_decisions.yaml @@ -0,0 +1,15 @@ +decisions_log: + - question: > + No PSR-4 (or equivalent) autoloading/namespace convention has been documented + for `app/`, `site/plugins/*`, and `site/themes/*`. Should that be captured here + before MILESTONES.yaml is written? + decision: > + Yes. PSR-4 namespace mapping: + - `app` => `SiteWeaver\Core` + - `site/plugins` => `SiteWeaver\Plugins` + - `site/themes` => `SiteWeaver\Themes` + - `site/admin_themes` => `SiteWeaver\AdminThemes` + - `tests` => `SiteWeaver\Tests` + rationale: > + Gives every directory tree a predictable, standard PSR-4 namespace so + Composer autoloading and MILESTONES.yaml file paths can be derived consistently. \ No newline at end of file diff --git a/.decisions/features_overview_decisions.yaml b/.decisions/features_overview_decisions.yaml new file mode 100644 index 0000000..32d1e6d --- /dev/null +++ b/.decisions/features_overview_decisions.yaml @@ -0,0 +1,27 @@ +decisions_log: + - question: How should plugins be installable by end users? + decision: > + Plugins are installable via ZIP file upload to the system. + rationale: > + ZIP upload keeps module installation accessible to admins without CLI or + Composer access. + - question: > + The `features` list itself was empty while all the actual feature content lived + in `notes`. Should `features` be populated with a concise catalog for + at-a-glance scanning? + decision: > + Yes — `features` has been populated. User Management and Role Based Auth + Management were also added to the list above, since they were documented in + `app.notes`/`auth_requirements`/`scope.in_scope` but missing from this catalog. + rationale: > + Keeps `features` a complete, scannable catalog rather than a partial list that + only covers items that happened to also get `notes`. + - question: > + Akismet was the only spam-protection measure named for the Comments System; + should a fallback be added in case Akismet is unavailable or insufficient? + decision: > + Yes — honeypot and rate-limiting have been added alongside Akismet (see + `notes.Comments System` above). + rationale: > + Layers a bot-trap and a submission-frequency cap on top of the third-party + Akismet service so spam protection doesn't have a single point of failure. \ No newline at end of file diff --git a/.decisions/frontend_architecture_decisions.yaml b/.decisions/frontend_architecture_decisions.yaml new file mode 100644 index 0000000..6d67028 --- /dev/null +++ b/.decisions/frontend_architecture_decisions.yaml @@ -0,0 +1,38 @@ +decisions_log: + - question: > + Which build tool bundles the Vue Components and produces the `public/build` + output (e.g. Vite, Webpack, esbuild)? + decision: Vite + rationale: > + Chosen as the build tool despite being new to the author, as a deliberate + opportunity to learn it. + - question: > + How are Vue components mounted into PHP-rendered templates, and how is data + passed from PHP to a mounted component? + decision: > + Components are referenced directly in server-rendered markup as a custom HTML + element tag matching the component name (e.g. ``); data is passed in as HTML attributes, which Vue exposes to the + component's script block as props. + rationale: > + Matches Vue's own documented Single-File Component usage pattern, keeping the + PHP-side markup declarative rather than needing a separate bootstrap script to + wire up mount points and data. + - question: > + Is there a single global Vue application instance mounting multiple components + per page, or does each interactive widget get its own independent Vue instance? + decision: Each interactive widget gets its own independent Vue instance. + rationale: > + Keeps each widget (Theme/Plugin Editor, Focal Point Selector, Faceted Search + filter panel, etc.) self-contained rather than coordinated through one shared + page-level app instance. + - question: > + Will Vue Components be authored as Single-File Components (`.vue` files needing + a build step) or as plain JS component definitions, and where do their source + files live? + decision: > + Single-File Components (`.vue` files), living under `assets/js/components` + within each theme. + rationale: > + Keeps component source files colocated with the theme that uses them, following + Vue's standard SFC authoring approach. \ No newline at end of file diff --git a/.decisions/glossary_decisions.yaml b/.decisions/glossary_decisions.yaml new file mode 100644 index 0000000..8d4adea --- /dev/null +++ b/.decisions/glossary_decisions.yaml @@ -0,0 +1 @@ +decisions_log: [] \ No newline at end of file diff --git a/.decisions/media_manager_architecture_decisions.yaml b/.decisions/media_manager_architecture_decisions.yaml new file mode 100644 index 0000000..df539cd --- /dev/null +++ b/.decisions/media_manager_architecture_decisions.yaml @@ -0,0 +1,21 @@ +decisions_log: + - question: > + Which image library backs JIT Generation, Glide or Intervention Image? (Same + question as `tech_stack`; flagging here too since this is where the + "Glide/Intervention" note actually lived.) + decision: Glide + rationale: > + Same decision/rationale as logged in `tech_stack.decisions_log`; kept in sync + across both sections. + - question: > + The glossary's "Storage Interface / Storage Abstraction" term lists GCP as an + example backend, but this section's "Storage Abstractions" list only names Local + Disk, S3, Nextcloud, and Linode Object-Storage. Should GCP be added to the + official supported-backends list here, or removed from the glossary example to + keep the two in sync? + decision: > + GCP has been removed from the glossary example (see `glossary`); it will not be + a supported storage backend at this time. + rationale: > + Keeps the glossary example list in sync with the actual supported backends + named here, rather than implying GCP support that doesn't exist. \ No newline at end of file diff --git a/.decisions/performance_decisions.yaml b/.decisions/performance_decisions.yaml new file mode 100644 index 0000000..f5265e2 --- /dev/null +++ b/.decisions/performance_decisions.yaml @@ -0,0 +1,15 @@ +decisions_log: + - question: How will caching be implemented? + decision: > + A hybrid caching approach using in-memory caches for frequently accessed data + and file-based caching for larger content. + rationale: > + This approach balances memory usage with performance while maintaining + reasonable disk space requirements. + - question: How will database queries be optimized? + decision: > + Database queries will use prepared statements, proper indexing, and lazy loading + of related data to reduce unnecessary overhead. + rationale: > + These practices ensure efficient database operations while maintaining + security against SQL injection attacks. \ No newline at end of file diff --git a/.decisions/plugin_architecture_decisions.yaml b/.decisions/plugin_architecture_decisions.yaml new file mode 100644 index 0000000..8a290ca --- /dev/null +++ b/.decisions/plugin_architecture_decisions.yaml @@ -0,0 +1,58 @@ +decisions_log: + - question: Should Composer-based installation of plugins be supported alongside ZIP upload? + decision: > + No — Composer installation of plugins has been removed, at least for v1. The + only installation process now is ZIP file upload. + rationale: > + Keeps a single, admin-friendly install path consistent with the "even a non-tech + person finds inviting" goal in `app`; CLI/Composer access isn't assumed to be + available to site admins. + - question: > + What is the resolution path when the Automatic Basic Security Audit rejects a + plugin — is the upload blocked outright, or installed with an "Install - Error" + status (as implied by the Plugins List Page) for the Admin to inspect further? + decision: > + The upload is blocked outright. Since the upload has to finish before the + system can unzip and inspect its contents, the flow is: the ZIP is unzipped, + files are analyzed; if no security issues are found, the plugin's migration + files run and the plugin is marked installed; if security issues are found, + the unzipped plugin files are immediately deleted and a PluginSecurityException + is thrown. There is no "Install - Error" status resulting from a failed + security audit — that status is reserved for other install-time errors (e.g. a + migration failure). + rationale: > + A plugin that fails the security audit should leave nothing installed or + listed; only errors unrelated to security (e.g. a broken migration) warrant + surfacing an "Install - Error" entry for the Admin to inspect. + - question: > + Is there a plugin dependency/versioning mechanism (e.g. a required core version, + or dependencies between plugins) that needs to be declared in `plugin.md`? + decision: > + Yes — a Core Version dependency is declared in `plugin.md` (added to its + required metadata list in `directory_structure`), mirroring the same field + already present in `theme.md`/`admin.theme.md`. + rationale: > + Lets the install process reject a plugin that requires a newer core version + than what's installed, consistent with how themes already declare a Core + Version. + - question: What approach will be used for plugin installation? + decision: > + Plugins will be installed as ZIP files that are extracted to a plugins directory, + with the plugin's service provider registered automatically. + rationale: > + This approach provides a simple and familiar installation process for users + while ensuring proper service registration through the DI container. + - question: How will plugin dependencies be handled? + decision: > + Plugins can declare their dependencies in their metadata files. The plugin + manager will handle loading plugins in the correct order based on dependencies. + rationale: > + This ensures that required services are available when a plugin is loaded, + preventing runtime errors. + - question: How will plugin configuration be handled? + decision: > + Each plugin can register its own configuration settings, which will be + automatically merged with the main configuration system. + rationale: > + This allows plugins to maintain their own settings while integrating cleanly + with the core configuration system. \ No newline at end of file diff --git a/.decisions/security_decisions.yaml b/.decisions/security_decisions.yaml new file mode 100644 index 0000000..8e3b367 --- /dev/null +++ b/.decisions/security_decisions.yaml @@ -0,0 +1,13 @@ +decisions_log: + - question: What authentication method will be used? + decision: > + Role-based authentication with support for TOTP-based 2FA. + rationale: > + This provides a good balance between security and usability while supporting + enterprise-level security requirements. + - question: How will user permissions be handled? + decision: > + A granular permission system based on roles and capabilities. + rationale: > + This allows for precise control over what actions users can perform while + maintaining a simple and understandable permission model. \ No newline at end of file diff --git a/.decisions/tech_stack_decisions.yaml b/.decisions/tech_stack_decisions.yaml new file mode 100644 index 0000000..db4fb0b --- /dev/null +++ b/.decisions/tech_stack_decisions.yaml @@ -0,0 +1,27 @@ +decisions_log: + - question: > + Which image processing library will back JIT Generation — Glide or Intervention + Image? + decision: Glide + rationale: > + Chosen over Intervention Image as the JIT-generation backend (see + `media_manager_architecture.decisions_log` for the same decision applied there). + - question: > + Should Composer dependency versions be pinned to exact versions, or use caret/ + tilde ranges, given the "no compatibility baggage" stance already taken on the + minimum PHP version? + decision: > + Use caret/tilde ranges, but make sure the minimums are set appropriately. + rationale: > + Caret/tilde ranges allow non-breaking dependency updates while still letting the + minimum-version floor for each package be set deliberately. + - question: > + Add a static analysis tool and a code-style tool to the stack to support the + "Quality First" pillar, since none was listed. + decision: > + Added `backend_static_analysis` (PHPStan for general code quality, Psalm for + code security — both are used, for different purposes) and + `backend_code_style_tool` (PHP CS Fixer) above. + rationale: > + PHPStan and Psalm catch different classes of issues (general type/quality bugs + vs. security-focused taint analysis), so both are kept rather than picking one. \ No newline at end of file diff --git a/.decisions/testing_decisions.yaml b/.decisions/testing_decisions.yaml new file mode 100644 index 0000000..7b97e32 --- /dev/null +++ b/.decisions/testing_decisions.yaml @@ -0,0 +1,7 @@ +decisions_log: + - question: | + Is there a target CI provider/pipeline where the Codeception suites (behavioral/ + feature/integration/unit) will run automatically, and at what stage (on every + PR, pre-merge, nightly)? + decision: > + The repo will be stored on a Forgejo installation. Forgejo Actions will be the target CI provider. \ No newline at end of file diff --git a/.decisions/themes_architecture_decisions.yaml b/.decisions/themes_architecture_decisions.yaml new file mode 100644 index 0000000..86bbb72 --- /dev/null +++ b/.decisions/themes_architecture_decisions.yaml @@ -0,0 +1,39 @@ +decisions_log: + - question: > + Should the same Automatic Basic Security Audit that applies to Plugins also + apply to uploaded Themes and to any file saved via the in-place Theme Editor, + given that `theme.md`/`layout.php`/partials can contain arbitrary PHP? + decision: > + Yes — the same security audit used for Plugins (no eval, no exec, no remote + code execution) also applies to Themes, both on ZIP upload and on files saved + via the in-place Theme Editor. The only difference from the plugin flow is that + a failed audit throws a `ThemeSecurityException` instead of a + `PluginSecurityException`. + rationale: > + Themes can contain just as much arbitrary PHP as Plugins (`theme.md`, + `layout.php`, partials), so leaving them unaudited would be an inconsistent + security gap. + - question: > + Should theme files be versioned/backed up before each save through the Theme + Editor, so an accidental bad edit can be reverted without needing the full Site + Restore feature? + decision: > + Yes — versioning is implemented on each Theme Editor save (see the "Theme + Editor File Versioning" note above). + rationale: > + Gives a lightweight, per-file undo path for editor mistakes instead of forcing + a full Site Restore for a single bad save. + - question: How will theme assets be handled? + decision: > + Theme assets (CSS, JS, images) will be served directly by the web server, + with support for versioning to prevent caching issues. + rationale: > + Direct serving of assets provides better performance than PHP-based serving + while still allowing for proper version management. + - question: How will theme customization be implemented? + decision: > + Themes will include a built-in editor that allows direct modification of + template files through the admin interface. + rationale: > + This provides an easy way for users to customize their themes without needing + to modify files directly on the server. diff --git a/.decisions/ui_decisions.yaml b/.decisions/ui_decisions.yaml new file mode 100644 index 0000000..496a2a3 --- /dev/null +++ b/.decisions/ui_decisions.yaml @@ -0,0 +1,15 @@ +decisions_log: + - question: > + Only the Theme/Plugin Editor's layout is documented here. Should the Admin + dashboard, Media Manager, Page Builder, and User Management screens each get + their own UI notes before milestones are planned, or will that detail be worked + out milestone-by-milestone? + decision: > + The Admin dashboard, Media Manager, User Management, Role Management, and Page + Builder screens are now documented above alongside the Theme/Plugin Editor (see + `notes` above), so no further per-screen UI documentation gap remains at this + time. + rationale: > + Rounds out `ui.notes` so every major screen named in `features_overview` has at + least a first-pass layout description to plan milestones against; any gaps + found later can be raised as new open_questions as they're discovered. \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index e74cbd0..3c1986b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,9 +42,14 @@ When raw ideas are shared at the start of a project, review them for gaps, ambig Organize top-level keys by feature/domain area (e.g. `app`, `glossary`, `scope`, `features_overview`, per-domain `*_architecture`/`*_requirements` sections, `ui`, `data_model`, `testing`, etc. — adapt the exact set of sections to the project at hand). Within any section, use these conventions to carry the ideation trail: - `notes`: free-form rationale, considerations, examples — plain prose or a short list. -- `decisions_log`: an append-only list of resolved questions, in the shape `{question: ..., decision: ..., rationale: ...}`, capturing what was asked, what was decided, and why — so the "why" behind a choice is never lost. +- `decisions_log_ref`: a relative path to the decisions log file (see `Decisions Logs` section below) - `open_questions`: a structured list of items still to be clarified with the user (empty list when none remain); each entry should name the question and, if relevant, the options under consideration. +### Decisions Logs + +Every top level item will contain a decisions log located in `.decisions/{top_level_item_name}_decisions.yaml`. +This decisions log is an append-only list of resolved questions, in the shape `{question: ..., decision: ..., rationale: ...}`, capturing what was asked, what was decided, and why — so the "why" behind a choice is never lost. + ### MILESTONES.yaml structure Write `MILESTONES.yaml` as an ordered YAML structure (e.g. a top-level `milestones:` list, each entry keyed by number/id with `goal`, `steps`, `details`, and `tests` keys). @@ -65,10 +70,11 @@ A capability may be: The milestone must include all work necessary to make that capability function as a coherent whole, but no work belonging to another capability. +Milestones should be written in an order that produces the least amount of work overall. If Milestone ABC relies on work from Milestone DEF, don't build Milestone ABC with stubs, and push Milestone DEF to later (with replacing the stubs with the real thing). Order the milestones so that Milestone DEF comes first. + #### Example Milestone Schema ```yaml milestones: - # ---- Milestone 0: Project Foundation & Setup ---- milestone_{id}_{name}: status: [TODO|IN_PROGRESS|COMPLETE] description: > diff --git a/APP.yaml b/APP.yaml index 56dee6d..dfe1f24 100644 --- a/APP.yaml +++ b/APP.yaml @@ -15,7 +15,7 @@ --- app: name: SiteWeaverCMS - build_path: Milestone + use_milestones: true description: > A new PHP CMS, whose goal is to be modular, with easy to build and understand templates, built-in module installer (using Zip file uploader), @@ -39,99 +39,17 @@ app: - i18n Translation Engine: - Database-backed with `t()` (or t-like method) helper for UI strings. - Multi-Locale Content Support: - - JSON-per-locale storage for Pages, Posts, and Media. + - storage for Pages, Posts, and Media. - Translate for Me Service: - Service to auto-translate Post/Page/etc to languages selected in Configuration Settings - Button to trigger Service - - Looking into self-hosting a LibreTranslate instance - - If using LibreTranslate, consider installing jefs42/libretranslate as client - decisions_log: - - question: What language and minimum version should the app target? - decision: PHP 8.4+ - rationale: > - 8.4 is as far back in PHP versions as we want to support; targeting a recent - minimum keeps the codebase modern instead of carrying compatibility baggage - for older, less-supported PHP versions. - - question: Should the backend use an existing framework (e.g. Laravel, Symfony) or a custom/home-brewed codebase? - decision: Custom/home-brewed backend codebase (no third-party framework). - rationale: > - Almost all other projects have been built on Laravel; going custom here is a - deliberate choice to keep the skill of producing solid custom codebases sharp, - rather than always building on top of someone else's framework. - - question: Which CSS framework should the UI be built on? - decision: Fomantic UI - rationale: > - Fomantic UI is an actively maintained fork of Semantic UI (which is no longer - maintained), and semantic class names are strongly preferred over - structural/utility class names for CSS. - - question: Which JavaScript approach/framework should the frontend use? - decision: > - Vue — but used specifically via its Components feature only (similar in spirit - to how Svelte components are used), not as a full-fledged SPA framework. - rationale: > - General preference for Vue over React or Angular. Svelte would also have been an - acceptable choice for a components-only approach, but Vue was the one picked. - - question: Which tool should manage PHP dependencies? - decision: Composer - rationale: Composer is the de facto standard package manager for PHP. - - question: > - Is SiteWeaverCMS intended to be self-hosted only for v1, or should the - "Multi-tenancy ready" note under `architecture` mean a hosted/SaaS offering - is also on the roadmap? - decision: > - Self-hosted only. Multi-tenancy has been removed from consideration entirely - (see `architecture.decisions_log`), so there is no hosted/SaaS offering on the - roadmap and no tenant-isolation/billing concerns to capture. - rationale: > - Once multi-tenancy was ruled out as an architectural direction, a hosted/SaaS - offering (which would require it) is ruled out along with it. - - question: > - When the "Automatic Basic Security Audits for Plugins" check fails (eval/exec/ - remote code execution detected), what happens — is the ZIP upload rejected - outright, or is the plugin installed with an "Install - Error" status (as - implied by the Plugins List Page in `plugins_architecture`) for an Admin to - review? - decision: > - The upload is rejected outright, not installed with an "Install - Error" status. - Flow: the ZIP is unzipped, files are analyzed; if no security issues are found, - the plugin's migrations run and it is marked installed; if security issues are - found, the unzipped plugin files are immediately deleted and a - PluginSecurityException is thrown. The same audit and rejection flow applies to - Themes (throwing a ThemeSecurityException instead) — see - `plugins_architecture.decisions_log` and `themes_architecture.decisions_log`. - rationale: > - Files must be unzipped before they can be scanned, so a full "Install - Error" - list entry is unnecessary — a failed audit is treated as a hard rejection with - no trace left on disk. - - question: How will dependency injection be implemented in the app? - decision: > - A custom DI container will be implemented at the application level using - League/Container, following the service provider pattern. This will allow for - better modularity, testability, and maintainability of the core services. - rationale: > - Using a DI container allows for easier dependency management, testing, and - extension points in the future. The service provider pattern ensures proper - initialization of services and allows for lazy loading where appropriate. - `plugins_architecture.decisions_log` and `themes_architecture.decisions_log`. - - question: What license (if any) will SiteWeaverCMS be released under (e.g. MIT, GPL, proprietary)? - decision: > - No public license for now — SiteWeaverCMS stays sole-ownership/proprietary. It - is being used to build client websites, with the clients themselves administering - the sites built for them; a public release/license may be revisited later. - rationale: > - Matches the current usage model (agency-built client sites) rather than a - publicly redistributable product, so there is no need to pick a license yet. - - question: > - Should a target audience/deployment size be stated up front, since it - influences caching, storage backend, and search decisions made later in the - spec? - decision: > - Yes — the initial target audience is self-employed business owners, small - craft makers, and small new bloggers, with room to grow toward larger sites - and capabilities over time. - rationale: > - Gives later caching/storage/search decisions a concrete initial scale to design - against, while leaving room to grow. + - Process: + - Text of Page/Post is sent to a self-hosted LibreTranslate instance, along with the list of approved languages + - LibreTranslate instance returns the translated text + - Translated text is stored in the {Page|Post}_Versions table, with the language identifier in the `locale` column + - composer_require: + - composer require jefs42/libretranslate + decisions_log_ref: ".decisions/app_decisions.yaml" open_questions: [] suggestions: [] @@ -280,14 +198,8 @@ glossary: definition: > Time-based One-Time Password — a short-lived numeric code generated from a shared secret and the current time, used as a second authentication factor. - - term: Translate for Me Service - definition: > - An on-demand, button-triggered service that auto-translates a Post/Page/etc. - into the other languages selected in Configuration Settings, potentially - backed by a self-hosted LibreTranslate instance (e.g. via the - jefs42/libretranslate client). notes: [] - decisions_log: [] + decisions_log_ref: ".decisions/glossary_decisions.yaml" open_questions: [] scope: @@ -297,10 +209,11 @@ scope: - User Management - Blog plugin - Media Manager - - Theme/Plugin Editor + - Theme/Plugin Management - i18n - Comments - - Backup/Restore + - Site Backup + - Site Restore - Search out_of_scope: - Multi-tenancy (SaaS) @@ -313,112 +226,10 @@ architecture: - Architecture & Scalability: - Single-site CMS, no multi-tenancy (see decisions_log) description: > - SiteWeaverCMS is a custom-built PHP CMS with a modular architecture, designed for + SiteWeaverCMS is a Laravel 13.x-based CMS with a modular architecture, designed for self-hosted use. It features a clean separation of concerns between the core system and plugins/themes, with a focus on developer productivity and extensibility. - decisions_log: - - question: > - Is a dedicated backend_architecture section needed (per the "e.g." placeholder - comment above)? - decision: > - No — a separate backend_architecture section is not needed. Backend detail is - already captured across the domain-specific sections (media_manager_architecture, - plugins_architecture, themes_architecture, auth_requirements), which together - provide sufficient depth on the backend. - rationale: > - Those sections already document the relevant backend concerns for their domain - (storage abstraction, migrations, Service Providers, permissions, etc.), so a - separate umbrella backend_architecture section would just duplicate them. - - question: > - Is a dedicated frontend_architecture section still needed to document how Vue - Components are organized/bundled, and how they integrate with Fomantic UI and - the templating engine? - decision: > - Yes — a `frontend_architecture` section has been added (see below), filled in - with what could be gathered from the rest of this document, with its own - `open_questions` for what couldn't be inferred. - rationale: > - Frontend integration (Vue Components + Fomantic UI + the custom PHP templating - engine) is enough of its own concern to warrant a dedicated section rather than - being folded into `architecture` or `ui`. - - question: > - Given the previous "Multi-tenancy ready" note, should v1's schema/routing - already carry a tenant identifier (even if unused), or should multi-tenancy be - deferred entirely to a future major version with its own migration path? - decision: > - Multi-tenancy has been removed from consideration altogether — not deferred, - not kept "ready for" — SiteWeaverCMS is a single-site, single-tenant CMS with - no planned migration path toward multi-tenancy. The "Multi-tenancy ready" note - has been removed from `notes` above, and the "Multi-tenancy" glossary term has - been removed. - rationale: > - Avoids carrying unused tenant-identifier columns/routing complexity for a - direction the project will not pursue. - - question: How will plugin and theme loading be handled? - decision: > - Plugins and themes will be loaded through service providers that register their - respective services. The plugin manager and theme manager will handle the - discovery, loading, and unloading of these components at runtime. - rationale: > - Using service providers ensures proper dependency injection for plugins and - themes, while also allowing for lazy loading and easy testing of individual - components. - - question: What approach will be used for routing? - decision: > - A custom router with plugin/extension points. The core system will provide - basic routing functionality, but plugins can register their own routes. - rationale: > - This allows for a flexible architecture where plugins can easily add new - routes without modifying the core routing system. - - question: How will configuration be managed? - decision: > - Configuration will be handled through a service that loads from environment - variables and .env files (if available), with support for runtime updates. - rationale: > - Using environment variables for configuration ensures secure handling of - sensitive data while allowing for easy deployment to different environments. - - question: How will services be registered? - decision: > - Services will be registered using a service provider pattern. Core services - will be registered in CoreServiceProvider, application-level services in - AppServiceProvider, and plugin/theme-specific services in their respective - service providers. - rationale: > - The service provider pattern ensures proper initialization of services, - provides lazy loading capabilities, and allows for easy testing and - dependency injection. - - question: How will caching be implemented? - decision: > - A combination of in-memory caching (for frequently accessed data) and file-based - caching (for larger content that doesn't need to be loaded on every request). - rationale: > - This hybrid approach ensures good performance for both frequently accessed - data and larger content while maintaining a reasonable memory footprint. - - question: How will security be implemented? - decision: > - Security will be implemented through a combination of input validation, - authentication, authorization, and secure storage practices. Plugins will be - subject to automated security audits during installation. - rationale: > - A layered security approach ensures that multiple attack vectors are covered, - with special attention to preventing code injection and unauthorized access. - - question: > - `in_scope` and `out_of_scope` were both still empty. What is explicitly IN scope - for v1 and what is explicitly OUT of scope for v1? - decision: > - `in_scope` and `out_of_scope` have been populated (see lists above): Page - Builder, Role Based Auth, User Management, Blog plugin, Media Manager, - Theme/Plugin Editor, i18n, Comments, Backup/Restore, and Search are in scope - for v1; multi-tenant/SaaS deployment, a plugin marketplace, and native mobile - apps are out of scope. - rationale: > - Gives a concrete, at-a-glance v1 boundary to plan milestones against instead of - inferring scope from `features_overview.notes`. - - question: What backup strategy is supported in v1? - decision: Manual trigger only via Admin UI, no automatic or scheduled backups. - rationale: > - The feature notes specify that backups are not automated; they must be triggered - manually by an admin action. + decisions_log_ref: ".decisions/architecture_decisions.yaml" open_questions: [] plugins_architecture: @@ -427,7 +238,17 @@ plugins_architecture: through modular components. Each plugin can register its own routes, services, and configuration settings. notes: - - Plugins/Modules: + - plugin_metadata_spec: + required_fields: + - name + - author + - description + - date + - version + - core_version + format: + core_version: semantic version string (e.g., "13.x") + - Plugins: - Admin section Plugins List Page: - Lists each plugin (its name, brief description, and active status): - Active: Plugin is installed and switched on @@ -472,6 +293,7 @@ plugins_architecture: - comments are auto-approved (no manual approval needed), but may be deleted/redacted by Admin or other User Role with proper permissions - Modular DB Migrations: - Each plugin/component has a `migrations` folder + - The migrations folder resides inside the plugin directory (`plugins/blog/migrations/{migration files}`) - CMS scans and runs them automatically on install - Plugin table names should be in the form of {plugin_name}_{table_name} - Example - Blog_Posts, Blog_Post_Versions @@ -488,64 +310,7 @@ plugins_architecture: - Front-end: - Plugin can include it's own template partials for specialized output, or be outputted by installed theme files: - Example: A calendar Plugin can include it's own calendar.php template file for outputting the calendar - decisions_log: - - question: Should Composer-based installation of modules be supported alongside ZIP upload? - decision: > - No — Composer installation of modules has been removed, at least for v1. The - only installation process now is ZIP file upload. - rationale: > - Keeps a single, admin-friendly install path consistent with the "even a non-tech - person finds inviting" goal in `app`; CLI/Composer access isn't assumed to be - available to site admins. - - question: > - What is the resolution path when the Automatic Basic Security Audit rejects a - plugin — is the upload blocked outright, or installed with an "Install - Error" - status (as implied by the Plugins List Page) for the Admin to inspect further? - decision: > - The upload is blocked outright. Since the upload has to finish before the - system can unzip and inspect its contents, the flow is: the ZIP is unzipped, - files are analyzed; if no security issues are found, the plugin's migration - files run and the plugin is marked installed; if security issues are found, - the unzipped plugin files are immediately deleted and a PluginSecurityException - is thrown. There is no "Install - Error" status resulting from a failed - security audit — that status is reserved for other install-time errors (e.g. a - migration failure). - rationale: > - A plugin that fails the security audit should leave nothing installed or - listed; only errors unrelated to security (e.g. a broken migration) warrant - surfacing an "Install - Error" entry for the Admin to inspect. - - question: > - Is there a plugin dependency/versioning mechanism (e.g. a required core version, - or dependencies between plugins) that needs to be declared in `plugin.md`? - decision: > - Yes — a Core Version dependency is declared in `plugin.md` (added to its - required metadata list in `directory_structure`), mirroring the same field - already present in `theme.md`/`admin.theme.md`. - rationale: > - Lets the install process reject a plugin that requires a newer core version - than what's installed, consistent with how themes already declare a Core - Version. - - question: What approach will be used for plugin installation? - decision: > - Plugins will be installed as ZIP files that are extracted to a plugins directory, - with the plugin's service provider registered automatically. - rationale: > - This approach provides a simple and familiar installation process for users - while ensuring proper service registration through the DI container. - - question: How will plugin dependencies be handled? - decision: > - Plugins can declare their dependencies in their metadata files. The plugin - manager will handle loading plugins in the correct order based on dependencies. - rationale: > - This ensures that required services are available when a plugin is loaded, - preventing runtime errors. - - question: How will plugin configuration be handled? - decision: > - Each plugin can register its own configuration settings, which will be - automatically merged with the main configuration system. - rationale: > - This allows plugins to maintain their own settings while integrating cleanly - with the core configuration system. + decisions_log_ref: ".decisions/plugin_architecture_decisions.yaml" open_questions: [] themes_architecture: @@ -557,7 +322,7 @@ themes_architecture: - Themes: - Default theme (provided by default) supports Light/Dark switching, and set to System by default - Admin theme supports Light/Dark switching, and set to System by default - - Expressive, easy-to-read templating engine (accessible for non-UI devs) + - Laravel Blade templates (accessible for non-UI devs) - Uses `$this->themeUrl($asset)` to produce a url to the asset in the theme directory - Uses `$this->adminThemeUrl($asset)` to produce a url to the asset in the admin theme directory - exposes `$theme` variable to templates: @@ -644,45 +409,7 @@ themes_architecture: - Each save made through the in-place Theme Editor creates a new version of the saved file, so an accidental bad edit can be reverted without needing the full Site Restore feature. - decisions_log: - - question: > - Should the same Automatic Basic Security Audit that applies to Plugins also - apply to uploaded Themes and to any file saved via the in-place Theme Editor, - given that `theme.md`/`layout.php`/partials can contain arbitrary PHP? - decision: > - Yes — the same security audit used for Plugins (no eval, no exec, no remote - code execution) also applies to Themes, both on ZIP upload and on files saved - via the in-place Theme Editor. The only difference from the plugin flow is that - a failed audit throws a `ThemeSecurityException` instead of a - `PluginSecurityException`. - rationale: > - Themes can contain just as much arbitrary PHP as Plugins (`theme.md`, - `layout.php`, partials), so leaving them unaudited would be an inconsistent - security gap. - - question: > - Should theme files be versioned/backed up before each save through the Theme - Editor, so an accidental bad edit can be reverted without needing the full Site - Restore feature? - decision: > - Yes — versioning is implemented on each Theme Editor save (see the "Theme - Editor File Versioning" note above). - rationale: > - Gives a lightweight, per-file undo path for editor mistakes instead of forcing - a full Site Restore for a single bad save. - - question: How will theme assets be handled? - decision: > - Theme assets (CSS, JS, images) will be served directly by the web server, - with support for versioning to prevent caching issues. - rationale: > - Direct serving of assets provides better performance than PHP-based serving - while still allowing for proper version management. - - question: How will theme customization be implemented? - decision: > - Themes will include a built-in editor that allows direct modification of - template files through the admin interface. - rationale: > - This provides an easy way for users to customize their themes without needing - to modify files directly on the server. + decisions_log_ref: ".decisions/themes_architecture_decisions.yaml" open_questions: [] components: @@ -690,22 +417,7 @@ components: SiteWeaverCMS consists of several key components that work together to provide a complete content management solution. These include the core system, plugins, themes, and various service components. - decisions_log: - - question: How will core components be organized? - decision: > - Core components will be organized under the src/Site/ directory with clear - separation between bootstrap code, router logic, services, and other core - functionality. - rationale: > - This organization provides a clean separation of concerns and makes it easy - to locate specific components within the codebase. - - question: How will service components be structured? - decision: > - Service components will be organized under src/Site/Services/ with each - service having its own class implementing a clear interface. - rationale: > - This structure promotes loose coupling and makes it easy to swap out - implementations for testing or alternative backends. + decisions_log_ref: ".decisions/components_decisions.yaml" open_questions: [] notes: [] @@ -718,216 +430,370 @@ data_model: - Site Settings: - Site Title/Description - Global SEO Settings - - Locales supported ('en','es','fr','ru',etc) + - Locales table uses `is_supported` to indicate supported locales, and `is_default` marks the default locale. - Blog Settings: - Blog Title - Inherits locales support from Site Settings - Configurable Blog "Home" URL segment (Admin/Editor can set the entry point for the blog plugin) - Database Tables: - Site_Settings: - - id: int, primary key - - name: string(255), unique, not null - - setting: string(255), null - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - id: int, not null + - name: string(255), not null + - setting: string(255), null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: id + - foreign_key: created_by (Users.id) + - foreign_key: updated_by (Users.id) + - index: name + - Locales: + - fields: + - id: int, not null + - code: string(7), not null + - name: string(100), not null + - direction: enum('ltr','rtl'), not null, default='ltr' + - is_supported: bool, not null, default=false + - is_default: bool, not null, default=false + - created_at: datetime, not null, default=now() + - updated_at: datetime, null + - indexes: + - primary_key: id + - unique: code + - unique: (CASE WHEN is_default = true THEN 1 ELSE NULL END) + - index: name - Users: - - id: int, primary key - - username: string(255), not null, unique - - password: string(255), not null - - f_name: string(255), not null - - l_name: string(255), null - - email: string(255), not null, unique - - avatar: string(255), null, comment="user account image, uses default if null" - - failed_login_attempts: int, not null, default=0 - - locked_until: datetime, null - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - id: int, not null + - username: string(255), not null + - password: string(255), not null + - f_name: string(255), not null + - l_name: string(255), null + - email: string(255), not null + - avatar: string(255), null, comment="user account image, uses default if null" + - failed_login_attempts: int, not null, default=0 + - locked_until: datetime, null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: id + - foreign_key: created_by (Users.id) + - foreign_key: updated_by (Users.id) + - unique: username + - unique: email - User_Totp: - - id: int, primary key - - user_id: int, foreign key (Users.id), not null - - secret: string(32), not null - - enabled: boolean, not null, default=true - - created_at: datetime, not null - - updated_at: datetime, null + - fields: + - id: int, not null + - user_id: int, not null + - secret: string(32), not null + - enabled: boolean, not null, default=true + - created_at: datetime, not null + - updated_at: datetime, null + - indexes: + - primary_key: id + - foreign_key: user_id (Users.id) - User_password_reset: - - id: int, primary key - - user_id: int, foreign key (Users.id), not null - - token: string(42), not null, unique - - expires_at: datetime, not null - - created_at: datetime, not null + - fields: + - id: int, not null + - user_id: int, not null + - token: string(42), not null, unique + - expires_at: datetime, not null + - created_at: datetime, not null + - indexes: + - primary_key: id + - foreign_key: user_id (Users.id) - User_email_verification: - - id: int, primary key - - user_id: int, foreign key (Users.id), not null - - token: string(42), not null, unique - - expires_at: datetime, not null - - created_at: datetime, not null - - Roles (Stores each logical role (e.g., Admin, Editor, Viewer)): - - id: int, primary key - - name: string(255), not null, unique - - description: string(255), null - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - id: int, not null + - user_id: int, not null + - token: string(42), not null + - expires_at: datetime, not null + - created_at: datetime, not null + - indexes: + - primary_key: id + - foreign_key: user_id (Users.id) + - unique: token + - Roles (Stores each logical role (e.g., Developer, Admin, Editor, Viewer)): + - fields: + - id: int, not null + - name: string(255), not null + - description: string(255), null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: id + - unique: name + - foreign_key: created_by (Users.id) + - foreign_key: updated_by (Users.id) - Permissions (Stores each distinct permission (e.g., read:document, delete:user)): - - id: int, primary key - - name: string(255), not null, unique - - type: string(255), null - - action: string(255), null - - description: string(255), null - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - id: int, not null + - name: string(255), not null + - type: string(255), null + - action: string(255), null + - description: string(255), null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: id + - unique: name + - foreign_key: created_by (Users.id) + - foreign_key: updated_by (Users.id) - Role_Permission (Junction table that maps many roles to many permissions): - - role_id: int, foreign key (Roles.id), not null - - permission_id: int, foreign key (Permissions.id), not null - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - role_id: int, not null + - permission_id: int, not null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: (role_id, permission_id) + - foreign_key: role_id (Roles.id) + - foreign_key: permission_id (Permissions.id) + - foreign_key: created_by (Users.id) + - foreign_key: updated_by (Users.id) - User_Role (Junction table that maps many users to many roles): - - user_id: int, foreign key (Users.id), not null - - role_id: int, foreign key (Roles.id), not null - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - user_id: int, not null + - role_id: int, not null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: (user_id, role_id) + - foreign key: user_id (Users.id) + - foreign key: role_id (Roles.id) + - foreign key: created_by (Users.id) + - foreign key: updated_by (Users.id) - Pages: - - id: int, primary key - - slug: string(255), unique, comment="human‑readable URL part" - - title: string(255) - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null - - published_at: datetime (set once on first publish (first version publish), never updated) + - fields: + - id: int, not null + - slug: string(255), comment="human‑readable URL part" + - title: string(255) + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - published_at: datetime, null - (set once on first publish (first version publish), never updated) + - indexes: + - primary_key: id + - unique: slug + - foreign key: created_by (Users.id) + - foreign key: updated_by (Users.id) - Page_Versions: - - id: int, primary key - - page_id: int, not null, foreign key (Pages.id) - - version_number: int, not null (incremental, starts at 1) - - locale: string(3), not null - - content: text, not null comment="the full HTML or templating markup" - - meta_json: json/text, null, comment="extra fields like seo_title, meta_description, tags" - - created_at: datetime, not null - - created_by: int, foreign key (Users.id) - - is_published: bool, not null, default=false - - published_at: datetime, null + - fields: + - id: int, not null + - page_id: int, not null + - version_number: int, not null - (incremental per page per locale, starts at 1) + - locale: string(7), not null + - content: text, not null, comment="the full HTML or templating markup" + - meta_json: json/text, null, comment="extra fields like seo_title, meta_description, tags" + - created_at: datetime, not null + - created_by: int, not null + - is_published: bool, not null, default=false + - published_at: datetime, null + - indexes: + - primary_key: id + - foreign_key: page_id (Pages.id) + - foreign key: created_by (Users.id) - Blog_Settings (Blog plugin): - - id: int, primary key - - name: string(255), unique, not null - - setting: string(255), null - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - id: int, not null + - name: string(255), not null + - setting: string(255), null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: id + - unique: name + - foreign key: created_by (Users.id) + - foreign key: updated_by (Users.id) - Blog_Posts (Blog plugin): - - id: int, primary key - - slug: string(255), unique, comment="human‑readable URL part" - - title: string(255) - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null - - published_at: datetime (set once on first publish (first version publish), never updated) + - fields: + - id: int, not null + - slug: string(255), not null, comment="human‑readable URL part" + - title: string(255), not null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - published_at: datetime, null - (set once on first publish (first version publish), never updated) + - indexes: + - primary_key: id + - unique: slug + - foreign key: created_by (Users.id) + - foreign key: updated_by (Users.id) - Blog_Post_Versions (Blog plugin): - - id: int, primary key - - post_id: int, not null, foreign key (Blog_Posts.id) - - version_number: int, not null (incremental, starts at 1) - - locale: string(3), not null - - extract: text, null (if null, extract is pulled from content) - - content: text, not null, comment="the full HTML or templating markup" - - meta_json: json/text, null comment="extra fields like seo_title, meta_description, tags" - - created_at: datetime, not null - - created_by: int, foreign key (Users.id) - - is_published: bool, not null, default=false - - published_at: datetime, null + - fields: + - id: int, not null + - post_id: int, not null + - version_number: int, not null (incremental per post_id per locale, starts at 1) + - locale: string(7), not null + - extract: text, null (if null, extract is pulled from content) + - content: text, not null, comment="the full HTML or templating markup" + - meta_json: json/text, null comment="extra fields like seo_title, meta_description, tags" + - created_at: datetime, not null + - created_by: int, not null + - is_published: bool, not null, default=false + - published_at: datetime, null + - indexes: + - primary_key: id + - foreign_key: post_id (Blog_Posts.id) + - foreign_key: created_by (Users.id) - Blog_Categories (Blog plugin): - - id: int, primary key - - name: string(255), not null, unique - - slug: string(255), not null, unique - - description: text, null + - fields: + - id: int, not null + - name: string(255), not null + - slug: string(255), not null + - description: text, null + - indexes: + - primary_key: id + - unique: name + - unique: slug - Blog_Tags (Blog plugin): - - id: int, primary key - - name: string(255), not null, unique - - description: text, null + - fields: + - id: int, not null + - name: string(255), not null + - description: text, null + - indexes: + - primary_key: id + - unique: name - Blog_Post_Categories (Blog plugin): - - post_id: int, foreign key (Blog_Posts.id) - - category_id: int, foreign key (Blog_Categories.id) - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - post_id: int, not null + - category_id: int, not null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - indexes: + - primary_key: (post_id, category_id) + - foreign_key: post_id (Blog_Posts.id) + - foreign_key: category_id (Blog_Categories.id) + - foreign key: created_by (Users.id) - Blog_Post_Tags (Blog plugin): - - post_id: int, foreign key (Blog_Posts.id) - - tag_id: int, foreign key (Blog_Tags.id) - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - post_id: int, not null + - tag_id: int, not null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - indexes: + - primary_key: (post_id, tag_id) + - foreign_key: post_id (Blog_Posts.id) + - foreign_key: tag_id (Blog_Tags.id) + - foreign key: created_by (Users.id) - Blog_Comments: - - id: int, primary key - - post_id: int, foreign key (Blog_Posts.id), not null - - user_id: int, foreign key (Users.id), not null - - content: text, not null - - likely_spam: boolean, not null - - created_at: datetime, not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null, comment="comments can be redacted (content set to "[redacted]") by Admins, and others with the appropriate permissions" + - fields: + - id: int, not null + - post_id: int, not null + - user_id: int, not null + - content: text, not null + - likely_spam: boolean, not null + - created_at: datetime, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: id + - foreign_key: post_id (Blog_Posts.id) + - foreign_key: user_id (Users.id) + - foreign key: updated_by (Users.id) - Media: - - id: int, primary key - - filename: string(255), not null - - path: string(255), not null - - storage_backend: string(255), not null - - mime_type: string(75), not null - - height: int, not null - - width: int, not null - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - id: int, not null + - filename: string(255), not null + - path: string(255), not null + - storage_backend: string(255), not null + - mime_type: string(75), not null + - height: int, not null + - width: int, not null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: id + - foreign key: created_by (Users.id) + - foreign key: updated_by (Users.id) - Media_Metadata: - - id: int, primary key - - media_id: int, foreign key (Media.id), not null - - name: string(255), not null - - alt: text, null, default=null - - locale: string(3), not null - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - id: int, not null + - media_id: int, not null + - name: string(255), not null + - alt: text, null, default=null + - locale: string(3), not null + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: id + - foreign_key: media_id (Media.id) + - foreign key: created_by (Users.id) + - foreign key: updated_by (Users.id) - Plugins: - - id: int, primary key - - name: string(255), not null - - version: string(10), not null - - core_version: string(10), not null - - status: enum('Active', 'Deactivated','Error'), not null, default='Deactivated' - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - id: int, not null + - name: string(255), not null + - version: string(10), not null + - core_version: string(10), not null + - status: enum('Active','Deactivated','Error'), not null, default='Deactivated' + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: id + - foreign key: created_by (Users.id) + - foreign key: updated_by (Users.id) - Frontend_Themes: - - id: int, primary key - - name: string(255), not null - - enabled: boolean, not null, default=false, comment="Only one may be enabled at a time. Currently controlled by app code, not DB constraints" - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - id: int, not null + - name: string(255), not null + - enabled: boolean, not null, default=false, comment="Only one may be enabled at a time. Currently controlled by app code, not DB constraints" + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: id + - foreign key: created_by (Users.id) + - foreign key: updated_by (Users.id) - Admin_Themes: - - id: int, primary key - - name: string(255), not null - - enabled: boolean, not null, default=false, comment="Only one may be enabled at a time. Currently controlled by app code, not DB constraints" - - created_at: datetime, not null, default=now() - - created_by: int, foreign key (Users.id), not null - - updated_at: datetime, null - - updated_by: int, foreign key (Users.id), null + - fields: + - id: int, not null + - name: string(255), not null + - enabled: boolean, not null, default=false, comment="Only one may be enabled at a time. Currently controlled by app code, not DB constraints" + - created_at: datetime, not null, default=now() + - created_by: int, not null + - updated_at: datetime, null + - updated_by: int, null + - indexes: + - primary_key: id + - foreign key: created_by (Users.id) + - foreign key: updated_by (Users.id) + - Backups: + - fields: + - id: int, not null + - filename: string(255), not null + - size: bigint, not null + - created_at: datetime, not null, default=now() + - indexes: + - primary_key: id + - index: filename example_seed_data_for_roles: > INSERT INTO Roles (name, description) VALUES + ('Developer', 'Full access to all CMS features and settings. Can create Admins'), ('Admin', 'Full access to all CMS features and settings.'), ('Editor', 'Can create, edit, and delete content, but cannot change site settings.'), ('Author', 'Can create and edit their own content; can view drafts.'), @@ -959,101 +825,35 @@ data_model: SELECT r.id, p.id FROM Roles r, Permissions p WHERE r.name = 'Author' AND p.name IN ('publish:page','edit:page', 'edit:draft','publish:draft'); - decisions_log: - - question: > - `notes` here only cover Settings; no entities/columns have been sketched yet for - Users, Roles, Permissions, Pages, Posts, Media, Comments, or Content Versions - (History). Should the core schema be drafted in this section before - MILESTONES.yaml is written, given how many features depend on it? - decision: > - Yes — the core schema has been drafted above (`Database Tables`), covering - Settings, Users, Roles, Permissions, Role_Permission, User_Role, Pages, - Page_Versions, Posts, Post_Versions, Media, and Comments. - rationale: > - Gives MILESTONES.yaml concrete table/column definitions to build migrations - from, instead of inferring the schema from feature descriptions alone. - - question: > - Should a cross-cutting convention for audit/timestamp columns (`created_at`/ - `updated_at`, and who created/last edited a record) be documented once entities - are sketched out, so every migration follows the same pattern? - decision: > - Partially applied so far: Pages, Page_Versions, Posts, Post_Versions, and - Comments already follow a `created_at`/`created_by`/`updated_at`/`updated_by` - pattern. Users, Roles, Permissions, Role_Permission, User_Role, and Media do - not yet have any `created_at`/`updated_at` columns (see the new suggestion - below flagging this gap). - rationale: > - Records the convention actually in use today, rather than assuming it was - applied uniformly across every table. - - question: > - Should the suggested `created_at`/`created_by`/`updated_at`/`updated_by` - columns be added to Users, Roles, Permissions, Role_Permission, User_Role, and - Media (see `suggestions` below)? - decision: > - Yes, accepted and applied — Users, Roles, Permissions, Role_Permission, and - User_Role already carried the columns above; Media has now also been given the - same `created_at`/`created_by`/`updated_at`/`updated_by` columns, so every - table in the schema now follows the convention. `User_Totp`, `User_password_reset`, - and `User_email_verification` are purposely missing the `created_by` and `updated_by` - fields, as these are system generated and not tied to specific users. - rationale: > - Closes the last gap (Media) so audit/timestamp tracking is consistent across - every table, matching the convention already used on Pages/Page_Versions/ - Posts/Post_Versions/Comments. - - question: What database engine will be used? - decision: > - Support for SQLite, MySQL, and PostgreSQL through PDO connections. - rationale: > - Supporting multiple engines provides flexibility for different deployment - scenarios while maintaining a consistent interface through PDO. - - question: How will data migrations be handled? - decision: > - Data migrations will be implemented using Phinx, with each plugin having - its own migration files. - rationale: > - This approach allows plugins to manage their own database schema changes - while providing a centralized migration management system. + seed_data_for_locales: > + INSERT INTO Locales (code, name, direction) VALUES + ("en","English","ltr"), ("sq","Albanian","ltr"), ("ar","Arabic","rtl"), ("az","Azerbaijani","ltr"), ("eu","Basque","ltr"), ("bn","Bengali","ltr"), + ("bg","Bulgarian","ltr"), ("ca","Catalan","ltr"), ("zh-Hans","Chinese","ltr"), ("zh-Hant","Chinese (traditional)","ltr"), + ("cs","Czech","ltr"), ("da","Danish","ltr"), ("nl","Dutch","ltr"), ("eo","Esperanto","ltr"), ("et","Estonian","ltr"), ("fi","Finnish","ltr"), + ("fr","French","ltr"), ("gl","Galician","ltr"), ("de","German","ltr"), ("el","Greek","ltr"), ("he","Hebrew","rtl"), ("hi","Hindi","ltr"), + ("hu","Hungarian","ltr"), ("id","Indonesian","ltr"), ("ga","Irish","ltr"), ("it","Italian","ltr"), ("ja","Japanese","ltr"), ("ko","Korean","ltr"), + ("ky","Kyrgyz","ltr"), ("lv","Latvian","ltr"), ("lt","Lithuanian","ltr"), ("ms","Malay","ltr"), ("nb","Norwegian","ltr"), ("fa","Persian","rtl"), + ("pl","Polish","ltr"), ("pt","Portuguese","ltr"), ("pt-BR","Portuguese (Brazil)","ltr"), ("ro","Romanian","ltr"), ("ru","Russian","ltr"), + ("sr","Serbian","ltr"), ("sk","Slovak","ltr"), ("sl","Slovenian","ltr"), ("es","Spanish","ltr"), ("sw","Swahili","ltr"), ("sv","Swedish","ltr"), + ("tl","Tagalog","ltr"), ("th","Thai","ltr"), ("tr","Turkish","ltr"), ("uk","Ukrainian","ltr"), ("ur","Urdu","rtl"), ("vi","Vietnamese","ltr"); + + decisions_log_ref: ".decisions/data_model_decisions.yaml" open_questions: [] deployment: description: > SiteWeaverCMS is designed for self-hosted deployment with support for various web server configurations and environments. - decisions_log: - - question: What are the deployment requirements? - decision: > - The application requires PHP 8.4+, Composer, and a web server (Apache/Nginx). - Database configuration will be handled through environment variables. - rationale: > - These requirements ensure compatibility with modern PHP features while - providing flexibility in hosting environments. - - question: How will environment-specific configuration be handled? - decision: > - Environment-specific configuration will be managed through .env files (if available) and - environment variables, with a default .env.example file included. - rationale: > - This approach allows for easy deployment to different environments while - keeping sensitive data out of version control. + decisions_log_ref: ".decisions/deployment_decisions.yaml" open_questions: [] notes: [] security: description: > - SiteWeaverCMS implements security best practices including input validation, - authentication, and secure coding practices. - decisions_log: - - question: What authentication method will be used? - decision: > - Role-based authentication with support for TOTP-based 2FA. - rationale: > - This provides a good balance between security and usability while supporting - enterprise-level security requirements. - - question: How will user permissions be handled? - decision: > - A granular permission system based on roles and capabilities. - rationale: > - This allows for precise control over what actions users can perform while - maintaining a simple and understandable permission model. + SiteWeaverCMS implements security best practices including input validation, csrf on every form, + form data cleansing (no