Migration modular monolith DDD (0.1 → 3.3) #17

Merged
matthieu merged 99 commits from integration/modular-monolith-0.1-1.3 into develop 2026-06-23 13:50:43 +00:00
Owner

Migration modular monolith DDD — Lesstime (0.1 → 3.3)

Cette MR regroupe l'intégralité de la refonte en monolithe modulaire (strangler progressif, additif). Elle remplace les MR stackées de Phase 1 (#12–#16), désormais incluses ici.

Ne pas merger avant validation fonctionnelle : branche destinée à être testée telle quelle.

Périmètre — 9 modules sous src/Module/

Phase Module Contenu
0.1 (socle) infrastructure modulaire, ModuleInterface, mapping Doctrine par module
0.2 (socle front) auto-détection des layers Nuxt sous frontend/modules/*
1.1 Core Identité (User/Auth), Notifications, Notifier
1.2 Core RBAC fin (permissions module.resource.action, sidebar gated)
1.3 Core Audit log (#[Auditable], listener, provider DBAL)
2.1 TimeTracking TimeEntry + MCP + export
2.2 ProjectManagement cœur métier Projets/Tâches + 38 MCP tools
2.3 Absence demandes, soldes, policies, justificatifs
2.4 Directory Clients (migrés) + Prospects (nouveau, conversion → Client)
2.5 Mail intégration IMAP OVH + liens tâches
2.6 Integration Gitea / BookStack / Zimbra / Share
3.1 Reporting rapports transverses (DBAL read-only, 0 import inter-module)
3.2 ClientPortal portail client (ROLE_CLIENT cloisonné, tickets, notifications)
3.3 (finition) nettoyage legacy — src/Entity vide, app 100% modulaire

Architecture

  • Découplage inter-modules par contrats (UserInterface, ProjectInterface, TaskInterface, TaskTagInterface, ClientInterface, ClientTicketInterface, LeaveProfileInterface) + resolve_target_entities 100% modulaire (aucune cible legacy).
  • Repositories : interface Domain/Repository + implémentation Infrastructure/Doctrine, bindées.
  • Reporting en DBAL read-only pur (aucun import d'entité d'un autre module).
  • Chaque migration de module : déplacement à comportement préservé (API publique et noms d'outils MCP inchangés), migrations additives uniquement (zéro destructif).

Sécurité

  • ROLE_CLIENT cloisonné : un utilisateur client n'accède qu'à /portal et à ses propres tickets (filtrés par allowedProjects), interdit sur toute l'API interne.
  • Correctif : interdiction pour un client de créer un lien vers le partage SMB (upload uniquement).

QA non-régression (branche reconstruite from scratch)

  • Migrations from scratch + fixtures : OK.
  • Compilation dev + prod : OK.
  • 180 tests PHPUnit verts, php-cs-fixer clean, ~96 routes, 66 outils MCP tous sous App\Module\*.
  • Smoke test runtime multi-rôles (admin / ROLE_USER / ROLE_CLIENT) : 44 vérifications HTTP, 0 écart, cloisonnement client étanche.
  • Build Nuxt OK, 9 layers, 0 import legacy résiduel.

Points à arbitrer (hors périmètre de cette migration)

  • Durcissement MCP/IDOR pré-existant (userId explicite sans scoping sur certains tools TimeTracking/Absence/TaskDocument) — ticket dédié recommandé.
  • Validation fonctionnelle de Prospect et ClientPortal (conçus depuis les specs disque).
  • Harmonisation visuelle Malio finale (3.3) — finition esthétique inter-modules laissée au PO.

⚠️ Déploiement / migration des données — à ne pas oublier

1. Resynchroniser les séquences PostgreSQL après tout import/restore de dump

Si la prod (ou tout environnement) est montée depuis un dump (pg_restore / COPY), les lignes sont chargées avec leurs id explicites sans avancer les séquences → au premier INSERT : duplicate key value violates unique constraint "..._pkey" (constaté en local sur notification, task, time_entry…).

À lancer juste après chaque restore/import :

DO $$
DECLARE r RECORD; maxid BIGINT; seq TEXT;
BEGIN
  FOR r IN SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public'
  LOOP
    seq := pg_get_serial_sequence(quote_ident(r.table_name), r.column_name);
    IF seq IS NOT NULL THEN
      EXECUTE format('SELECT COALESCE(MAX(%I),0) FROM %I', r.column_name, r.table_name) INTO maxid;
      PERFORM setval(seq, GREATEST(maxid,1), maxid > 0);
    END IF;
  END LOOP;
END $$;

Ne concerne pas une prod qui tourne déjà (séquences avancées organiquement) — uniquement le cas restore/import. Idempotent, sans risque.

2. Fix dénormalisation des collections typées-contrat (code, inclus dans la branche)

Les relations to-many typées par une interface Shared\Domain\Contract\* (TimeEntry::tagsTaskTagInterface, Task::collaboratorsUserInterface) étaient indénormalisables par API Platform (mono-valué OK via IRI, collection KO) → tout POST/PATCH portant une telle collection renvoyait 400/500. Corrigé par un dénormaliseur générique ContractRelationDenormalizer (réutilise resolve_target_entities, zéro couplage par-entité) + test fonctionnel de non-régression.

## Migration modular monolith DDD — Lesstime (0.1 → 3.3) Cette MR regroupe l'intégralité de la refonte en monolithe modulaire (strangler progressif, additif). Elle remplace les MR stackées de Phase 1 (#12–#16), désormais incluses ici. **Ne pas merger avant validation fonctionnelle** : branche destinée à être testée telle quelle. ### Périmètre — 9 modules sous `src/Module/` | Phase | Module | Contenu | |------|--------|---------| | 0.1 | (socle) | infrastructure modulaire, `ModuleInterface`, mapping Doctrine par module | | 0.2 | (socle front) | auto-détection des layers Nuxt sous `frontend/modules/*` | | 1.1 | **Core** | Identité (User/Auth), Notifications, Notifier | | 1.2 | Core | RBAC fin (permissions `module.resource.action`, sidebar gated) | | 1.3 | Core | Audit log (`#[Auditable]`, listener, provider DBAL) | | 2.1 | **TimeTracking** | TimeEntry + MCP + export | | 2.2 | **ProjectManagement** | cœur métier Projets/Tâches + 38 MCP tools | | 2.3 | **Absence** | demandes, soldes, policies, justificatifs | | 2.4 | **Directory** | Clients (migrés) + **Prospects** (nouveau, conversion → Client) | | 2.5 | **Mail** | intégration IMAP OVH + liens tâches | | 2.6 | **Integration** | Gitea / BookStack / Zimbra / Share | | 3.1 | **Reporting** | rapports transverses (DBAL read-only, 0 import inter-module) | | 3.2 | **ClientPortal** | portail client (ROLE_CLIENT cloisonné, tickets, notifications) | | 3.3 | (finition) | nettoyage legacy — `src/Entity` vide, app 100% modulaire | ### Architecture - Découplage inter-modules par **contrats** (`UserInterface`, `ProjectInterface`, `TaskInterface`, `TaskTagInterface`, `ClientInterface`, `ClientTicketInterface`, `LeaveProfileInterface`) + `resolve_target_entities` 100% modulaire (aucune cible legacy). - Repositories : interface `Domain/Repository` + implémentation `Infrastructure/Doctrine`, bindées. - Reporting en DBAL read-only pur (aucun import d'entité d'un autre module). - Chaque migration de module : déplacement à comportement préservé (API publique et noms d'outils MCP inchangés), migrations **additives** uniquement (zéro destructif). ### Sécurité - ROLE_CLIENT cloisonné : un utilisateur client n'accède qu'à `/portal` et à ses propres tickets (filtrés par `allowedProjects`), interdit sur toute l'API interne. - Correctif : interdiction pour un client de créer un lien vers le partage SMB (upload uniquement). ### QA non-régression (branche reconstruite from scratch) - Migrations from scratch + fixtures : OK. - Compilation dev + prod : OK. - **180 tests PHPUnit verts**, php-cs-fixer clean, ~96 routes, **66 outils MCP** tous sous `App\Module\*`. - Smoke test runtime multi-rôles (admin / ROLE_USER / ROLE_CLIENT) : 44 vérifications HTTP, **0 écart**, cloisonnement client étanche. - Build Nuxt OK, 9 layers, 0 import legacy résiduel. ### Points à arbitrer (hors périmètre de cette migration) - Durcissement MCP/IDOR pré-existant (`userId` explicite sans scoping sur certains tools TimeTracking/Absence/TaskDocument) — ticket dédié recommandé. - Validation fonctionnelle de **Prospect** et **ClientPortal** (conçus depuis les specs disque). - **Harmonisation visuelle Malio finale** (3.3) — finition esthétique inter-modules laissée au PO. --- ## ⚠️ Déploiement / migration des données — à ne pas oublier ### 1. Resynchroniser les séquences PostgreSQL après tout import/restore de dump Si la prod (ou tout environnement) est **montée depuis un dump** (`pg_restore` / `COPY`), les lignes sont chargées avec leurs `id` explicites **sans avancer les séquences** → au premier `INSERT` : `duplicate key value violates unique constraint "..._pkey"` (constaté en local sur `notification`, `task`, `time_entry`…). À lancer **juste après chaque restore/import** : ```sql DO $$ DECLARE r RECORD; maxid BIGINT; seq TEXT; BEGIN FOR r IN SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public' LOOP seq := pg_get_serial_sequence(quote_ident(r.table_name), r.column_name); IF seq IS NOT NULL THEN EXECUTE format('SELECT COALESCE(MAX(%I),0) FROM %I', r.column_name, r.table_name) INTO maxid; PERFORM setval(seq, GREATEST(maxid,1), maxid > 0); END IF; END LOOP; END $$; ``` > Ne concerne **pas** une prod qui tourne déjà (séquences avancées organiquement) — uniquement le cas restore/import. Idempotent, sans risque. ### 2. Fix dénormalisation des collections typées-contrat (code, inclus dans la branche) Les relations **to-many** typées par une interface `Shared\Domain\Contract\*` (`TimeEntry::tags` → `TaskTagInterface`, `Task::collaborators` → `UserInterface`) étaient **indénormalisables par API Platform** (mono-valué OK via IRI, collection KO) → **tout POST/PATCH portant une telle collection renvoyait 400/500**. Corrigé par un dénormaliseur générique `ContractRelationDenormalizer` (réutilise `resolve_target_entities`, zéro couplage par-entité) + test fonctionnel de non-régression.
matthieu added 66 commits 2026-06-21 17:10:04 +00:00
Plan de migration complet Lesstime vers modular monolith DDD (archi Starseed) : roadmap en 14 tickets ordonnés par dépendances + design technique détaillé du socle (Shared/, contrats, endpoints modules/sidebar, plan strangler).
Plan TDD en 4 tâches : endpoints /api/modules et /api/sidebar, garde-fou Timestampable/Blamable, helper ColumnCommentsCatalog.
First business module of Phase 2 (LST-64, rodage). Strangler-style,
additive move — no behavioural change to the public API or MCP tools.

- New module App\Module\TimeTracking (TimeTrackingModule, id "time-tracking",
  declares time-tracking.entries.view/export permissions in the RBAC catalog;
  operation security left on ROLE_USER, not re-wired here).
- Move TimeEntry entity, repository (now interface + Doctrine impl bound in
  services.yaml), ActiveTimeEntryProvider, export service/controller and the
  4 MCP TimeEntry tools into the module. #[ApiResource] (operations, security,
  uriTemplates /time_entries/*), filters and serialization groups preserved.
- Doctrine mapping "TimeTracking" added; table time_entry unchanged.
- Sidebar item gated with module "time-tracking" (SidebarFilter disables the
  route when the module is inactive).
- Timestampable/Blamable adopted (first adopter): additive migration adds
  created_at/updated_at/created_by/updated_by (nullable, FK SET NULL) +
  COMMENT ON COLUMN. Functional test confirms created_at on persist and
  updated_at refresh on update — the suspected preUpdate recompute issue does
  not occur (Doctrine ORM 3.6.2 recomputes change sets after preUpdate).

159 tests green, schema mapping valid, php-cs-fixer clean.
Companion to the backend module migration (LST-64). The Nuxt layer is
auto-detected from frontend/modules/* — no nuxt.config change needed.

- Move page, timer store, time-entries service + DTO and the 6 time-tracking
  components into frontend/modules/time-tracking/.
- Rewrite explicit service/DTO imports to ~/modules/time-tracking/* (store and
  components stay auto-imported); update the dashboard (index.vue) consumer.
- Route /time-tracking preserved; i18n keys kept in the global locale file.

nuxt build passes; /time-tracking routed.
Tranche 1 of LST-65 (ProjectManagement module migration). Decouples the
TimeTracking module from the core-business entities before they move, with
no entity relocation yet — keeps the diff minimal and the risk isolated.

- New read contracts in Shared/Domain/Contract (minimal surface, aligned on
  the entities' real nullable signatures): ProjectInterface (id/code/name),
  TaskInterface (id/number/title), TaskTagInterface (id/label/color),
  ClientInterface (id/name).
- Project/Task/TaskTag/Client implement their contract (entities stay in
  src/Entity for now). Project.client typed as ClientInterface.
- TimeEntry (TimeTracking) now references ProjectInterface/TaskInterface/
  TaskTagInterface instead of the concrete entities; repository + DQL
  untouched in behaviour.
- resolve_target_entities maps the 4 contracts to the legacy entities (will
  be repointed to the module in tranche 2).
- Adds the migration plan doc.

159 tests green, mapping valid, cs-fixer clean.
Tranche 2 of LST-65. Mechanical, behaviour-preserving move of the core
business domain into src/Module/ProjectManagement/. API operations,
securities, uriTemplates and the 38 MCP tool names are all unchanged.

- 10 entities + 2 enums moved to Domain/{Entity,Enum}; intra-module
  relations stay concrete, cross-module relations go through contracts
  (Project.client -> ClientInterface, Task/TaskDocument users ->
  UserInterface).
- 9 repositories split into Domain/Repository interfaces + Doctrine impls,
  bound in services.yaml; consumers inject the interfaces. find() kept off
  the interfaces (ServiceEntityRepository ?object compat) -> findById().
- State (7), MCP tools (38), controller, CalDavService/RecurrenceCalculator,
  3 Doctrine listeners and SwitchWorkflowOutput moved under Infrastructure/.
- doctrine.yaml: ProjectManagement mapping + resolve_target_entities of the
  3 module contracts repointed to the module (ClientInterface stays legacy).
- ProjectManagementModule registered (id project-management, 4 RBAC perms,
  not re-wired); sidebar my-tasks/projects gated by the module.
- Legacy not-yet-modularised consumers (Mail/Gitea/BookStack, Serializer,
  fixtures, tests) swapped to the module FQCN — transitional coupling to be
  cleaned in 2.4/2.5/2.6.

159 tests green, mapping valid, no API route regression, cs-fixer clean.
Tranche 3 of LST-65. Task and Project adopt TimestampableBlamableTrait.

- Additive migration on task and project: created_at/updated_at (nullable),
  created_by/updated_by (nullable INT, FK to "user" ON DELETE SET NULL) +
  indexes + COMMENT ON COLUMN. down() drops only the added objects.
- Trait fields stay out of the existing API groups (trait carries its own).
- Functional test (TaskTimestampableTest) confirms created_at on persist and
  updated_at refresh on update.

161 tests green, no destructive migration.
Tranche 4 of LST-65. Companion to the backend module migration.

- Move pages (my-tasks, projects, projects/[id]/{index,groups,archives}),
  18 components (project + task), 10 services and 10 DTOs into
  frontend/modules/project-management/ (auto-detected layer).
- Rewrite explicit ~/services/* and ~/services/dto/* imports across 38
  consumers (admin tabs, mail modals, dashboard, mail page, layout) including
  the time-tracking module whose DTOs referenced project/task/task-tag.
- clients.ts and shared DTOs (client, user-data) stay at the root.
- Routes /my-tasks, /projects, /projects/:id(/groups|/archives) preserved;
  i18n stays global.

nuxt build passes; routes confirmed.
LST-66 (2.3) backend. Behaviour-preserving move of the absences domain into
src/Module/Absence/. API operations, securities, routes and the 10 MCP tool
names are unchanged.

- 3 entities + 3 enums moved to Domain/{Entity,Enum}; user relations stay on
  UserInterface. 3 repositories split into Domain/Repository interfaces +
  Doctrine impls (bound in services.yaml); find() kept off interfaces
  (findById instead).
- Pure services (AbsenceDayCalculator, PublicHolidayProvider) -> Domain/Service;
  AbsenceBalanceService -> Application/Service; State (5), controllers (5),
  10 MCP tools and AccrueLeaveCommand -> Infrastructure/.
- New LeaveProfileInterface contract (Shared) exposes the HR getters used by
  AbsenceBalanceService/AccrueLeaveCommand; User implements it -> Absence no
  longer imports the concrete Core User. MCP tools/command inject
  UserRepositoryInterface (findById) instead of the concrete repository.
- Timestampable/Blamable added to AbsenceBalance and AbsencePolicy (additive
  migration: created_at/updated_at + created_by/updated_by FK ON DELETE SET
  NULL + COMMENT). AbsenceRequest untouched (already has createdAt/reviewedAt).
- AbsenceModule registered (id absence, 4 RBAC perms, not re-wired); doctrine
  mapping added; team-absences sidebar item gated by the module.

161 tests green, mapping valid, no API route regression, cs-fixer clean.
LST-66 (2.3) front. Companion to the backend module migration.

- Move pages (absences, team-absences), 8 components, the absences service +
  DTO and the useAbsenceHelpers composable into frontend/modules/absence/
  (auto-detected layer; composable now auto-imported).
- Rewrite consumers: AdminAbsencePolicyTab and the time-tracking calendar
  (getPublicHolidays) point to ~/modules/absence/...
- Middlewares (employee/admin) and shared services (clients, users,
  user-data DTO) stay at the root. i18n stays global.
- Routes /absences and /team-absences preserved.

nuxt build passes; routes confirmed.
LST-58 (2.4), part 1/2 — Client move. Prospect + repertoire front are pending
the product spec and will be added on this branch afterward.

- Client entity moved to src/Module/Directory/Domain/Entity; repository split
  into Domain/Repository/ClientRepositoryInterface + Doctrine impl (bound in
  services.yaml). 5 client MCP tools moved to Infrastructure/Mcp/Tool, now
  injecting the interface.
- resolve_target_entities ClientInterface repointed to Directory\Client;
  Directory mapping added; DirectoryModule registered (id directory, 2 RBAC
  perms). Client.projects relation now uses ProjectInterface -> Directory no
  longer depends on ProjectManagement.
- ProjectManagement Create/UpdateProjectTool inject Directory's
  ClientRepositoryInterface; Serializer and fixtures repointed.
- Garde-fous: #[Auditable] + Timestampable/Blamable on Client (additive
  migration: created_at/updated_at + created_by/updated_by FK ON DELETE SET
  NULL + COMMENT).

161 tests green, mapping valid, no API route regression, cs-fixer clean.
LST-58 (2.4), part 2 — Prospect (new entity). Completes the Directory backend.

- ProspectStatus enum (new/contacted/qualified/won/lost) + Prospect entity
  (name, company, email, phone, address, status, source, notes,
  convertedClient -> ClientInterface) with Timestampable/Blamable + #[Auditable].
- API: GetCollection/Get (ROLE_USER), Post/Patch/Delete (ROLE_ADMIN),
  custom POST /prospects/{id}/convert (ConvertProspectProcessor: creates a
  Client from the prospect, links convertedClient, sets status=Won; idempotent).
  SearchFilter on status.
- Repository interface + Doctrine impl (bound); 6 MCP tools (list/get/create/
  update/delete/convert-prospect); Serializer::prospect(). Module perms
  directory.prospects.view/manage. Demo fixtures (3 prospects, one converted).
- Additive migration: CREATE TABLE prospect + FKs ON DELETE SET NULL + COMMENT.

163 tests green (incl. conversion test), mapping valid, cs-fixer clean.
LST-58 (2.4) front. Completes the Directory module.

- New frontend/modules/directory/ layer (auto-detected): /directory page with
  Clients and Prospects tabs.
- Client front moved into the layer (clients service + client DTO +
  ClientDrawer). New prospects service, prospect DTO and ProspectDrawer (with
  a "Convert to client" action calling POST /prospects/{id}/convert).
- Consumers repointed to ~/modules/directory/... (admin client tab, PM project
  drawer + project pages + project DTO, time-tracking page + export drawer).
- Sidebar admin item /directory gated by the directory module; /directory
  protected by the admin middleware. i18n keys added (directory.*, prospects.*).

nuxt build passes; routes preserved.

Adds the 2.4 plan doc.
LST-67 (2.5) backend. Behaviour-preserving move of the IMAP mail integration
into src/Module/Mail/. All /api/mail/* routes, securities (ROLE_CLIENT still
excluded via MailAccessChecker) and the async sync are unchanged.

- 4 entities + 4 repositories (Domain interfaces + Doctrine impls, bound).
  TaskMailLink.task now references TaskInterface (contract) instead of the
  concrete PM Task. Link/unlink/list-mails controllers load tasks via
  TaskRepositoryInterface; MailCreateTaskController keeps the concrete Task
  (instantiation) — documented Mail->PM coupling.
- Domain (MailProviderInterface, exception), Application (5 DTOs, MailSyncService,
  MailSyncRequested message + handler), Infrastructure (ImapMailProvider +
  MimeHeaderDecoder, MailAccessChecker, 2 console commands, 12 controllers,
  ApiPlatform state + MailSettings resource). TokenEncryptor stays shared.
- doctrine mapping Mail; messenger routing repointed; services.yaml repo +
  provider bindings; MailModule registered (id mail, mail.access/configure).
- #[Auditable] + Timestampable on MailConfiguration only (additive migration);
  IMAP data entities keep their own sync timestamps.

163 tests green, mapping valid, no route regression, cs-fixer clean.
LST-67 (2.5) front. Completes the Mail module.

- New frontend/modules/mail/ layer (auto-detected): /mail page (3 columns),
  7 components, mail service + DTO, mail store (folders/messages/unread polling).
- sanitizeMailHtml util and useSystemFolderLabel composable stay global;
  AdminMailTab stays in /admin (service import repointed).
- Consumers repointed: AdminMailTab and PM TaskModal -> ~/modules/mail/...;
  the store is auto-imported (Pinia storesDirs) so the layout badge/polling is
  unchanged.
- /mail gated by the mail module: sidebar.php item with module=mail (so
  SidebarFilter disables /mail when the module is off); the layout filters /mail
  from the API sections to avoid a visual duplicate. ROLE_CLIENT exclusion kept.
- i18n key sidebar.general.mail added.

nuxt build passes; /mail and all other routes preserved.
LST-68 (2.6) backend. Behaviour-preserving move of the external integrations
into src/Module/Integration/. All 26 routes and securities unchanged.

- 5 entities (4 *Configuration singletons + TaskBookStackLink) + 5 repositories
  (Domain interfaces + Doctrine impls, bound). TaskBookStackLink.task now
  references TaskInterface (contract).
- Domain (FileSource interface, SharePathResolver, share DTOs + exceptions);
  Infrastructure (GiteaApiService, BookStackApiService, SmbFileSource, 15
  ApiResources, 21 State, 4 Share controllers).
- Cross-module couplings via abstractions: CalDavService (PM) injects
  ZimbraConfigurationRepositoryInterface; PM TaskDocument consumers repointed
  to the module's FileSource/SharePathResolver; Gitea/BookStack State load
  tasks via TaskRepositoryInterface (concrete Project read for integration
  fields — documented). ZimbraTestConnection keeps CalDavService (no build
  cycle). TokenEncryptor stays shared.
- IntegrationModule registered; doctrine mapping added.
- #[Auditable] + Timestampable on the 4 Configuration entities (additive
  migration on the 4 *_configuration tables).

163 tests green, container compiles (no cycle), no route regression, cs-fixer clean.
LST-68 (2.6) front. Completes the Integration module and Phase 2.

- New frontend/modules/integration/ layer (auto-detected): services
  (gitea, bookstack, zimbra, share, share-settings) + their DTOs, and the
  useShareStatus composable.
- Consumers repointed to ~/modules/integration/...: admin tabs
  (Gitea/BookStack/Zimbra/Share), PM task sections (TaskGitSection,
  TaskBookStackLinks, TaskDocumentShareLinker), ProjectDrawer, TaskModal,
  pages/documents.vue, components/share/SharedFilePreview.vue.
- Admin tabs, SharedFilePreview and documents/admin pages stay at their
  location (only imports updated). i18n stays global.

nuxt build passes; all routes preserved.
LST-59 (3.1) backend. New native reporting module that aggregates across
TimeTracking/ProjectManagement/Absence with ZERO direct inter-module imports —
coupled only to the physical SQL schema via read-only DBAL (AuditLog provider
pattern).

- 4 read-only reports (ApiResource + DBAL provider + readonly DTO,
  paginationEnabled false, security reporting.view): /api/reports/
  {time-per-project, time-per-user, tasks-by-status, absences-by-type}.
  All filters bound-param, dates validated YYYY-MM-DD (default = current month),
  int filters validated by regex (cs-fixer-stable).
- No Doctrine entity, no migration. ReportFilterTrait centralises validation.
  Absence status compared by literal 'approved' to avoid importing the enum.
- ReportingModule registered (id reporting, reporting.view/export perms);
  sidebar /reporting item gated by module + permission (ROLE_ADMIN section).

169 tests green (163 + 6), 4 routes exposed, cs-fixer clean.
LST-59 (3.1) front. Completes the Reporting module.

- New frontend/modules/reporting/ layer (auto-detected): /reporting page
  (admin middleware) consuming the 4 read-only report endpoints.
- Filters (period presets + custom dates, project, user). 4 sections (time per
  project, time per user, tasks by status, absences by type) each with a
  DataTable + a Chart.js chart (reused global registration).
- Front-side CSV export per section (useCsvExport: BOM UTF-8, ; separator).
- i18n keys (reporting.*, sidebar.admin.reporting).

nuxt build passes; /reporting routed; no route regression.
LST-69 (3.2) phase 1. New ClientPortal module + security foundations for the
client portal (spec docs/superpowers/specs/2026-03-15-client-portal-design.md).

- Security: User::getRoles() no longer adds ROLE_USER to ROLE_CLIENT users;
  role_hierarchy ROLE_ADMIN: [ROLE_USER, ROLE_CLIENT]. Existing Task/Project/
  Client/TimeEntry/metadata endpoints already required ROLE_USER -> a pure
  ROLE_CLIENT is walled off (verified: 403).
- User (Core): client (ManyToOne ClientInterface, SET NULL) + allowedProjects
  (ManyToMany ProjectInterface). UserInterface extended (getClient/
  getAllowedProjects).
- New ClientTicket entity (module ClientPortal) + enums + repository + API with
  per-client isolation (ClientTicketProvider: own tickets ∩ allowedProjects),
  per-project numbering under advisory lock (rejects if user.client null),
  status transition rules. ClientTicketInterface contract for Task/TaskDocument.
- TaskDocument generalized: task nullable + clientTicket (CASCADE) + CHECK;
  per-role access. Task.clientTicket exposed in task:read.
- Additive migration; demo client fixtures.
- Tenancy tests assert the isolation invariant (a client never sees another
  client's tickets) rather than brittle absolute counts (shared test DB).

178 tests green, mapping valid, cs-fixer clean.
Security hardening on the document POST that phase 1 widened to ROLE_CLIENT:
a client user could reach the share-link path (arbitrary SMB file reference)
instead of an upload. Now the sharePath branch is admin-only — client users
must upload. attachTarget already scopes documents to the client's own ticket.

178 tests green.
LST-69 (3.2) front. Client portal UI on the phase-1 backend.

- New frontend/modules/client-portal/ layer: /portal (project cards from the
  client's allowedProjects via /me), /portal/projects/[id] (tickets list,
  detail modal, create modal with document upload), client-tickets service +
  DTO, CT-XXX formatting.
- Front tenancy: auth.global.ts redirects a pure ROLE_CLIENT to /portal and
  blocks internal routes; portal pages open to any authenticated user.
- Admin: UserDrawer manages client accounts (ROLE_CLIENT + client +
  allowedProjects); new "Tickets client" admin tab (list, filters, status
  change with required comment on reject, detail modal).
- Kanban/my-tasks: client-ticket icon + tooltip when task.clientTicket is set
  (data via task:read, no extra call). TaskDocument upload generalized with a
  clientTicketId prop. getContent uses native fetch (text response).
- i18n portal/clientTicket keys; sidebar /portal item (module client-portal).

nuxt build passes; /portal routes present, existing routes intact.
LST-69 (3.2) phase 3. Wires the existing notification system to client-ticket
events (the bell/useNotifications/endpoints already existed).

- Notification.relatedTicket (ManyToOne ClientTicketInterface, SET NULL) +
  additive migration + notification:read group.
- NotifierInterface::notify() gains a backward-compatible optional
  relatedTicket param (existing callers unchanged).
- ClientTicketNumberProcessor (POST): notifies all ROLE_ADMIN users
  (ticket_created), tolerant try/catch after flush. ClientTicketStatusProcessor
  (PATCH): notifies submittedBy on status change (ticket_status_changed).
- Front: notification DTO relatedTicket; NotificationBell navigates to /admin
  (admin) or /portal (client) on ticket notifications.

180 tests green (178 + 2), nuxt build passes, cs-fixer clean.
LST-60 (3.3). Closes the modular-monolith migration. src/Entity was already
empty; this removes the last legacy residue.

- Doctrine: drop the legacy "App" mapping (empty src/Entity). resolve_target_
  entities already targets modules only.
- MCP User tools (Reference/) -> Core/Infrastructure/Mcp/Tool; MCP Serializer
  -> Shared/Infrastructure/Mcp (33 usages repointed).
- Controllers (mark-all-read, notification unread-count, regenerate-api-token,
  user-avatar) -> Core/Infrastructure/Controller. TokenEncryptor -> Shared/
  Infrastructure/Service (11 usages). AppVersion resource+provider -> Shared.
  ContractType enum -> Core/Domain/Enum.
- src/{Entity,State,Controller,Service,Enum,ApiResource} now empty; routes,
  MCP tool names and public API unchanged.

180 tests green, mapping valid, no route regression, cs-fixer clean.
Note: final Malio visual harmonisation (subjective) left to the PO.
matthieu added 1 commit 2026-06-21 17:31:38 +00:00
Findings from the post-migration code review. The arrival of ROLE_CLIENT
exposed internal endpoints still guarded only by IS_AUTHENTICATED_FULLY (or no
security), reachable by a client. Verified by re-running a multi-role smoke
test (client -> 403, internal roles -> 200).

Security (closed real client-isolation holes):
- TaskDocumentDownloadController: add ownership check (admin all / client only
  own clientTicket docs / user only task-linked docs) — the custom download
  bypassed the cloistered provider.
- Share browse/download/search/status controllers: IS_AUTHENTICATED_FULLY ->
  ROLE_USER (SMB share is internal).
- User Get/GetCollection: add security ROLE_USER (was exposing the internal
  directory to clients).
- BookStackLink GetCollection/Post/Delete: IS_AUTHENTICATED_FULLY -> ROLE_USER.

Contracts / robustness:
- TaskInterface gains getProject(): ?ProjectInterface; TimeTracking export
  controller/service drop concrete cross-module entities for repo interfaces.
- Shared MCP Serializer signatures widened to the contracts (user/projectRef/
  taskRef/tags/users); project()/userFull()/etc. kept concrete (use getters
  outside the contracts).
- RecurrenceHandler: null-guard before findMaxNumberByProjectForUpdate().

180 tests green, cs-fixer clean, routes unchanged.
matthieu added 1 commit 2026-06-21 17:33:45 +00:00
Data-provided test asserting a pure ROLE_CLIENT gets 403 on the internal
endpoints hardened after the review (/api/users, /api/share/browse,
/api/share/status, bookstack links), so the fixes can't silently regress.
matthieu added 6 commits 2026-06-22 07:07:11 +00:00
matthieu added 1 commit 2026-06-22 07:50:04 +00:00
refactor(client-portal) : remove client portal feature entirely
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m11s
Pull Request — Quality gate / Frontend (build) (pull_request) Successful in 1m17s
a18e1f575f
- drop ClientPortal module, ClientTicket entity, ROLE_CLIENT and all couplings (Task, TaskDocument, User, Notification) back to an internal-only model

- migration drops client_ticket / user_allowed_projects / related FK columns and removes leftover external client accounts (would otherwise be promoted to ROLE_USER)

- remove client-portal frontend module, admin tickets tab, user portal section, portal nav item and portal/clientTicket i18n keys

- fix directory nav icon (invalid mdi:contact-multiple-outline -> mdi:card-account-details-outline)

- add 'make sync-permissions' target, wire it into install/db-reset and the prod deploy script
matthieu added 20 commits 2026-06-22 12:17:30 +00:00
docs : add directory commercial reports spec and implementation plan
Pull Request — Quality gate / Frontend (build) (pull_request) Successful in 41s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 58s
1589908e4c
matthieu added 1 commit 2026-06-22 15:28:50 +00:00
fix(api) : register #[ApiFilter] services by mapping module entity paths
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m9s
Pull Request — Quality gate / Frontend (build) (pull_request) Successful in 1m20s
f6d37e4667
Modular monolith moved entities out of src/Entity into src/Module/*/Domain/Entity
without configuring api_platform.mapping.paths. Resources stayed discoverable via
service autoconfiguration, but annotated filter services were registered only for
classes found in resource_class_directories (the now-empty default src/Entity and
src/ApiResource), so every #[ApiFilter] (SearchFilter, BooleanFilter, OrderFilter,
DateFilter) was silently ignored across the whole API — collection filters never
narrowed results (my-tasks showed all users' tasks, time entries leaked across
users, directory would leak per-client data).

Declare the seven module entity directories under mapping.paths so the annotated
filter services are generated again.
matthieu added 2 commits 2026-06-23 10:15:06 +00:00
TaskModal now emits the fresh task returned by the API (same task:read
shape as the collection). The board, my-tasks and archives pages reinject
it into their local state and selectedTask before the background re-fetch,
so the list and a reopened modal no longer show the previous snapshot while
loadData() is still running.
fix(directory) : refresh clients list after converting a prospect
Pull Request — Quality gate / Frontend (build) (pull_request) Successful in 1m13s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m32s
bfbab5bbf2
Converting a prospect creates a client and removes the prospect, but only
loadProspects() was called, so the new client did not appear in the Clients
tab until a manual page refresh. Both the table convert button and the
ProspectDrawer saved event now reload prospects and clients.
matthieu added 1 commit 2026-06-23 13:43:15 +00:00
fix(project-management) : make my-tasks kanban drag-drop status change instant
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m21s
Pull Request — Quality gate / Frontend (build) (pull_request) Successful in 1m28s
ee9b751a1f
matthieu merged commit 8313c759c6 into develop 2026-06-23 13:50:43 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: MALIO-DEV/Lesstime#17