# Existing System Landscape — PCM / Product Domain (code-forensics report)

Date: 2026-08-10 · Method: static code analysis of 5 repos + read-only queries against the live legacy DB (`astara_cockpit` on 192.168.200.4:3307, via PDO). No code modified.
Every claim below is cited to a file or to a live DB query. Items that could not be proven are marked **UNKNOWN**.

---

## 1. Current Laravel app (`/mnt/devel/md/product-db`) — verified inventory

**Stack (composer.json):** `laravel/framework ^13.17`, `inertiajs/inertia-laravel ^3.0`, `laravel/fortify ^1.37.2`, `laravel/wayfinder ^0.1.14`, `laravel/chisel`, Pest 5. `package.json`: React 19 + TS + Tailwind v4 + shadcn/ui + AG Grid v36. Frontend is the ACE Cockpit dashboard ported 1:1 from `../product-db-react` (AGENTS.md, DESIGN.md).

**Models** (`app/Models/`): only `User.php` — `Authenticatable implements PasskeyUser` with `PasskeyAuthenticatable` + `TwoFactorAuthenticatable` traits. **No domain models exist.**

**Controllers** (`app/Http/Controllers/`): only base `Controller.php` and `Settings/ProfileController.php`, `Settings/SecurityController.php`. No cockpit/domain controllers.

**Routes** (`routes/web.php`): all cockpit pages are `Route::inertia(...)` static renders (no controller, no data):
- `/` → `cockpit/dashboard` (name `home`)
- `/inbox`, `/inbox/{messageId}` (closure passes `messageId` prop), `/links`, `/documents`, `/documents/new`, `/compose`, `/admin`
- `auth,verified` group → `/dashboard` (starter-kit only)
- `routes/settings.php`: `/settings/profile` (+PATCH/DELETE), `/settings/security` (+password PUT, throttle 6:1), `/settings/appearance`, `.well-known/passkey-endpoints`

**Migrations** (`database/migrations/`): starter kit only — users, cache, jobs, passkeys, 2FA columns. **No domain tables.**

**Tests** (`tests/`): `Feature/Auth/*`, `Feature/Settings/*`, `DashboardTest.php` (guest redirect / authed 200). No domain tests.

**Frontend inventory** (`resources/js/`):
- `app.tsx` layout switch: `cockpit/*` → `CockpitLayout`, `auth/*` → `AuthLayout`, `settings/*` → `[AppLayout, SettingsLayout]`, else `AppLayout`.
- Pages `pages/cockpit/**` are thin wrappers over `components/views/*`: `dashboard-view`, `inbox-view`, `message-detail-view`, `compose-view`, `documents-view`, `add-document-view`, `links-view`, `admin-view`.
- Mock data in `lib/data.ts` (no backend — AGENTS.md: "no backend yet, do not wire forms to real endpoints"): `BRANDS` (Abarth, Alfa Romeo, Fiat, Fiat Professional, Hyundai, Jeep, KGM, MAXUS, Nissan), `MESSAGES`, `DASHBOARD_INBOX`, `DOCUMENTS` (ids 7,8,10,12,18,27,32, dated 2025-12→2026-02, "0063.26_Sommerreifen Saison 2026" style), `LATEST_DOCS`, `FREQUENT_LINKS`, `PRICELISTS`, `VISITS`, `FOLDER_TREE` (Sales→Circular letter/Fleet/Used cars/Stock list/Sales promotions/Priorities/Dealer events; Aftersales 50; Marketing 88; Product 56), `LINKS_TABS` (aftersales/dnd/marketing with Helpdesk Astara, PDC xRelation, Snap on, BMI, DRS Skopos, LMS), `INBOX_CATEGORIES` (Sales 693, Aftersales 890, Marketing 388), `MESSAGE_DETAILS` (status "Published", openRate, attachments), `BRAND_ABBR`.
- `components/layout/cockpit-sidebar.tsx` NAV_ITEMS: **Dashboard, Inbox, Campaign Portal, Content Management, DIAS, Reporting, Links, Documents, Pricelists, Administration** (sections Management/Material/System).
- `inbox-view.tsx` chrome tabs: Inbox 3151 / Scheduled 1 / Archive 167 / Drafts 28 — counts mirror the legacy notification statuses (see §2.4).
- `admin-view.tsx`: profile + notification category email-pref switches (Sales/Aftersales/Marketing/Product) — mirrors `aws_notification_email_pref_categories` concept.
- `compose-view.tsx`: brand pills ("Leave empty to send without brand restriction"), Category select (Circular Letter / Guidelines), High Priority switch, Sender, Scheduling date/time — mirrors the legacy notification compose form (§2.4).
- `add-document-view.tsx`: DE title REQUIRED chip, FR/IT OPTIONAL, brand multi-select, multi-file upload — mirrors legacy documents create (§2.5).

**Starter-kit features present:** Fortify auth (login/register/2FA/passkeys/email verification) via `app/Providers/FortifyServiceProvider.php` + `app/Actions/Fortify/*`; settings pages (profile, security, appearance). Cockpit pages are **public** (no auth middleware) per AGENTS.md.

---

## 2. Legacy cockpit (`/mnt/devel/md/cockpit`) — modules & business rules

### 2.1 Architecture (AGENTS.md, api/index.php, lib/session.php)
- Frontend: `/index.php/<component>`; bootstrap `lib/session.php` (auth, session, GDPR gate, lang de/fr/it, ACL). Components in `component/<name>.php`; JS at `js/<name>.js`.
- API: `/api/ClassName/action/param` via `api/index.php` — HTTP verb → `$api->get()|post()|put()|patch()|delete()`. Auth chain: session → `X-API-KEY`/`api_key` (api_keys) → temporary tokens (whitelist `$allowedEndpoints`: vehicleProviderOffers, dwsModels, Campaigns, vehicleProviderTranslations, Documents, Register, dealers) → bearer (`api_tokens`). Public methods whitelist `$publicMethods` (api/index.php:77): Register, PasswordReset, Support, decodeDealer, workshopID, hyundaiProcessor, OfferPlaceholders, leadProcessor, dwsInventoryWidget.
- i18n: `language/{lang}.ini` + `.json` (de/fr/it), `{KEY}` placeholders, `lib/Translator.php`.
- Cron hits API via curl (AGENTS.md): `cron/drain_mail_jobs.php` (every minute), `cron/user_inactivity.php` (daily), `api/notifications/send-scheduled` (hourly), `api/dealer_notification_trigger.php` (daily). SSE push via `api/push_stream.php`.
- DB: `astara_cockpit`, `aws_` prefix. Live DB table list (read-only query) includes ~300 tables: `aws_brands`, `aws_offers`, `aws_services`, `aws_campaigns`, `aws_offers_bkp_080726` (backup convention), `entity_content_assets`, `aws_entity2dealer`, `aws_entity2models`, `aws_campaigns_excl_dealers`, `aws_notifications*`, `aws_documents*`, `aws_users`, `aws_acl`, `dealer2brands`, `aws_dealers`, `ACE__*` staging tables, `offer_placeholders`, `api_tokens`, `lead_logs`, etc.

### 2.2 Functional modules (component/ + menu)
Main menu (modules/menu/menu-lte.html href list): dashboard, offers, services, campaigns, notifications, doks (documents), urls (links), dealers (DIAS), users, administration, groups, acl, user2group, pricelist, contacts, invoicing, import, import_offers, import_sap, master, modele, aftersales_models, analytics, announcements, support, pending_users, leads_logs, versions, contentful_data, contentful_log, dws_list, dealer_discounts, data_protection, category2group, category2dealer_type, user_audit_logs, showtoken, inbox_populate, sap_data(_ital/_nissan), logs, debuginfo. Legacy Polish modules (wnioski, zgloszenia, akcesoria, czesci, tir, kodyoperacji…) remain in component/ but are outside the ACE scope.
Dashboard tiles (component/dashboard.php:80-98 + modules/): `frequently_used_links`, `recent_campaigns` OR `recent_offers_services` (slot by ACL), `recent_pricelists`, `latest_documents`, `latest_messages`, `activity_tracker` — the direct ancestor of the new dashboard-view.

### 2.3 Offers / Services / Campaigns (api/Campaigns.php, 3526 lines)
**Tables (live schema):**
- `aws_offers` / `aws_services`: `id_*`, `hash` (varchar 64), `status enum('Draft','Published','Internal')` default Draft, `market enum('CH','AT')`, `id_brands`, `old_brand`, `energy char(1)` (A–G), `teaser_title_DE/FR/IT`, `teaser_description_DE/FR/IT`, `teaser_image` + `teaser_real_name_image` + `teaser_ext_image`, `detail_page_image` + `detail_page_real_name_image` + `detail_page_ext_image`, `created_by/at`, `updated_by/at`, `published_by/at`, `ext_id` (Contentful entry ID), `legacy`, `scheduled_date`, `global`, `prio`, `hidden_dws`, `aftersales`.
- `aws_campaigns`: same teaser/detail/status/market columns + `dealers` text (dealer-includes list from Contentful JSON, see ACE__campaigns.sql payloads), `image_hash`, `teaser_image_url`, `prio`, `hidden_dws`, `scheduled_date`.
- `entity_content_assets`: `entity_type enum('campaign','service','offer')`, `entity_id`, `section_key`, `locale`, `asset_type enum('html','css','project')`, `content longtext` — GrapeJS-generated content. Live counts: offers html/css/project 86/86/88, services 52/52/69, campaigns 31/31/256 (project) — queries run read-only.
- `aws_entity2dealer` (entity_type enum('offers','services'), entity_id, dealer_id, published) — per-dealer visibility; `aws_entity2models` (type enum('offers','campaigns'), id_models); `aws_campaigns_excl_dealers`; `aws_campaigns2dealers` (older include-list).

**Business rules (cited):**
- Statuses enum `['Published','Draft','Internal']` (Campaigns.php:38 `$entityStatuses`); `Internal` only settable by superuser (validateRequestedEntityStatus, Campaigns.php:827-829: `if ($requestedStatus === 'Internal' && !$this->isSuperUser())` → 400).
- Required fields on create: Brand + teaser_headline_DE + teaser_subheadline_DE + teaser image (validateEntityRequiredFields, Campaigns.php:776-813; max image 660×330, Campaigns.php:15-16).
- DWS public endpoint `GET /api/Campaigns/dws_content/{dws-domain}` (getContentForDws, Campaigns.php:1333): resolves dealers via `dealer2brands.dws_url`, returns grouped `offers|services|campaigns`; public requests include **only `status='Published'`**; `show_internal=1` additionally includes Internal + Draft; offers/services scoped via `aws_entity2dealer`; campaigns scoped via `aws_campaigns_excl_dealers` exclusion; `COALESCE(hidden_dws,0)=0` filter; ordering `priority DESC, created_at DESC`; Fiat (brand 1) dealers also receive Abarth (brand 3) content (expandDwsBrandIds, Campaigns.php:1497-1499).
- Single-content endpoint `GET /api/Campaigns/dws_content/{offers|services|campaigns}/{id-or-ext-id}` (getSingleContentForDws, Campaigns.php:1547): joins entity table + `entity_content_assets`; Published-only unless `show_internal=1`; 404 `{"error":"DWS content not found","html":"","css":""}` when not publicly available (docs/campaigns.md:72-98).
- Unpublish → sets status `'Draft'`, does **not** delete `entity_content_assets` (docs/campaigns.md:100-104).
- Dealer-scoped offers/services: dealer users create/modify only their dealer-scoped entities (`buildDealerEntityScopeSql`, `canDealerModifyEntity`, `canDealerTogglePublication`, `syncDealerEntityPublication`); main-entity publication syncs dealer rows (`syncMainEntityPublicationStatus`).
- Campaigns may be linked to models (`aws_entity2models`) and carry embedded request-quote forms (`getCampaignFormsForDws` — Contentful `request-quote-form` modules with `offerFor` IDs, visible in ACE__campaigns.sql JSON).
- Image URLs: `{live_site}uploads/{type}/{hash}/{teaser_image}.{teaser_ext_image}` (Campaigns.php:1434-1447).
- UI (component/campaigns.php / offers.php / services.php + js/entities.js): filter chips for statuses (Draft/Published/Internal) and global filter (All/Global/Dealer), brand filter per user, models modal, energy A–G, preview domains per brand via `testdealer.*` DWS domains (e.g. `testdealer.garage.fiat.ch`, `testdealer.hyundai.ch`); "New Campaign" button gated by ACL `$ACL[$entity_type]['nw']`; `canSaveInternal` = ADMIN or groups 7/42.
- Live status distribution (query): offers 50 Published CH + 1 AT, 35 Draft CH + 1 AT; services 42 Published CH + 7 AT, 19 Draft CH + 1 AT; campaigns 100 Published CH + 9 AT, 139 Draft CH + 8 AT.

### 2.4 Notifications (api/notifications.php, 9433 lines)
**Tables (live schema):** `aws_notifications` (id_notifications, `id_notification` varchar 50 — Contentful ID, `category` int, `priority`, `dealers_only`, `status` varchar default 'Published', sender, sender_id, published_at, is_legacy), `aws_notification_translations` (locale de/fr/it(-CH), title, executive_summary, description, content_html), `aws_notification_brands` (notification_id, brand_id), `aws_notification_assets` (asset_id, lang, asset_name, stored_name, relative_path, mime_type, file_size, uploaded_by/at), `aws_notification_excluded_dealers`, `aws_user_notifications` (id_user, notification_id, is_read, is_trash, is_deleted, read_at, first_opened_at — per-user inbox snapshot), `aws_scheduled_notifications` (scheduled_at, send_email_copy, override_email), `aws_notification_mail_jobs` (status enum('pending','running','done','error'), per-lang subject/body/preheader, recipient counts), `aws_notification_email_prefs` + `_categories` + `_brands`, `aws_notification_revision*` (edit history).

**Business rules (cited):**
- Statuses in live DB: Published 3492, Hidden 14, Draft 18, Scheduled 1 (read-only query). Docs (agent-notifications.md): admins = notifications ACL nw+wr+rm; non-admins see only rows materialized in `aws_user_notifications`; Draft/scheduled-draft keep admin rows only; Published adds non-admin rows; Trash rewrites recipients to admins; permanent delete removes recipient rows but keeps data in `aws_notifications`.
- Recipient sync uses `aws_user2brand`, `aws_user2dealer`, `dealer2brands`, `aws_notification_excluded_dealers` → materializes `aws_user_notifications` (syncNotificationRecipients, notifications.php:4177; populateInboxRowsForUser 3961; batchPopulateInboxRowsForUser 4032).
- `dealers_only` affects recipient sync, not runtime filtering (agent-notifications.md).
- Category ACL: `aws_category2group` (group→category) and `aws_category2dealer_type`; category ids are **document-group ids** (`aws_documents_groups_new` — getNotificationCategoryPathLabelForEmailLang, notifications.php:1773-1803); category with no c2g entries = unrestricted (notifications.php:3755).
- Scheduling: `aws_scheduled_notifications`; cron `POST /api/notifications/send-scheduled` sends every due row where `scheduled_at <= now`, publishes, clears scheduled row, re-syncs recipients (docs/notifications-external-api.md:207-214).
- Mail: `send_email_copy=1` → author confirmation e-mail; `send_to_users=1` → fan-out to all `aws_user_notifications` recipients; admin e-mail prefs (email_lang, category/brand allow-lists) applied; bulk fan-out with dedupe, per-recipient not BCC (agent-notifications.md:26-36). `aws_notification_mail_jobs` drained by `cron/drain_mail_jobs.php`.
- External publish API (docs/notifications-external-api.md + lib/notifications_external_auth.inc.php): `POST /api/notifications/digitas-token` (client credentials → `api_tokens.name='digitas'`, 8h lifetime), `POST /api/notifications/send-external` (create+publish only; JSON; **title.de, exec_summary.de, message_html.de mandatory**; `brand_ids` numeric, invalid ids rejected, empty = unrestricted; runs as fixed service user with notifications ACL + brand scope). Known brand ids: 1=Fiat, 2=FiatPro, 3=Abarth, 4=Jeep, 5=AlfaRomeo, 6=Hyundai, 7/8=Nissan, 10=KGM, 11=Maxus.
- Full endpoint surface (notifications.php:3288-3389): GET list/read/email-prefs/user-brands/filters/groups/dealers/asset/trash-count/unread-count/mail-job-drain/edit-log/opened-dealers/populate-stream; POST unpublish/send/digitas-token/send-external/send-scheduled/email-prefs/publish/mark-read(-bulk)/mark-unread(-bulk)/delete/upload-asset/track-attachment/delete-asset/delete-bulk/hide(-bulk)/duplicate/move-bulk/recover(-bulk)/mark-all-read.
- Notification `has_attachments` must be true when uploaded assets or linked documents exist (agent-notifications.md:65-66); deletes are "hard-delete/trash logic implemented by feature", not confirmed soft-delete (agent-notifications.md:68).

### 2.5 Documents (api/Documents.php, 8164 lines)
**Tables (live schema):** `aws_documents` (id_documents, id, title_DE/FR/IT, description_DE/FR/IT, brand, market, uploadDate, documentId, group, rootGroup, rootGroupId, virtualFullPath, id_group, brand_id, pinned, priority, legacy, active, originate, id_originate, vpath_de/fr/it/en, indexed_at, main_dealers), `aws_document_files` (documentId, doc_id, realname_de/fr/it, extension), `aws_documents_groups_new` (id, root_id, parent_id, brand, hash, hash_cockpit, name_DE/FR/IT/EN, level, sort_order, active), `aws_documents2brands`, `aws_documents2user_groups`, `aws_documents2notifications`, `aws_dealer_type2document_group`, `aws_document_ai_index`, `aws_document_ai_jobs`, `aws_doks_searches`.
Live top-level groups (query): Sales, Aftersales, Marketing, Product, ACE Administration, ACE Digital & CX, ACE Preislisten, ACE Dealer Development, ACE Training; children e.g. Sales→Circular letter/Fleet(Used cars,Stock list)/Sales promotions, Aftersales→Type approvals/Newsletter, Marketing→Campaigns/CI — the exact tree mirrored in the new UI's FOLDER_TREE mock.

**Business rules (cited):**
- List visibility (getDocumentsList, Documents.php:221+): document must belong to an **active** group (`aws_documents_groups_new.active=1`); dealer users (non-main) cannot see `main_dealers=1` docs; group filter expands to descendants (getDocumentGroupWithDescendantIds); category ACL via `aws_category2group` + brand content scope (dealer_user_resolve_effective_category_ids); brand filter intersected with user's authorized brands (superusers 7/42 get all unless `aftersales_content_only` brand, e.g. MG); special groups "new" (max 20) and "all".
- Search: `searchDocuments` (Documents.php:2134) with `aws_document_ai_index/jobs` + Indexer.php API; previews for PDF/ICS/XLS/ZIP (readArchiveEntries, parseIcsEvents, readSpreadsheetPreview, buildDocumentPreviewUrl).
- Create/update: validateDocumentData (3847) incl. `rejectExcessiveUploadSize` (3828); DE file mandatory (hasUploadedDocumentFileDe / existingDocumentFileDeExists); saveFiles → `assets/documents/new/...` + metadata into aws_document_files; saveDocumentBrands; moveDocumentsToFolder (2560) moves physical files + reindexes (reindexMovedDocuments 3447).
- Document↔notification links: `aws_documents2notifications` / `getDocumentsForNotification` (2461).
- Dev/prod migration tool: `document_migration.php` (CLI) syncs aws_documents between dev and prod via `api/migrationTool` (migrationTool.php) — used for the ACE documents rollout.

### 2.6 Links / URLs (api/urls.php + ACE__links)
**Tables (live schema):** `ACE__links` (sort_order, id_links, link_group, code, name_de/fr/it, brand, url, description_de/fr/it, market, advanced, last_modified_by/date), `ACE__links_groups` (id, name_de/fr/it). API surface: getGroups (440), getLinks (503), saveLink (642), reorderLinks (800), deleteLink (836), trackLinkClick (876); aftersales-only groups hidden from sales-only users (isAftersalesGroupId 154, isAftersalesOnlyUser 387); brand display filtered by user's authorized brands (applyBrandDisplayFromRelation 168). Group tabs in the new UI (Aftersales/DND/Marketing) mirror the legacy link groups.

### 2.7 Dealers / DIAS (api/dealers.php, 259 KB; dealer2brands)
- `aws_dealers`: name, legal_name, website_url, sap_soldto/shipto, digitas_code, group_id_dws, payer, area/country, search_provider*, autolina_id, autoscout_id, carmarket_id, advert_right, showroom/workshop sizes, accepted_date/accepted_by, rejection_comment, change_reason.
- `dealer2brands` (per brand-dealer row): oem_code, warranty_no, salesforce_id(+add), workshop_id(+add), **dws_url** (+add), group_id_dws, dealer_language, available_languages, address/post_code/vat, contract_type, sales_cont_date, aftersales_cont_date, dealer_type, parent, main_dealer_info, contract_end/termination dates, cockpit_status, cockpit_charges, ci_status/ci_date, sales/aftersales_dms, awards, zones + zone managers, showroom, acception_status, active, domain_published, termination_email_sent dates, moccamedia_dws_notified_url, days_to_termination, special_contract, lat/lng, post_code_list, gtm/gtag ids, active_languages, meta_id, telephone_tracking_number.
- API: getDealersData (1049), saveDealer (1732), acceptDealer/rejectDealer (3837/4108 — acceptance workflow with temp tables + side effects incl. DWS DNS/live notifications), quickStatusUpdate (3445), terminateNow (2781), sendDealerNotification (2894), geocoding (2576), post-code list validation (2662, conflicts 2674).
- DIAS = the dealers/dealer-management area: `lib/dealer_dws_dns_notification.php:3` "Automatic DNS A-entry request emails when a DWS URL is accepted in DIAS"; new UI sidebar item "DIAS" maps to this component.
- Dealer-scoped user model: `aws_user2dealer`, `aws_user2brand`, `aws_user2dealer2brand2type`, `dealer_user_brand_type.inc.php` (dealer_user_component_allowed_for_user gates component access, e.g. component/offers.php:10-14); `aws_dealer_types` has `aftersales_content_only`, `hide_sales_modules`.

### 2.8 Users / ACL / auth
- `aws_users`: login, phpass password, nazwisko, email, last_login_at, blokada (lock flag), dealer, gdpr_accepted(+at/version); groups via `aws_user2group` + `aws_groups`; special groups: **7 = superuser (ADMIN), 42 = internal-access group, 10 = dealer** (lib/session.php:194-238, component/campaigns.php `canSaveInternal`).
- ACL: `aws_acl` per (appname, funkcja = component name, grupa) with flags **rd / wr / nw / rm** (read / write / create-new / remove), OR-ed across user's groups into `$_SESSION['ACL']` (lib/session.php:320-337).
- Category ACL: `buildUserCategoryAcl` (lib/session.php:26) — group → `aws_category2group` → document-group ids with English names.
- GDPR gate: `lib/gdpr_consent.inc.php` blocks API and components until consent accepted.
- users API (api/users.php): create/login generation (generateLoginCandidates, buildUniqueLogin), group/brand/dealer assignments with dealer-admin restriction (isDealerAdminContextRestricted, getAllowedDealerAssignableGroupIds), audit events (logUserAuditEvent → aws_user_logs / user_audit_logs component).

### 2.9 Multi-language (de/fr/it) model
- Content columns are per-language: offers/services/campaigns `teaser_title_DE/FR/IT` + `teaser_description_DE/FR/IT`; documents `title_DE/FR/IT`, `description_DE/FR/IT`, `vpath_de/fr/it/en`, files `realname_de/fr/it`; links `name_de/fr/it` + `description_de/fr/it`; notifications translations rows per locale `de`, `fr-CH`, `it-CH` (handover_import.sql uses de/fr-CH/it-CH; normalizeNotificationLocale in api/notifications.php:123); document groups `name_DE/FR/IT/EN`.
- **DE is mandatory** on entity create (Campaigns.php:796-806) and notification external API (title.de/exec_summary.de/message_html.de); FR/IT optional. The new UI encodes this as the yellow "REQUIRED" DE card (DESIGN.md, add-document-view).
- UI strings: `language/{lang}.ini`/`.json` with `{KEY}` placeholders; session `lang` limited to de/fr/it (api/index.php:22-25); per-user email language `aws_notification_email_prefs.email_lang` (default de).
- Campaign content JSON uses per-market-per-locale keys `CH_de/CH_fr/CH_it` (ACE__campaigns.sql payloads).

---

## 3. Data origin: source → import → cockpit DB (`/mnt/devel/md/digitas`)

**Sources (summary.json):** Contentful export with two spaces:
- `ace` — spaceId `8716ll81c9fs`, env `master`, locales de-CH/fr-CH/it-CH (mode preview)
- `dealer_website` — spaceId `wrkkfp3wx9rg`, locales de/fr/it
- Handover totals: documents 2923 written (19 categories, 57 missing locales), notifications 3894, offers 82 (148 source entries, 66 reclassified as services), services 66 (classification strategy `offer.type`, token `service`), campaigns 244. Assets hosted at `images.ctfassets.net` / `assets.secure.ctfassets.net` (URLs in ACE__campaigns.sql and summary.json errors).

**import_offers_services.py** (`/mnt/devel/md/digitas/import_offers_services.py`):
- Input: Contentful export folders `offers/` and `services/` (per-brand subfolders; each item has `metadata.json` + `content.json` with `context.brand`/`context.market`, `teaserHeadline`, `teaserSubheadline`, `energyLabel`, `assets[]` with sourceField `fields.teaserImage`/`fields.detailsImage`, per-locale assets).
- Output: `output/aws_offers_import.sql` + `output/aws_services_import.sql` (columns: hash, status, market, id_brands, energy, teaser_* DE/FR/IT, images, timestamps, ext_id, legacy=0, scheduled_date, global, prio) + assets copied to `output/uploads/{type}/{hash}/`.
- Brand map: Fiat=1, FiatPro=2, Abarth=3, Jeep=4, AlfaRomeo=5, Hyundai=6, Nissan=7, Nissan AT=8 (market AT), MgMotor/MG=9, KGM=10, Maxus=11 (script BRAND_MAP).
- STATUS_MAP: draft→Draft, published→Published, archived→Internal.
- Idempotence: parses previous SQL for `ext_id→hash` pairs and skips already-imported entries.

**convert_contentful_to_grapesjs.py**:
- Converts Contentful entries (bodyCopyData, disclaimerData, teaser, detail images) into GrapeJS project JSON + HTML + CSS (CSS_TEMPLATE blocks: multilang-textfield, heroimage, banner-large) → `output/entity_content_assets_import.sql`; `BASE_URL = https://cockpit.astara-partner.com`.

**handover_import.sql** (5.3 MB, generated by the handover tool — script itself not in repo):
- `START TRANSACTION`; first creates `backup_aws_notifications*` tables; then inserts **notifications only**:
  - `aws_notifications (id_notification, category, status, is_legacy, priority, sender, published_at)` — category is an **int** (values seen: 9; mapping to document-group ids), status 'Published', is_legacy=1, sender 'Handover Import' (statement counts in file: 221 notifications, 660 translations, 541 brand links, 10,342 assets)
  - `aws_notification_translations (notification_id, locale de|fr-CH|it-CH, title, executive_summary, content_html)` (660)
  - `aws_notification_brands (notification_id, brand_id)` (541)
  - `aws_notification_assets (notification_id, asset_id, lang, …)` (10,342 asset rows)
  - All statements `ON DUPLICATE KEY UPDATE` (idempotent re-run).
- Content is real dealer communication (e.g. "Deaktivieren von Astara Platform Benutzer", IT Helpdesk) with per-locale titles/summaries/HTML bodies.

**handover_cp.sh** (7.2 MB generated bash): copies notification attachment files from `/mnt/devel/md/digitas/notifications/{Category}/{entry}/de-CH|fr-CH|it-CH/attachments/*` (Contentful export layout, folders no longer present) to a desktop folder keyed by asset hash — proves attachments were downloaded from Contentful and imported per-asset.

**Other origin artifacts:**
- Cockpit staging tables `ACE__*` (ACE__campaigns, ACE__offers, ACE__documents, ACE__notifications, ACE__links, ACE__models, ACE__dealers, ACE__pricelists, ACE__documents_groups, ACE__brands…) hold the full Contentful JSON payload (e.g. `ACE__campaigns` = id_campaigns, id, brand, market, json) — the handover staging layer. `ACE__campaigns.sql` (1.4 MB dump) shows campaign JSON fields: status Draft/Published, created_at/published_at, brand, market, teaser_headline/subheadline {de,fr,it}, imageId, energy_label, **dealers_includes[]** (dealer slugs, e.g. "digitas-fiatpro", "astara.ch"), hero_*, content.modules[] (stage, Text, Cta, text-img, mosaic, request-quote-form with offerFor[] ids, disclaimer), aftersales_campaign.
- `/mnt/devel/md/` root: `aws_offers_import.sql` (67 KB), `aws_services_import.sql` (30 KB), `entity_content_assets_import.sql` (672 KB) — copies of the digitas outputs; plus `aws_offers_delete_dealer.sql`, `aws_entity2dealer_migration.sql` in `digitas/output/`.
- `dealer_*` XLSX files in digitas/ (`dealers_AllBrands_20260724.xlsx`, `DWS URLs - 24.07.2026.xlsx`) — dealer master data / DWS URL lists (contents not parsed; xlsx binary — UNKNOWN exact mapping beyond filenames).
- `import_offers.php` / `component/import_offers.php` / `api/import_offers.php` in cockpit: the in-app import UI for these SQL/asset bundles.

**Consumers (data flow completion):**
- DWS websites: `/mnt/devel/md/dws-ch` — Joomla 5 multi-brand dealer platform (fiat.ch, hyundai.ch…) consuming the Cockpit API (`cockpit_api_url` — CMS content, vehicle offers, campaigns; AGENTS.md) via `/api/Campaigns/dws_content/...` and templates/cassiopeia/js (offers-and-services.js, campaign-offers-services.js; docs/campaigns.md:10-14).
- Dealer cockpits / dealer_website content, mobile dealer app (vehicleProviderOffers, dwsModels endpoints).
- Konfigdmax (`/mnt/devel/md/konfigdmax`): legacy PHP configurator for dealers — `con_*` tables (con_modele, con_silniki, con_colors, con_opcje, con_sety, con_konfiguracje with code + config_json + postcode), emits XML/JSON configs (getConfig.php object2xml, sendConfig.php, sharesend.php, testDrive.php, zipcode.php, driveform.php). It is a **separate legacy domain** (vehicle configuration), not part of the offers/services/campaigns pipeline; its only link to the current system is the KGM configurator in dws-ch (`konfigdata_server`). No evidence it feeds astara_cockpit.

---

## 4. UI template workflows (`/mnt/devel/md/product-db-react` → ported into product-db)

TanStack Router app, routes: `index` (dashboard), `inbox` + `inbox.$messageId`, `documents` + `documents.new`, `links`, `compose`, `admin` (src/routes/*). Mock data identical to product-db (`src/lib/data.ts`). Workflows mapped to domain:

| Template route/view | Workflow implemented | Legacy domain concept it maps to |
|---|---|---|
| dashboard-view | greeting, stats, recent docs (PDF/XLS/ZIP tones), inbox rows, pricelists, visits chart, frequently used links | legacy dashboard tiles: frequently_used_links, recent_offers_services/recent_campaigns, recent_pricelists, latest_documents, latest_messages (component/dashboard.php + modules/) |
| inbox-view | AG Grid; chrome tabs Inbox 3151 / Scheduled 1 / Archive 167 / Drafts 28; brand filter chips; search; row menu Edit/Delete | notifications list: aws_notifications statuses Published/Scheduled/Trash(Hidden)/Draft — counts mirror live DB (3492/1/14/18) |
| message-detail-view | brand badge, category chip, Published status badge, open rate, sender, attachments list | notification detail: aws_notifications + translations + brands + assets, opened-dealers/openRate (api/notifications.php GET read/opened-dealers) |
| compose-view | brand pills (empty = unrestricted), Category select, High Priority switch, Sender, Scheduling date/time, rich editor | notification compose: brand_ids, category_id (document group), priority, sender, scheduled_at (component/notifications.php:468-488, api/notifications.php) |
| documents-view | folder tree (Sales/Aftersales/Marketing/Product + children) + grid + brand filter | aws_documents + aws_documents_groups_new tree (same top-level groups), Documents API getDocumentsList |
| add-document-view | DE title REQUIRED / FR-IT OPTIONAL cards, brand multi-select, multi-file upload | document create: DE mandatory file/title, per-lang names, brands via aws_documents2brands (api/Documents.php validateDocumentData/saveDocumentBrands) |
| links-view | tabs Aftersales/DND/Marketing; portal link cards with brand + description | ACE__links + ACE__links_groups (api/urls.php getGroups/getLinks) |
| admin-view | profile/password + notification category email-pref toggles (Sales/Aftersales/Marketing/Product) | users API + aws_notification_email_pref_categories/prefs (api/notifications.php saveEmailPrefs) |

Sidebar (both apps): Dashboard, Inbox, Campaign Portal, Content Management, DIAS, Reporting, Links, Documents, Pricelists, Administration — direct mapping to legacy components: dashboard, notifications, campaign_portal (external portal with per-brand credentials), offers/services/campaigns, dealers, analytics, urls, doks, pricelist, users/administration.

---

## 5. Domain concepts observed across all codebases

1. **Brand** — fixed registry `aws_brands` (1=Fiat, 2=Fiat Professional, 3=Abarth, 4=Jeep, 5=Alfa Romeo, 6=Hyundai, 7=Nissan, 8=Nissan AT, 9=MG, 10=KGM, 11=MAXUS, + Lancia/Infiniti/Aiways unused); cockpit_name/dnd_name/website_brand_name variants; brand filtering is the global UI convention; Fiat DWS dealers inherit Abarth content.
2. **Market** — enum CH/AT (Nissan AT = brand 8); content locales CH_de/CH_fr/CH_it vs de/fr/it.
3. **Content language triad DE/FR/IT** — DE mandatory, FR/IT optional; UI language de/fr/it; per-user email language.
4. **Entity lifecycle** — offers/services/campaigns: Draft → Published, with Internal (superuser-only) and hidden_dws; unpublish → Draft keeps content; scheduled_date for later publish. Notifications: Draft/Published/Scheduled/Hidden + per-user trash; external create+publish only.
5. **Dealer visibility scoping** — global vs dealer-scoped (aws_entity2dealer include-list for offers/services; aws_campaigns_excl_dealers exclusion-list for campaigns), dealers_only flag, main_dealers flag, dealer2brands.dws_url as the dealer-website key.
6. **Categories = document-group tree** — one tree (aws_documents_groups_new) serves documents folders AND notification categories; ACL via aws_category2group + aws_category2dealer_type.
7. **User/ACL model** — users ↔ groups (aws_user2group) ↔ aws_acl (rd/wr/nw/rm per component); users ↔ brands (aws_user2brand) ↔ dealers (aws_user2dealer) for content scope; groups 7/42 superuser, 10 dealer; GDPR gate.
8. **GrapeJS content** — entity_content_assets html/css/project per entity+locale; generated from Contentful by convert_contentful_to_grapesjs.py; rendered on DWS pages.
9. **Contentful as source of truth** — ACE__* staging tables with full JSON; ext_id (Contentful entry id) kept on every aws_* row; import scripts idempotent via ext_id→hash maps.
10. **Inbox = per-user notification snapshot** — aws_user_notifications materialized from brand/dealer membership; read/trash/deleted state; first_opened_at tracking; SSE stream populate.
11. **Scheduling + mail pipeline** — aws_scheduled_notifications + cron send-scheduled; aws_notification_mail_jobs with per-language subject/body/preheader drained by cron; admin confirmation mails; e-mail prefs.
12. **Offer placeholders** — offer_placeholders table (offer_tag, start/end window, de/fr/it offer+disclaimer, emission_label A–G) resolved by POST /api/OfferPlaceholders/resolve for brand websites/AEM with origin whitelist (93 live rows).
13. **Campaign Portal** — external campaign system (ace-campaigns.astara-partner.com) with per-brand login credentials stored in component/campaign_portal.php.
14. **DWS (dealer website system)** — Joomla 5 sites consuming the Cockpit API; testdealer preview domains; DNS/live notifications on dealer acceptance; GA4 analytics integration.

---

## 6. UNKNOWN items (not provable from code)

- **Contentful export folders** (`digitas/offers/`, `services/`, `notifications/`, `documents/`, `campaigns/`) are absent — deleted after import. `handover_cp.sh` references `digitas/notifications/...` paths that no longer exist. The handover tool itself (which produced handover_import.sql/summary.json) is **not in the repo** — its category→document-group-id mapping and documents import SQL are unknown (handover_import.sql contains notifications only).
- **Documents data origin**: no documents import SQL/script found in digitas; `document_migration.php` is a dev→prod sync tool, not the Contentful import. UNKNOWN how aws_documents rows 1..N were initially populated.
- **Campaigns origin**: `ACE__campaigns.sql` shows the staging payloads, but the script that loaded ACE__campaigns → aws_campaigns (and mapped `dealers_includes` dealer slugs → dealer ids, plus aws_campaigns2dealers rows) is UNKNOWN.
- **Exact notification category ids**: `category` int values in aws_notifications (0-10, 27, NULL) map to aws_documents_groups_new ids, but the handover mapping table is not in the repo (only usage code in api/notifications.php).
- **campaign_portal internals** (ace-campaigns.astara-partner.com / campaigns.astara-partner.com): only credentials + URLs visible (component/campaign_portal.php); data model and workflow of that system UNKNOWN.
- **konfigdmax DB** (`con_*` tables): which database/server it reads from and whether it still runs — UNKNOWN (only file-level evidence).
- **XLSX contents** (`dealers_AllBrands_20260724.xlsx`, `DWS URLs - 24.07.2026.xlsx`) not parsed — UNKNOWN mapping to dealer2brands columns.
- **New-app pages without ported mock views**: sidebar items "Campaign Portal", "Content Management", "DIAS", "Reporting", "Pricelists" have no views in product-db/product-db-react yet — their final UI is UNKNOWN (legacy components exist as reference: campaigns/offers/services, dealers, analytics, pricelist).
- **Inbox tab counts** (3151/1/167/28) do not exactly equal live DB notification status counts (3492/1/14/18) — the mock numbers came from the ace-cockpit.html demo, not from a live query; exact derivation UNKNOWN.
- **Users table equivalence**: legacy aws_users ↔ new Laravel users table mapping (login vs email, dealer link, groups) is UNKNOWN — no migration plan exists in code.
- Whether the DWS (dws-ch) also consumes `aws_notifications` for the "DIAS" inbox of dealers — only `vw_user_visible_notifications` / `aws_user_notifications` exist; the dealer-facing notification consumption path is not evidenced beyond cockpit itself.
