Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
@@ -0,0 +1,77 @@
-- ============================================================
-- Omika Diagnostic Center — base dédiée omika_diagnostics (§14)
-- Outil SUPPORT uniquement : jamais utilisé par le métier (§13).
-- 100 % Windows / PostgreSQL. Aucune dépendance Linux/Docker.
-- PostgreSQL est NON critique : Omika fonctionne sans cette base.
-- ============================================================
-- ------------------------------------------------------------
-- Table principale des événements (§15)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS diagnostic_event (
id BIGSERIAL PRIMARY KEY,
event_id UUID NOT NULL, -- clé de déduplication (§16)
timestamp_utc TIMESTAMPTZ NOT NULL,
priority TEXT, -- P0..P4
level TEXT, -- trace..critical
event_code TEXT NOT NULL,
component TEXT,
trace_id UUID, -- X-Omika-TraceId conservé (§33)
session_id UUID,
operation_id UUID,
app_version TEXT,
platform TEXT,
error_code TEXT,
message TEXT, -- Phase 6 (crash reporting)
exception_type TEXT, -- Phase 6
stack_trace TEXT, -- Phase 6
duration_ms BIGINT,
source TEXT, -- mobile/api/worker/collector
metadata_json JSONB, -- whitelist sécurité (§18/§19)
ingested_at_utc TIMESTAMPTZ NOT NULL
);
-- Déduplication at-least-once : INSERT ... ON CONFLICT (event_id) DO NOTHING (§16)
CREATE UNIQUE INDEX IF NOT EXISTS ux_diagnostic_event_event_id
ON diagnostic_event (event_id);
-- Index utiles uniquement (§17) — pas d'index sur chaque colonne
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_timestamp_utc ON diagnostic_event (timestamp_utc);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_trace_id ON diagnostic_event (trace_id);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_operation_id ON diagnostic_event (operation_id);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_session_id ON diagnostic_event (session_id);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_event_code ON diagnostic_event (event_code);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_component ON diagnostic_event (component);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_level ON diagnostic_event (level);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_error_code ON diagnostic_event (error_code);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_app_version ON diagnostic_event (app_version);
-- ------------------------------------------------------------
-- Métriques agrégées horaires (§28) — low-cardinality (§30)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS telemetry_metric_hourly (
id BIGSERIAL PRIMARY KEY,
bucket_utc TIMESTAMPTZ NOT NULL,
metric_name TEXT NOT NULL,
component TEXT NOT NULL DEFAULT '', -- '' = métrique globale
value DOUBLE PRECISION NOT NULL,
count BIGINT NOT NULL,
min_value DOUBLE PRECISION NOT NULL,
max_value DOUBLE PRECISION NOT NULL,
tags_json JSONB NOT NULL DEFAULT '{}'::jsonb
);
-- Upsert : un agrégat unique par (bucket, métrique, composant, tags)
CREATE UNIQUE INDEX IF NOT EXISTS ux_telemetry_metric_hourly
ON telemetry_metric_hourly (bucket_utc, metric_name, component, tags_json);
CREATE INDEX IF NOT EXISTS ix_telemetry_metric_hourly_bucket
ON telemetry_metric_hourly (bucket_utc);
-- ------------------------------------------------------------
-- Rétention (§45) — valeur configurable (Diagnostics:RetentionDays, défaut 60).
-- NON exécutée automatiquement : à planifier en maintenance (purge destructive
-- documentée). Exemple (à adapter) :
-- DELETE FROM diagnostic_event
-- WHERE timestamp_utc < now() - interval '60 days';
-- ------------------------------------------------------------
+23
View File
@@ -0,0 +1,23 @@
-- ============================================================
-- Omika Diagnostic Center — 002_actor_key.sql (Phase 5B2A.1)
-- Pseudonymisation ActorKey (HMAC-SHA256) pour la recherche support.
-- ============================================================
-- IDEMPOTENT : ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS.
-- AUCUN DROP, AUCUNE suppression, AUCUNE migration destructive.
-- PostgreSQL 18 compatible (syntaxe disponible depuis 9.6).
--
-- ActorKey = HMAC-SHA256( "v1|tenant:<CodeClient>|collaborateur:<CollaborateurId>" )
-- calculé côté SERVEUR (Omika.Api) depuis l'identité AUTHENTIFIÉE (§3).
-- Format : hexadécimal minuscule, 64 caractères. NULL = système / sans
-- utilisateur / clé HMAC indisponible (fail-open).
-- ActorKeyVersion = version de l'algorithme (1 aujourd'hui) → rotation future.
-- ============================================================
ALTER TABLE diagnostic_event
ADD COLUMN IF NOT EXISTS actor_key TEXT,
ADD COLUMN IF NOT EXISTS actor_key_version INTEGER;
-- Recherche support « tous les diagnostics d'un collaborateur » (§12) :
-- index composé (actor_key, timestamp_utc DESC).
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_actor_key_timestamp
ON diagnostic_event (actor_key, timestamp_utc DESC);
+123
View File
@@ -0,0 +1,123 @@
-- ============================================================
-- Omika Diagnostic Center -- 003_diagnostics_roles.sql (Phase 5B2B.3)
-- PostgreSQL hardening : least privilege (Worker / Web / Owner).
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres),
-- BEFORE Phase 5C. DO NOT run from the development PC.
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op.
-- - Non destructive : NO DROP, no role/object/data removal.
-- - NO password, NO secret (LOGIN role passwords are set manually via
-- \password, never in this file).
--
-- Role model :
-- omika_diagnostics_owner NOLOGIN : owner of DB/schema/tables/sequences/indexes.
-- omika_diagnostics_worker LOGIN : Worker runtime (JSONL import).
-- omika_diagnostics_web LOGIN : Web runtime (read only).
-- omika_diagnostics_app (existing) : KEPT (controlled transition),
-- NOT disabled here.
--
-- Server apply order : 001 (schema) -> 002 (actor_key) -> 003 (roles).
-- ============================================================
-- ------------------------------------------------------------
-- 1. Create roles if absent (NO password here).
-- ------------------------------------------------------------
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_owner') THEN
CREATE ROLE omika_diagnostics_owner;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_worker') THEN
CREATE ROLE omika_diagnostics_worker;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_web') THEN
CREATE ROLE omika_diagnostics_web;
END IF;
END
$$;
-- ------------------------------------------------------------
-- 2. Secure attributes (idempotent).
-- All: NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS.
-- ------------------------------------------------------------
ALTER ROLE omika_diagnostics_owner NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS NOLOGIN;
ALTER ROLE omika_diagnostics_worker NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS LOGIN;
ALTER ROLE omika_diagnostics_web NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS LOGIN;
-- ------------------------------------------------------------
-- 3. Ownership transfer (explicit -- no global REASSIGN OWNED).
-- Only Omika Diagnostics objects are targeted.
-- ------------------------------------------------------------
ALTER DATABASE omika_diagnostics OWNER TO omika_diagnostics_owner;
ALTER SCHEMA public OWNER TO omika_diagnostics_owner;
ALTER TABLE diagnostic_event OWNER TO omika_diagnostics_owner;
ALTER TABLE telemetry_metric_hourly OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostic_event_id_seq OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE telemetry_metric_hourly_id_seq OWNER TO omika_diagnostics_owner;
-- PostgreSQL 18:
-- Index ownership follows table ownership.
-- No ALTER INDEX OWNER is required.
-- (Sequences are NOT auto-transferred by ALTER TABLE: the ALTER SEQUENCE
-- OWNER commands above are therefore kept and required.)
-- ------------------------------------------------------------
-- 4. Schema "public" hardening.
-- Decision : YES, REVOKE CREATE -- the database is dedicated to the
-- Diagnostic Center; only the owner/admin should create objects.
-- (PostgreSQL 15+ already revokes it for new databases; this is an
-- idempotent belt-and-suspenders.)
-- ------------------------------------------------------------
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- ------------------------------------------------------------
-- 5. MINIMUM runtime privileges (from real code audit).
--
-- WORKER (omika_diagnostics_worker):
-- * diagnostic_event : INSERT only
-- (INSERT ... ON CONFLICT (event_id) DO NOTHING -- dedup).
-- * sequence diagnostic_event_id_seq : USAGE (id BIGSERIAL).
-- * NO SELECT/UPDATE/DELETE (60-day retention is NOT implemented;
-- telemetry_metric_hourly upsert is registered but has NO caller).
--
-- WEB (omika_diagnostics_web): SELECT ONLY on diagnostic_event
-- (QueryAsync count+page, GetByTraceId, GetByOperationId, IsAvailableAsync).
--
-- NO privilege granted on telemetry_metric_hourly while no code uses it
-- (least privilege).
-- ------------------------------------------------------------
-- Explicit connection intent (PUBLIC keeps default CONNECT; documented only,
-- not hardened beyond requested scope).
GRANT CONNECT ON DATABASE omika_diagnostics TO omika_diagnostics_worker, omika_diagnostics_web;
GRANT USAGE ON SCHEMA public TO omika_diagnostics_worker, omika_diagnostics_web;
-- Worker : JSONL import -> diagnostic_event.
GRANT INSERT ON TABLE diagnostic_event TO omika_diagnostics_worker;
GRANT USAGE ON SEQUENCE diagnostic_event_id_seq TO omika_diagnostics_worker;
-- Web : read only.
GRANT SELECT ON TABLE diagnostic_event TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- 6. Legacy role omika_diagnostics_app : KEPT (no DROP, no NOLOGIN here).
-- After transfer it owns no Diagnostics object and has no object privilege;
-- disabling it (ALTER ROLE ... NOLOGIN) is an EXPLICIT administrator action
-- AFTER validating the new roles.
-- ------------------------------------------------------------
-- ============================================================
-- END 003. Manual next steps (outside this script):
-- 1. Set passwords: psql -> \password omika_diagnostics_worker
-- then \password omika_diagnostics_web (never in script/log/history).
-- 2. Point Worker: Username=omika_diagnostics_worker ; Web:
-- Username=omika_diagnostics_web (ConnectionStrings__DiagnosticsDatabase).
-- 3. Validate Worker (INSERT) and Web (SELECT), then only after,
-- disable omika_diagnostics_app (ALTER ROLE ... NOLOGIN) if desired.
-- ============================================================
@@ -0,0 +1,62 @@
-- ============================================================
-- Omika Diagnostic Center -- 003_diagnostics_roles_preprod.sql
-- VARIANTE PRÉPROD LOCALE de 003_diagnostics_roles.sql (T21).
-- ============================================================
-- ⚠️ EXCLUSIVEMENT LOCAL / PRÉPROD — jamais en Production.
--
-- TRANSFORMATION UNIQUE par rapport à l'original 003 :
-- « ALTER DATABASE omika_diagnostics OWNER TO ... »
-- « GRANT CONNECT ON DATABASE omika_diagnostics TO ... »
-- → base cible renommée « omika_diagnostics_preprod ».
-- Rôles, privilèges, ownership et idempotence STRICTEMENT identiques.
-- (L'original code en dur le nom de la base centrale Production
-- « omika_diagnostics » ; cette variante ne change QUE ce nom.)
--
-- Teste réellement, sur une base locale jetable, l'exécutabilité des
-- rôles/grants 003 : création de rôles cluster-wide, ownership, REVOKE
-- public, GRANT worker/web. Aucun mot de passe (posé manuellement via
-- \password, jamais dans ce fichier).
-- ============================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_owner') THEN
CREATE ROLE omika_diagnostics_owner;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_worker') THEN
CREATE ROLE omika_diagnostics_worker;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_web') THEN
CREATE ROLE omika_diagnostics_web;
END IF;
END
$$;
ALTER ROLE omika_diagnostics_owner NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS NOLOGIN;
ALTER ROLE omika_diagnostics_worker NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS LOGIN;
ALTER ROLE omika_diagnostics_web NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS LOGIN;
ALTER DATABASE omika_diagnostics_preprod OWNER TO omika_diagnostics_owner;
ALTER SCHEMA public OWNER TO omika_diagnostics_owner;
ALTER TABLE diagnostic_event OWNER TO omika_diagnostics_owner;
ALTER TABLE telemetry_metric_hourly OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostic_event_id_seq OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE telemetry_metric_hourly_id_seq OWNER TO omika_diagnostics_owner;
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
GRANT CONNECT ON DATABASE omika_diagnostics_preprod TO omika_diagnostics_worker, omika_diagnostics_web;
GRANT USAGE ON SCHEMA public TO omika_diagnostics_worker, omika_diagnostics_web;
GRANT INSERT ON TABLE diagnostic_event TO omika_diagnostics_worker;
GRANT USAGE ON SEQUENCE diagnostic_event_id_seq TO omika_diagnostics_worker;
GRANT SELECT ON TABLE diagnostic_event TO omika_diagnostics_web;
-- ============================================================
-- END 003 (variante preprod). Les mots de passe des rôles LOGIN
-- (worker/web) sont posés manuellement via \password si nécessaire.
-- ============================================================
@@ -0,0 +1,39 @@
-- ============================================================
-- Omika Diagnostic Center -- 004_diagnostics_retention.sql (Phase 5C.2C)
-- PostgreSQL : moindre privilège pour la rétention diagnostic.
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres).
-- DO NOT run from the development PC.
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (GRANT is idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, NO role/object removal.
-- - NO password, NO secret.
--
-- Context (Phase 5C.2C) : the retention loop (DiagnosticRetentionWorker) purges
-- diagnostic_event by BOUNDED batches :
--
-- DELETE FROM diagnostic_event
-- WHERE id IN (
-- SELECT id FROM diagnostic_event
-- WHERE timestamp_utc < @cutoff
-- ORDER BY timestamp_utc ASC, id ASC
-- LIMIT @batchSize
-- )
--
-- This SQL requires :
-- * DELETE on diagnostic_event (the target of the DELETE) ;
-- * SELECT on diagnostic_event (the sub-query reads the table).
--
-- The worker role (omika_diagnostics_worker) currently holds only INSERT +
-- USAGE sequence (see 003). Least privilege : grant ONLY SELECT + DELETE.
-- NO UPDATE, NO TRUNCATE, NO DDL, NO privilege on telemetry_metric_hourly.
-- ============================================================
GRANT SELECT, DELETE ON TABLE diagnostic_event TO omika_diagnostics_worker;
-- ============================================================
-- END 004. Apply order on server : 001 -> 002 -> 003 -> 004.
-- No server action is performed by the development PC.
-- ============================================================
@@ -0,0 +1,45 @@
-- ============================================================
-- Omika Diagnostic Center -- 005_diagnostics_client_context.sql
-- Tâche 13-A : contexte client / serveur / environnement (additif).
-- ============================================================
-- IDEMPOTENT : ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS.
-- AUCUN DROP, AUCUNE suppression, AUCUNE migration destructive.
-- PostgreSQL 18 compatible (syntaxe disponible depuis 9.6).
--
-- Ajoute au diagnostic_event trois dimensions de contexte de déploiement,
-- NULLABLES : les événements antérieurs restent parfaitement valides
-- (client_code = NULL, server_instance = NULL, environment = NULL),
-- AUCUN backfill, AUCUNE valeur inventée (« UNKNOWN »/« N/A » interdits).
--
-- client_code : CodeClient de référence = ClientBase.CL_Code
-- (identifiant NON secret du client Omika, jamais
-- Client.CL_CodeClient, CL_CleAPIInstall, clé licence,
-- ActorKey ni nom société).
-- server_instance : identifiant d'instance serveur (configuré, ex.
-- « API-SRV-01 ») — T13-D.
-- environment : environnement ASP.NET (Development / Production / …).
--
-- CHOIX DE TYPE (cohérence schéma) : le schéma existant utilise TEXT sans
-- VARCHAR ni CHECK de longueur (priority, level, component, app_version…).
-- On reste cohérent : colonnes TEXT, PAS de CHECK de longueur arbitraire.
-- La validation de longueur (<= 100 / <= 100 / <= 20) sera portée par le
-- code au moment de la résolution (T13-D), comme pour les autres champs.
-- ============================================================
ALTER TABLE diagnostic_event
ADD COLUMN IF NOT EXISTS client_code TEXT,
ADD COLUMN IF NOT EXISTS server_instance TEXT,
ADD COLUMN IF NOT EXISTS environment TEXT;
-- Recherche support « tous les diagnostics d'un client » : index composé
-- (client_code, timestamp_utc DESC), aligné sur le style de
-- ix_diagnostic_event_actor_key_timestamp (002).
--
-- PAS d'index dédié sur server_instance : aucun usage immédiat ne justifie
-- le coût d'écriture/maintenance d'un second index composé (T13 — « composite
-- éventuel »). À réévaluer en T13-F si un filtre serveur devient un hot path.
--
-- PAS d'index sur environment : très faible cardinalité (2-3 valeurs), un
-- index serait peu sélectif et sans bénéfice réel.
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_client_code_timestamp
ON diagnostic_event (client_code, timestamp_utc DESC);
@@ -0,0 +1,69 @@
-- ============================================================
-- Omika Diagnostic Center -- 006_diagnostics_device_license.sql
-- Tâche 17-B : dimensions appareil + licence (additif).
-- ============================================================
-- IDEMPOTENT : ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS.
-- AUCUN DROP, AUCUNE suppression, AUCUNE migration destructive.
-- PostgreSQL 18 compatible (syntaxe disponible depuis 9.6).
--
-- Ajoute au diagnostic_event les dimensions appareil/licence, TOUTES NULLABLES :
-- les événements antérieurs restent parfaitement valides (toutes nouvelles colonnes
-- à NULL), AUCUN backfill, AUCUNE valeur inventée (« UNKNOWN »/« N/A » interdits).
--
-- license_number : numéro de licence fonctionnel = Licence.LI_Numero,
-- dérivé SERVEUR du token authentifié
-- (MobileTokenIdentity.NumeroLicence). JAMAIS la PK HFSQL
-- LI_PKID, JAMAIS un secret, JAMAIS fourni par le mobile.
-- device_key : pseudonyme d'appareil HMAC-SHA256 calculé SERVEUR =
-- HMAC("v1|tenant:<CodeClient>|device:<DeviceId>").
-- JAMAIS le DeviceId brut.
-- device_key_version : version de l'algorithme DeviceKey (1 aujourd'hui).
-- device_manufacturer : fabricant (mobile, déclaratif).
-- device_model : modèle d'appareil (mobile, déclaratif).
-- operating_system : nom de l'OS (mobile, déclaratif).
-- operating_system_version : version de l'OS (mobile, déclaratif).
-- app_build : numéro de build applicatif (mobile, déclaratif).
-- screen_width : largeur d'écran physique (px, >= 0).
-- screen_height : hauteur d'écran physique (px, >= 0).
-- logical_width : largeur d'écran logique (unités indépendantes densité).
-- logical_height : hauteur d'écran logique.
-- display_scale : facteur d'échelle (ex. 1, 1.25, 1.5, 2, 3).
--
-- CHOIX DE TYPE (cohérence schéma) : le schéma existant utilise TEXT sans VARCHAR
-- pour les chaînes (priority, level, component, app_version…) ; on reste cohérent.
-- Les dimensions écran physiques/logiques sont des entiers non négatifs (INTEGER) ;
-- display_scale est un réel (DOUBLE PRECISION, valeurs décimales 1.25/1.5/2.75/3).
-- La validation (longueur, bornes, non-négativité) est portée par le code au moment
-- de la résolution (T17-C), comme pour les autres champs.
-- ============================================================
ALTER TABLE diagnostic_event
ADD COLUMN IF NOT EXISTS license_number TEXT,
ADD COLUMN IF NOT EXISTS device_key TEXT,
ADD COLUMN IF NOT EXISTS device_key_version INTEGER,
ADD COLUMN IF NOT EXISTS device_manufacturer TEXT,
ADD COLUMN IF NOT EXISTS device_model TEXT,
ADD COLUMN IF NOT EXISTS operating_system TEXT,
ADD COLUMN IF NOT EXISTS operating_system_version TEXT,
ADD COLUMN IF NOT EXISTS app_build TEXT,
ADD COLUMN IF NOT EXISTS screen_width INTEGER,
ADD COLUMN IF NOT EXISTS screen_height INTEGER,
ADD COLUMN IF NOT EXISTS logical_width INTEGER,
ADD COLUMN IF NOT EXISTS logical_height INTEGER,
ADD COLUMN IF NOT EXISTS display_scale DOUBLE PRECISION;
-- Index composés immédiatement justifiés (recherche support « tous les diagnostics
-- d'un appareil / d'une licence »), alignés sur le style de
-- ix_diagnostic_event_actor_key_timestamp (002) et
-- ix_diagnostic_event_client_code_timestamp (005).
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_device_key_timestamp
ON diagnostic_event (device_key, timestamp_utc DESC);
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_license_number_timestamp
ON diagnostic_event (license_number, timestamp_utc DESC);
-- PAS d'index dédié sur device_manufacturer / device_model / operating_system /
-- operating_system_version / app_build / screen_* : dimensions de filtrage/agrégation
-- futures (T17-E) mais sans usage immédiat justifiant le coût d'écriture/maintenance.
-- PAS d'index sur display_scale (valeur continue, jamais filtrée par égalité).
-- À réévaluer en T17-E lorsque les statistiques de parc seront définies.
@@ -0,0 +1,47 @@
-- ============================================================
-- Omika Diagnostic Center -- 007_diagnostics_usage.sql
-- Tâche 18 : télémétrie d'utilisation des écrans / fonctionnalités (additif).
-- ============================================================
-- IDEMPOTENT : ADD COLUMN IF NOT EXISTS + CREATE INDEX IF NOT EXISTS.
-- AUCUN DROP, AUCUNE suppression, AUCUNE migration destructive.
-- PostgreSQL 18 compatible (syntaxe disponible depuis 9.6).
--
-- Ajoute au diagnostic_event deux dimensions d'USAGE fonctionnel, NULLABLES :
-- les événements antérieurs restent parfaitement valides (screen_code = NULL,
-- feature_code = NULL), AUCUN backfill, AUCUNE valeur inventée (« UNKNOWN »/« N/A »
-- interdits).
--
-- screen_code : code fonctionnel STABLE d'un écran (ex. AGENDA, SITE_DETAIL).
-- Produit DEPUIS le catalogue applicatif (jamais un nom de
-- Page/ViewModel, jamais un identifiant métier, jamais une saisie
-- utilisateur). Null pour les événements background sans écran.
-- feature_code: code fonctionnel STABLE d'une action métier significative
-- (ex. EMAIL_SEND, REPORT_GENERATE). Attaché à l'événement précis
-- de l'action, jamais à un « contexte courant » global. Null si non
-- applicable.
--
-- CHOIX DE TYPE (cohérence schéma) : TEXT sans VARCHAR, comme les autres chaînes
-- (priority, level, component, license_number…). La validation (longueur, valeurs
-- bornées) est portée par le code au moment de la résolution, comme les autres
-- champs. Les deux colonnes sont NON SENSIBLES : jamais de DeviceId/UserId/
-- CollaborateurId/email/téléphone/token/id métier dynamique (§9/§10/§72).
-- ============================================================
ALTER TABLE diagnostic_event
ADD COLUMN IF NOT EXISTS screen_code TEXT,
ADD COLUMN IF NOT EXISTS feature_code TEXT;
-- Index de recherche « tous les ScreenView d'un écran dans le temps » (T18, §49) :
-- les agrégations d'usage filtrent par (event_code, screen_code) puis par période,
-- mais l'index composé (screen_code, timestamp_utc DESC) couvre le filtre écran +
-- le tri temporel des diagnostics filtrés (§60), aligné sur le style de
-- ix_diagnostic_event_device_key_timestamp (006).
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_screen_code_timestamp
ON diagnostic_event (screen_code, timestamp_utc DESC);
-- Index (feature_code, timestamp_utc DESC) : filtre fonctionnalité + tri temporel.
-- FeatureCode est une dimension de catalogue à cardinalité TRÈS faible (< 10), donc
-- un index est peu sélectif ; il est néanmoins ajouté par symétrie avec screen_code
-- car le filtre « Tous les diagnostics d'une fonctionnalité » (§60) est prévu.
CREATE INDEX IF NOT EXISTS ix_diagnostic_event_feature_code_timestamp
ON diagnostic_event (feature_code, timestamp_utc DESC);
@@ -0,0 +1,38 @@
-- ============================================================
-- Omika Diagnostic Center -- 008_diagnostics_clients.sql
-- Tâche 19 : registre des clients autorisés (serveurs clients) de l'API centrale.
-- ============================================================
-- IDEMPOTENT : CREATE TABLE IF NOT EXISTS + CREATE UNIQUE INDEX IF NOT EXISTS.
-- AUCUN DROP, AUCUNE suppression, AUCUNE migration destructive.
-- PostgreSQL 18 compatible.
--
-- Une ligne = un SERVEUR CLIENT autorisé à transmettre ses diagnostics vers
-- Omika.Diagnostics.Api (CodeClient métier, ex. BATIK.01).
--
-- client_code : CodeClient métier autoritaire (unique, normalisé UPPER côté code).
-- enabled : FALSE => le client ne peut plus envoyer (réponse 403).
-- api_key_hash : hash SHA-256 (hex 64) de la clé d'accès. JAMAIS la clé en clair.
-- key_prefix : préfixe d'affichage de la clé (ex. "omkdiag_"), NON secret,
-- permet d'identifier la clé dans l'interface sans l'exposer.
-- created_at_utc : création du client.
-- key_created_at_utc : génération de la clé COURANTE (utile après régénération).
-- last_seen_at_utc : dernière réception d'un lot accepté ; NULL si jamais.
--
-- CHOIX DE TYPE : aligné sur diagnostic_event (TEXT, TIMESTAMPTZ). Aucun secret
-- réversible stocké ; la clé n'existe que sous forme de hash SHA-256 (§29/§30).
-- ============================================================
CREATE TABLE IF NOT EXISTS diagnostics_clients (
id BIGSERIAL PRIMARY KEY,
client_code TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
api_key_hash TEXT NOT NULL,
key_prefix TEXT NOT NULL,
created_at_utc TIMESTAMPTZ NOT NULL,
key_created_at_utc TIMESTAMPTZ NOT NULL,
last_seen_at_utc TIMESTAMPTZ
);
-- Unicité par CodeClient (un seul client par code, §26).
CREATE UNIQUE INDEX IF NOT EXISTS ux_diagnostics_clients_client_code
ON diagnostics_clients (client_code);
@@ -0,0 +1,103 @@
-- ============================================================
-- Omika Diagnostic Center -- 009_diagnostics_clients_grants.sql
-- T22 : moindre privilège pour la table diagnostics_clients (008).
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d omika_diagnostics -f 009_...),
-- AFTER 008 (table diagnostics_clients) and AFTER 003/004 (rôles owner/worker/web).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op.
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret (LOGIN role password is set manually via \password).
--
-- Motivations (T22) :
-- 008 crée diagnostics_clients APRÈS 003/004. La table reste donc possédée
-- par le rôle qui exécute 008 (postgres) et ne reçoit AUCUN GRANT applicatif :
-- l'API centrale (authentification ClientCode+ApiKey + last_seen_at_utc) et
-- l'administration Web (/Clients) échoueraient avec leurs comptes de service.
--
-- Rôle AJOUTÉ : omika_diagnostics_api (LOGIN). Le modèle 003 ne prévoyait que
-- owner/worker/web ; Omika.Diagnostics.Api (service distinct du Worker et du
-- Web) n'y figurait pas. Elle n'accède QU'À diagnostics_clients.
--
-- Ownership : même propriétaire logique que les autres tables Diagnostics
-- (omika_diagnostics_owner), comme 003. Les index suivent automatiquement la
-- table ; la séquence BIGSERIAL est transférée explicitement.
--
-- Note portabilité : GRANT CONNECT utilise current_database() — la migration
-- s'applique donc à la base courante (psql -d <base>), sans coder en dur le
-- nom (contrairement à 003 qui, pour ALTER DATABASE, devait le fixer).
-- ============================================================
-- ------------------------------------------------------------
-- 1. Créer le rôle API s'il est absent (aucun mot de passe ici).
-- ------------------------------------------------------------
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'omika_diagnostics_api') THEN
CREATE ROLE omika_diagnostics_api;
END IF;
END
$$;
ALTER ROLE omika_diagnostics_api NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS LOGIN;
-- ------------------------------------------------------------
-- 2. Ownership : omika_diagnostics_owner (comme 003).
-- ------------------------------------------------------------
ALTER TABLE diagnostics_clients OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostics_clients_id_seq OWNER TO omika_diagnostics_owner;
-- ------------------------------------------------------------
-- 3. Défense PUBLIC (ceinture + bretelles, idempotent) : aucune
-- table Diagnostics n'est accessible à PUBLIC.
-- ------------------------------------------------------------
REVOKE ALL ON TABLE diagnostics_clients FROM PUBLIC;
-- ------------------------------------------------------------
-- 4. Connexion explicite (intention documentée, comme 003) + schéma.
-- ------------------------------------------------------------
DO $$
BEGIN
EXECUTE format('GRANT CONNECT ON DATABASE %I TO omika_diagnostics_api', current_database());
END
$$;
GRANT USAGE ON SCHEMA public TO omika_diagnostics_api;
-- ------------------------------------------------------------
-- 5. API centrale (Omika.Diagnostics.Api) : SELECT (auth : client,
-- enabled, api_key_hash…) + UPDATE(last_seen_at_utc) uniquement.
-- AUCUN INSERT, AUCUN DELETE, AUCUNE modification de enabled /
-- api_key_hash / key_prefix (moindre privilège, anti-falsification
-- de l'autorisation en cas de compromission de l'API).
-- ------------------------------------------------------------
GRANT SELECT ON TABLE diagnostics_clients TO omika_diagnostics_api;
GRANT UPDATE (last_seen_at_utc) ON TABLE diagnostics_clients TO omika_diagnostics_api;
-- ------------------------------------------------------------
-- 6. Web (Omika.Diagnostics.Web, admin /Clients) : liste + création +
-- activation/désactivation + régénération de clé. AUCUN DELETE
-- (aucune opération de suppression n'existe dans le code T19).
-- USAGE séquence : INSERT ... RETURNING id (id BIGSERIAL).
-- ------------------------------------------------------------
GRANT SELECT ON TABLE diagnostics_clients TO omika_diagnostics_web;
GRANT INSERT ON TABLE diagnostics_clients TO omika_diagnostics_web;
GRANT UPDATE (enabled, api_key_hash, key_prefix, key_created_at_utc)
ON TABLE diagnostics_clients TO omika_diagnostics_web;
GRANT USAGE ON SEQUENCE diagnostics_clients_id_seq TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- 7. Worker (Omika.Diagnostics.Worker) : AUCUN droit sur
-- diagnostics_clients (il n'y accède jamais : import JSONL ->
-- diagnostic_event + rétention diagnostic_event uniquement).
-- Rien à accorder. Aucun REVOKE nécessaire : le rôle n'a reçu
-- aucun privilège sur cette table.
-- ------------------------------------------------------------
-- ============================================================
-- END 009. Ordre serveur : 001 -> 002 -> 003 -> 004 -> 005 -> 006
-- -> 007 -> 008 -> 009. Mots de passe posés manuellement (\password),
-- jamais dans un script/log.
-- ============================================================
@@ -0,0 +1,99 @@
-- ============================================================
-- Omika Diagnostic Center -- 010_diagnostics_web_users.sql
-- T23 : gestion des utilisateurs de Omika.Diagnostics.Web.
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 010_...),
-- AFTER 003 (rôles owner/worker/web), 008 (diagnostics_clients) and 009 (rôle api + grants clients).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (CREATE ... IF NOT EXISTS + GRANT idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret (no column stores a clear-text password).
--
-- Une ligne = une PERSONNE autorisée à ouvrir Omika.Diagnostics.Web
-- (username + password_hash + rôle). À NE PAS confondre avec
-- diagnostics_clients (serveurs clients, ClientCode + ApiKey).
--
-- username : identifiant unique (unicité insensible à la casse).
-- password_hash : hash PBKDF2-SHA256 (DiagnosticPasswordHasher). JAMAIS en clair.
-- role : 'Support' ou 'Admin' (validé par le code, pas de CHECK arbitraire).
-- enabled : FALSE => l'utilisateur ne peut plus se connecter.
-- created_at_utc : création du compte.
-- updated_at_utc : dernière mutation du compte.
-- password_changed_at_utc : dernier changement de mot de passe.
-- last_login_at_utc : dernier login réussi ; NULL si jamais.
--
-- CHOIX DE TYPE : aligné sur diagnostic_event / diagnostics_clients (TEXT, TIMESTAMPTZ,
-- BOOLEAN). Aucun VARCHAR, aucun CHECK de longueur (validation portée par le code,
-- comme 005/007/008). Aucune colonne password/plain_password/clear_password.
-- ============================================================
CREATE TABLE IF NOT EXISTS diagnostics_users (
id BIGSERIAL PRIMARY KEY,
username TEXT NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_at_utc TIMESTAMPTZ NOT NULL,
updated_at_utc TIMESTAMPTZ NOT NULL,
password_changed_at_utc TIMESTAMPTZ NOT NULL,
last_login_at_utc TIMESTAMPTZ
);
-- Unicité insensible à la casse (§T23/11) : un index sur LOWER(username) garantit
-- qu'un même identifiant ne peut exister deux fois quelle que soit la casse.
CREATE UNIQUE INDEX IF NOT EXISTS ux_diagnostics_users_username
ON diagnostics_users (LOWER(username));
-- ------------------------------------------------------------
-- Ownership : omika_diagnostics_owner (comme 003/009).
-- ------------------------------------------------------------
ALTER TABLE diagnostics_users OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostics_users_id_seq OWNER TO omika_diagnostics_owner;
-- ------------------------------------------------------------
-- Défense PUBLIC (ceinture + bretelles, idempotent).
-- ------------------------------------------------------------
REVOKE ALL ON TABLE diagnostics_users FROM PUBLIC;
-- ------------------------------------------------------------
-- Connexion explicite + schéma pour omika_diagnostics_web
-- (déjà accordés par 003 ; ré-affirmé idempotent, intention documentée).
-- ------------------------------------------------------------
DO $$
BEGIN
EXECUTE format('GRANT CONNECT ON DATABASE %I TO omika_diagnostics_web', current_database());
END
$$;
GRANT USAGE ON SCHEMA public TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- Web (Omika.Diagnostics.Web, admin /Users) : SELECT + INSERT +
-- UPDATE colonne-ciblé (moindre privilège) + USAGE séquence.
-- - INSERT : création utilisateur (id BIGSERIAL).
-- - UPDATE (password_hash, role, enabled, updated_at_utc,
-- password_changed_at_utc, last_login_at_utc) : reset mot de passe,
-- changement rôle, activation/désactivation, last login.
-- - username n'est JAMAIS mis à jour (aucun renommage en V1).
-- - AUCUN DELETE (aucune suppression physique en V1).
-- ------------------------------------------------------------
GRANT SELECT ON TABLE diagnostics_users TO omika_diagnostics_web;
GRANT INSERT ON TABLE diagnostics_users TO omika_diagnostics_web;
GRANT UPDATE (password_hash, role, enabled, updated_at_utc, password_changed_at_utc, last_login_at_utc)
ON TABLE diagnostics_users TO omika_diagnostics_web;
GRANT USAGE ON SEQUENCE diagnostics_users_id_seq TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- API (omika_diagnostics_api) : AUCUN droit sur diagnostics_users.
-- Worker (omika_diagnostics_worker) : AUCUN droit sur diagnostics_users.
-- Aucun GRANT accordé ; les rôles n'ont aucun privilège par défaut.
-- (Aucun REVOKE nécessaire : aucun privilège n'a été accordé.)
-- ------------------------------------------------------------
-- ============================================================
-- END 010. Ordre serveur : 001 -> 002 -> 003 -> 004 -> 005 -> 006
-- -> 007 -> 008 -> 009 -> 010. Mots de passe posés manuellement
-- (\password), jamais dans un script/log.
-- ============================================================
@@ -0,0 +1,140 @@
-- ============================================================
-- Omika Diagnostic Center -- 011_diagnostics_reference.sql
-- T24-A : référentiel administrable ScreenCode / FeatureCode.
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 011_...),
-- AFTER 010 (diagnostics_users). Order: 001 -> ... -> 010 -> 011.
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (CREATE ... IF NOT EXISTS + GRANT idempotent
-- + seed ON CONFLICT DO NOTHING).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- PURPOSE :
-- diagnostic_event reste la VÉRITÉ sur ce qui a réellement été observé. Ce
-- référentiel est un ENRICHISSEMENT administrable (libellé / description /
-- ordre / état) depuis Diagnostics.Web. Il ne détermine JAMAIS quels événements
-- sont acceptés : un nouveau code observé reste visible même non référencé.
--
-- reference_type : 'SCREEN_CODE' ou 'FEATURE_CODE' (V1). Extensible plus tard.
-- code : code technique, normalisé TRIM + UPPER (unique par type).
-- label : libellé métier français.
-- description : documentation métier (nullable).
-- enabled : FALSE => archivé (aucun DELETE physique en V1).
-- sort_order : ordre d'affichage.
-- created_at_utc / updated_at_utc : TIMESTAMPTZ (comme 005/007/008/010).
--
-- Ownership : omika_diagnostics_owner (comme 003/008/009/010).
-- ============================================================
CREATE TABLE IF NOT EXISTS diagnostics_reference (
id BIGSERIAL PRIMARY KEY,
reference_type TEXT NOT NULL,
code TEXT NOT NULL,
label TEXT NOT NULL,
description TEXT,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at_utc TIMESTAMPTZ NOT NULL,
updated_at_utc TIMESTAMPTZ NOT NULL
);
-- Unicité d'un code POUR SON TYPE, insensible à la casse (T24-A §20) : l'index
-- porte sur UPPER(code), donc 'agenda' / 'AGENDA' / 'Agenda' sont le même code.
CREATE UNIQUE INDEX IF NOT EXISTS ux_diagnostics_reference_type_code
ON diagnostics_reference (reference_type, UPPER(code));
-- ------------------------------------------------------------
-- Ownership : omika_diagnostics_owner.
-- ------------------------------------------------------------
ALTER TABLE diagnostics_reference OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostics_reference_id_seq OWNER TO omika_diagnostics_owner;
-- ------------------------------------------------------------
-- Défense PUBLIC (ceinture + bretelles, idempotent).
-- ------------------------------------------------------------
REVOKE ALL ON TABLE diagnostics_reference FROM PUBLIC;
-- ------------------------------------------------------------
-- Connexion explicite + schéma pour omika_diagnostics_web
-- (déjà accordés par 003 ; ré-affirmé idempotent).
-- ------------------------------------------------------------
DO $$
BEGIN
EXECUTE format('GRANT CONNECT ON DATABASE %I TO omika_diagnostics_web', current_database());
END
$$;
GRANT USAGE ON SCHEMA public TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- Web (Omika.Diagnostics.Web, admin /Referentiel/Admin) : SELECT + INSERT +
-- UPDATE colonne-ciblé (moindre privilège) + USAGE séquence.
-- - INSERT : création d'une entrée (id BIGSERIAL).
-- - UPDATE (label, description, enabled, sort_order, updated_at_utc) :
-- modification + archivage (enabled=false). Le CODE est immuable.
-- - AUCUN DELETE (aucune suppression physique en V1, §24).
-- ------------------------------------------------------------
GRANT SELECT ON TABLE diagnostics_reference TO omika_diagnostics_web;
GRANT INSERT ON TABLE diagnostics_reference TO omika_diagnostics_web;
GRANT UPDATE (label, description, enabled, sort_order, updated_at_utc)
ON TABLE diagnostics_reference TO omika_diagnostics_web;
GRANT USAGE ON SEQUENCE diagnostics_reference_id_seq TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- API (omika_diagnostics_api) : AUCUN droit sur diagnostics_reference.
-- Worker (omika_diagnostics_worker) : AUCUN droit sur diagnostics_reference.
-- Aucun GRANT accordé ; les rôles n'ont aucun privilège par défaut.
-- (Testé négativement : SELECT référentiel REFUSÉ pour api et worker.)
-- ------------------------------------------------------------
-- ------------------------------------------------------------
-- INITIALISATION IDEMPOTENTE (T24-A §36) : les 29 ScreenCode et 3 FeatureCode
-- du catalogue applicatif. ON CONFLICT DO NOTHING => un libellé modifié par un
-- Admin n'est JAMAIS écrasé au redémarrage. Les libellés correspondent EXACTEMENT
-- à DiagnosticScreenCodeCatalog / DiagnosticFeatureCodeCatalog.
-- ------------------------------------------------------------
INSERT INTO diagnostics_reference
(reference_type, code, label, description, enabled, sort_order, created_at_utc, updated_at_utc)
VALUES
('SCREEN_CODE', 'AGENDA', 'Agenda', NULL, TRUE, 10, now(), now()),
('SCREEN_CODE', 'INTERVENTION_SEARCH', 'Recherche d''interventions', NULL, TRUE, 20, now(), now()),
('SCREEN_CODE', 'DOSSIER_LIST', 'Dossiers', NULL, TRUE, 30, now(), now()),
('SCREEN_CODE', 'TEAM', 'Équipe', NULL, TRUE, 40, now(), now()),
('SCREEN_CODE', 'TEAM_DETAIL', 'Détail d''équipe', NULL, TRUE, 50, now(), now()),
('SCREEN_CODE', 'CLIENT_LIST', 'Clients', NULL, TRUE, 60, now(), now()),
('SCREEN_CODE', 'SITE_LIST', 'Sites', NULL, TRUE, 70, now(), now()),
('SCREEN_CODE', 'SETTINGS', 'Paramètres', NULL, TRUE, 80, now(), now()),
('SCREEN_CODE', 'RENDEZVOUS_DETAIL', 'Détail de rendez-vous', NULL, TRUE, 90, now(), now()),
('SCREEN_CODE', 'AGENDA_TEAM', 'Agenda d''équipe', NULL, TRUE, 100, now(), now()),
('SCREEN_CODE', 'INTERVENTION_DETAIL', 'Détail d''intervention', NULL, TRUE, 110, now(), now()),
('SCREEN_CODE', 'INTERVENTION_DOCUMENTS', 'Documents d''intervention', NULL, TRUE, 120, now(), now()),
('SCREEN_CODE', 'INTERVENTION_NOTES', 'Notes d''intervention', NULL, TRUE, 130, now(), now()),
('SCREEN_CODE', 'INTERVENTION_NOTES_ADMIN', 'Notes administratives', NULL, TRUE, 140, now(), now()),
('SCREEN_CODE', 'INTERVENTION_EMAILS', 'Courriels d''intervention', NULL, TRUE, 150, now(), now()),
('SCREEN_CODE', 'EMAIL', 'Nouveau courriel', NULL, TRUE, 160, now(), now()),
('SCREEN_CODE', 'RAPPORT', 'Rapport', NULL, TRUE, 170, now(), now()),
('SCREEN_CODE', 'INTERVENTION_RESOURCES', 'Contenus d''intervention', NULL, TRUE, 180, now(), now()),
('SCREEN_CODE', 'PHOTO_VIEWER', 'Visionneuse photo', NULL, TRUE, 190, now(), now()),
('SCREEN_CODE', 'IMAGE_EDITOR', 'Éditeur d''image', NULL, TRUE, 200, now(), now()),
('SCREEN_CODE', 'INTERVENTION_CONTACTS', 'Contacts d''intervention', NULL, TRUE, 210, now(), now()),
('SCREEN_CODE', 'CONTACT_DETAIL', 'Détail de contact', NULL, TRUE, 220, now(), now()),
('SCREEN_CODE', 'DOSSIER_DETAIL', 'Détail de dossier', NULL, TRUE, 230, now(), now()),
('SCREEN_CODE', 'CLIENT_DETAIL', 'Détail de client', NULL, TRUE, 240, now(), now()),
('SCREEN_CODE', 'SITE_DETAIL', 'Détail de site', NULL, TRUE, 250, now(), now()),
('SCREEN_CODE', 'COLLABORATEUR_DETAIL', 'Détail de collaborateur', NULL, TRUE, 260, now(), now()),
('SCREEN_CODE', 'INTERVENTION_EDIT', 'Formulaire intervention', NULL, TRUE, 270, now(), now()),
('SCREEN_CODE', 'SUIVI_TEMPS_CREATE', 'Saisie de temps', NULL, TRUE, 280, now(), now()),
('SCREEN_CODE', 'COMPTE_RENDU_CREATE', 'Compte rendu', NULL, TRUE, 290, now(), now()),
('FEATURE_CODE', 'INTERVENTION_SEARCH_RUN', 'Recherche d''intervention', NULL, TRUE, 10, now(), now()),
('FEATURE_CODE', 'REPORT_GENERATE', 'Génération de rapport', NULL, TRUE, 20, now(), now()),
('FEATURE_CODE', 'EMAIL_SEND', 'Envoi de courriel', NULL, TRUE, 30, now(), now())
ON CONFLICT DO NOTHING;
-- ============================================================
-- END 011. Ordre serveur : 001 -> 002 -> 003 -> 004 -> 005 -> 006
-- -> 007 -> 008 -> 009 -> 010 -> 011. Aucun DELETE, aucune donnée écrasée.
-- ============================================================
@@ -0,0 +1,90 @@
-- ============================================================
-- Omika Diagnostic Center -- 012_diagnostics_tasks.sql
-- T25 : tâches de traitement des bugs récurrents.
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 012_...),
-- AFTER 003 (rôles owner/worker/web) and 010 (diagnostics_users).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (CREATE ... IF NOT EXISTS + GRANT idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- Une ligne = une TÂCHE de traitement liée à un « type de bug », c'est-à-dire une
-- signature d'erreur (code événement + code erreur + exception + composant normalisés).
-- La signature est stockée sous sa forme URL-safe (DiagnosticErrorGroup.UrlIdentifier),
-- identique à celle utilisée par la page /Erreurs/{id}. Une seule tâche par signature
-- (unicité) : toutes les occurrences du même type de bug sont couvertes par la tâche.
--
-- group_url_identifier : identifiant URL-safe du groupe d'erreurs lié (unique, immuable).
-- title : intitulé de la tâche.
-- description : description libre du bug (nullable).
-- cause : cause de l'erreur / analyse root cause (nullable).
-- status : 'Todo' | 'InProgress' | 'Resolved' | 'Rejected' (liste fermée,
-- validée par le code, pas de CHECK arbitraire).
-- created_by_username : username de l'auteur (immuable, jamais renommé en V1).
-- assignee_username : username du responsable (nullable, optionnel).
-- created_at_utc : création.
-- updated_at_utc : dernière mutation.
-- resolved_at_utc : date de résolution/rejet ; NULL si statut non terminal.
--
-- CHOIX DE TYPE : aligné sur diagnostics_clients / diagnostics_users (TEXT, TIMESTAMPTZ,
-- BOOLEAN). Aucun VARCHAR, aucun CHECK de longueur (validation portée par le code).
-- Aucune colonne secret. Aucune colonne « password ».
-- ============================================================
CREATE TABLE IF NOT EXISTS diagnostics_tasks (
id BIGSERIAL PRIMARY KEY,
group_url_identifier TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
cause TEXT,
status TEXT NOT NULL,
created_by_username TEXT NOT NULL,
assignee_username TEXT,
created_at_utc TIMESTAMPTZ NOT NULL,
updated_at_utc TIMESTAMPTZ NOT NULL,
resolved_at_utc TIMESTAMPTZ
);
-- Une seule tâche par signature d'erreur (T25 §4).
CREATE UNIQUE INDEX IF NOT EXISTS ux_diagnostics_tasks_group_url_identifier
ON diagnostics_tasks (group_url_identifier);
-- ------------------------------------------------------------
-- Ownership : omika_diagnostics_owner (comme 003/008/010).
-- ------------------------------------------------------------
ALTER TABLE diagnostics_tasks OWNER TO omika_diagnostics_owner;
ALTER SEQUENCE diagnostics_tasks_id_seq OWNER TO omika_diagnostics_owner;
-- ------------------------------------------------------------
-- Défense PUBLIC (ceinture + bretelles, idempotent).
-- ------------------------------------------------------------
REVOKE ALL ON TABLE diagnostics_tasks FROM PUBLIC;
-- ------------------------------------------------------------
-- Web (Omika.Diagnostics.Web) : SELECT + INSERT + UPDATE
-- colonne-ciblé (moindre privilège) + USAGE séquence.
-- - INSERT : création d'une tâche (id BIGSERIAL).
-- - UPDATE (title, description, cause, status, assignee_username,
-- updated_at_utc, resolved_at_utc) : édition de la tâche.
-- - group_url_identifier et created_by_username ne sont JAMAIS mis à jour.
-- - AUCUN DELETE (aucune suppression physique en V1).
-- ------------------------------------------------------------
GRANT SELECT ON TABLE diagnostics_tasks TO omika_diagnostics_web;
GRANT INSERT ON TABLE diagnostics_tasks TO omika_diagnostics_web;
GRANT UPDATE (title, description, cause, status, assignee_username, updated_at_utc, resolved_at_utc)
ON TABLE diagnostics_tasks TO omika_diagnostics_web;
GRANT USAGE ON SEQUENCE diagnostics_tasks_id_seq TO omika_diagnostics_web;
-- ------------------------------------------------------------
-- API (omika_diagnostics_api) : AUCUN droit sur diagnostics_tasks.
-- Worker (omika_diagnostics_worker) : AUCUN droit sur diagnostics_tasks.
-- Aucun GRANT accordé ; les rôles n'ont aucun privilège par défaut.
-- (Aucun REVOKE nécessaire : aucun privilège n'a été accordé.)
-- ------------------------------------------------------------
-- ============================================================
-- END 012. Ordre serveur : 001 -> 002 -> ... -> 011 -> 012.
-- ============================================================
@@ -0,0 +1,24 @@
-- ============================================================
-- Omika Diagnostic Center -- 013_diagnostics_tasks_delete.sql
-- T25 : autorise la suppression d'une tâche de traitement (V2).
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 013_...),
-- AFTER 012 (table diagnostics_tasks + rôles Web).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (GRANT is idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- Jusqu'en V1, aucune suppression physique n'était prévue : 012 n'accordait AUCUN
-- DELETE. La V2 ajoute un bouton « Supprimer » sur /Taches : on accorde DELETE au
-- seul rôle Web (moindre privilège). L'API (omika_diagnostics_api) et le Worker
-- (omika_diagnostics_worker) n'ont TOUJOURS aucun droit de suppression.
-- ============================================================
GRANT DELETE ON TABLE diagnostics_tasks TO omika_diagnostics_web;
-- ============================================================
-- END 013. Ordre serveur : 001 -> 002 -> ... -> 012 -> 013.
-- ============================================================
@@ -0,0 +1,40 @@
-- ============================================================
-- Omika Diagnostic Center -- 014_diagnostics_telemetry_retention.sql
-- Rétention télémétrie : privilège de purge de telemetry_metric_hourly.
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 014_...),
-- AFTER 003 (rôles) et 001 (table telemetry_metric_hourly).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (GRANT is idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- Context : la boucle DiagnosticTelemetryRetentionWorker purge les agrégats horaires
-- plus anciens que Diagnostics__TelemetryRetentionDays (défaut 180 j = 6 mois), par
-- LOTS BORNÉS :
--
-- DELETE FROM telemetry_metric_hourly
-- WHERE id IN (
-- SELECT id FROM telemetry_metric_hourly
-- WHERE bucket_utc < @cutoff
-- ORDER BY bucket_utc ASC, id ASC
-- LIMIT @batchSize
-- )
--
-- Cette requête requiert :
-- * DELETE sur telemetry_metric_hourly (cible du DELETE) ;
-- * SELECT sur telemetry_metric_hourly (la sous-requête lit la table).
--
-- Jusqu'ici le rôle worker (omika_diagnostics_worker) n'avait AUCUN droit sur cette
-- table (least privilege, cf. 003). On accorde le MINIMUM pour la rétention :
-- SELECT + DELETE uniquement. NO UPDATE, NO TRUNCATE, NO DDL, NO INSERT (l'upsert de
-- télémétrie n'a toujours pas de caller, la table reste non alimentée côté worker).
-- ============================================================
GRANT SELECT, DELETE ON TABLE telemetry_metric_hourly TO omika_diagnostics_worker;
-- ============================================================
-- END 014. Ordre serveur : 001 -> 002 -> 003 -> 004 -> ... -> 013 -> 014.
-- ============================================================
@@ -0,0 +1,51 @@
-- ============================================================
-- Omika Diagnostic Center -- 015_diagnostics_retention_untreated.sql
-- Rétention : ne pas purger les erreurs non traitées (privilège SELECT colonne-ciblé).
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 015_...),
-- AFTER 004 (SELECT/DELETE sur diagnostic_event) et 012 (table diagnostics_tasks).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (GRANT is idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- Règle de conservation : la purge DiagnosticRetentionWorker supprime un événement
-- ancien selon DEUX cas :
-- 1) événement NON-erreur (trace/debug/info/warning, ou level NULL) → toujours purgé ;
-- 2) événement d'ERREUR (error/critical) → purgé SEULEMENT si son groupe possède une
-- tâche (diagnostics_tasks) en statut TERMINAL (Resolved/Rejected). Une erreur non
-- traitée (aucune tâche, ou Todo/InProgress) est conservée même au-delà du cutoff.
--
-- Le DELETE joint donc diagnostics_tasks sur l'identifiant URL-safe du groupe (recalculé
-- en SQL, sans colonne fingerprint) :
--
-- DELETE FROM diagnostic_event
-- WHERE id IN (
-- SELECT id FROM diagnostic_event
-- WHERE timestamp_utc < @cutoff
-- AND (
-- level IS NULL OR LOWER(level) NOT IN ('error', 'critical') OR
-- (LOWER(level) IN ('error', 'critical') AND <group_url_identifier recalculé> IN (
-- SELECT group_url_identifier FROM diagnostics_tasks
-- WHERE LOWER(status) IN ('resolved','rejected')
-- ))
-- )
-- ORDER BY timestamp_utc ASC, id ASC
-- LIMIT @batchSize
-- )
--
-- Jusqu'ici le rôle worker (omika_diagnostics_worker) n'avait AUCUN droit sur
-- diagnostics_tasks (012). On accorde le MINIMUM pour le rapprochement : SELECT
-- UNIQUEMENT sur group_url_identifier (la clé) et status (le statut terminal).
-- NO SELECT sur title/description/cause (notes de support, potentiellement sensibles),
-- NO SELECT sur assignee/created_by (usernames), NO UPDATE/INSERT/DELETE.
-- ============================================================
GRANT SELECT (group_url_identifier, status)
ON TABLE diagnostics_tasks TO omika_diagnostics_worker;
-- ============================================================
-- END 015. Ordre serveur : 001 -> 002 -> ... -> 013 -> 014 -> 015.
-- ============================================================
@@ -0,0 +1,31 @@
-- ============================================================
-- Omika Diagnostic Center -- 016_diagnostics_event_delete.sql
-- Erreurs : autorise la suppression d'un groupe d'erreurs complet (V2).
-- ============================================================
-- TO RUN ON SRV-OMIKA as PostgreSQL superuser (e.g. psql -U postgres -d <base> -f 016_...),
-- AFTER 003 (rôles owner/worker/web).
--
-- Guarantees :
-- - PostgreSQL 18 compatible.
-- - Idempotent : re-running is a no-op (GRANT is idempotent).
-- - Non destructive : NO DROP, NO DELETE/TRUNCATE, no data removal.
-- - NO password, NO secret.
--
-- Jusqu'ici le rôle Web (omika_diagnostics_web) n'avait que SELECT sur
-- diagnostic_event (migration 003) : il lisait les événements mais ne pouvait jamais en
-- supprimer. La V2 ajoute, sur la page /Erreurs, un bouton « Supprimer le groupe » qui
-- supprime TOUTES les occurrences d'un groupe d'erreurs (signature normalisée :
-- event_code + error_code/exception_type/component). Le DELETE est exécuté par le Web,
-- donc on accorde le privilège DELETE au seul rôle Web (moindre privilège).
--
-- Le Worker (omika_diagnostics_worker) conserve déjà SELECT + DELETE (migration 004,
-- rétention par lots bornés) ; son périmètre est inchangé. L'API n'a AUCUN droit de
-- suppression. Le DELETE du Web est borné par la signature du groupe (jamais un DELETE
-- massif non filtré) et restreint aux niveaux d'erreur réels par le code applicatif.
-- ============================================================
GRANT DELETE ON TABLE diagnostic_event TO omika_diagnostics_web;
-- ============================================================
-- END 016. Ordre serveur : 001 -> 002 -> ... -> 015 -> 016.
-- ============================================================