#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Generate the architecture Markdown deliverable and README from arch_data.json.""" import json from pathlib import Path HERE = Path(__file__).resolve().parent ROOT = HERE.parent PUBLIC_URL = "https://8000-ips0uqxecikoswqhoil78.e2b.app" PUBLIC_STATIC_URL = "https://dffi0o2hbh.brewpage.app/" ARCH = json.loads((HERE / "arch_data.json").read_text(encoding="utf-8")) SEED = json.loads((HERE / "seed_data.json").read_text(encoding="utf-8")) DOC = ROOT / "SRM_RSK_Architecture_APEX.md" README = ROOT / "README.md" def cell(v): return str(v if v is not None else "").replace("|", "\\|").replace("\n", "
") def md_table(headers, rows): out = ["| " + " | ".join(cell(h) for h in headers) + " |", "| " + " | ".join("---" for _ in headers) + " |"] for r in rows: out.append("| " + " | ".join(cell(c) for c in r) + " |") return "\n".join(out) def split_col(s): parts = s.split("|", 3) while len(parts) < 4: parts.append("") return parts def schema_table_md(t): cols = [split_col(c) for c in t["c"]] + [split_col(c) for c in ARCH["AUDIT_COLS"]] body = md_table(["Colonne", "Type Oracle", "Null", "PK / FK / règle"], [[f"`{c[0]}`", f"`{c[1]}`", c[2], c[3]] for c in cols]) return (f"#### {t['n']}\n\n" f"**Domaine :** {t['d']} · **PK :** `{t['pk']}` · **FK :** {t['fk']}\n\n" f"{t['p']}\n\n{body}\n") def fmt_num(v, maxd=3): if v is None: return "—" return f"{v:.{maxd}f}".rstrip("0").rstrip(".").replace(".", ",") def limit_md(p): if p["min"] is not None and p["max"] is not None: return f"{fmt_num(p['min'])} – {fmt_num(p['max'])} {p['unite']}" if p["max"] is not None: return f"≤ {fmt_num(p['max'])} {p['unite']}" if p["min"] is not None: return f"≥ {fmt_num(p['min'])} {p['unite']}" return "—" ps = ARCH["PROJECT_SUMMARY"] schema_summary = [[t["n"], t["d"], t["pk"], t["fk"]] for t in ARCH["SCHEMA_TABLES"]] thresholds = [[p["code"], p["libelle"], p["unite"], limit_md(p), p["article"], "Calculé" if p["derived"] else ("Dérogatoire" if not p["actif"] else "Actif")] for p in SEED["parametres"]] KPI_FORMULAS = [ ["Industriels recensés", "COUNT(INDUSTRIEL WHERE DATE_ARCHIVAGE IS NULL)", "INDUSTRIEL", "V_KPI_GENERAL"], ["Taux conformité rejets", "100 × prélèvements CONFORME / prélèvements contrôlés", "V_PRELEVEMENT_CONFORMITE", "V_KPI_GENERAL / MV_KPI_MENSUEL"], ["Taux couverture enquêtes", "100 × industriels actifs enquêtés / industriels actifs", "ENQUETE_CONTROLE", "V_KPI_GENERAL"], ["MED actives", "COUNT(DOSSIER type=MED AND statut=ACTIVE AND échéance ≥ SYSDATE)", "DOSSIER_REGLEMENTAIRE", "V_KPI_GENERAL"], ["Consommation annuelle", "SUM(RELEVE_COMPTEUR.VOLUME_M3) sur l'année", "RELEVE_COMPTEUR", "V_KPI_GENERAL"], ["Délai moyen réclamation", "AVG(DATE_CLOTURE − DATE_OUVERTURE)", "RECLAMATION_INCIDENT", "Colonne virtuelle + V_KPI_GENERAL"], ["Dépassements", "COUNT(V_RESULTAT_CONFORMITE.STATUT_RESULTAT = NON_CONFORME)", "RESULTAT_ANALYSE", "Trigger + V_ALERTES_ACTIVES"], ["Total métaux lourds", "SUM(Cd, Cu, Zn, Cr, Ni, Fe) ≤ 15 mg/L", "RESULTAT_ANALYSE", "V_TOTAL_METAUX / V_RESULTAT_CONFORMITE"], ["Statut cartographique", "Dernier prélèvement par industriel", "V_PRELEVEMENT_CONFORMITE", "V_DERNIER_STATUT_INDUSTRIEL"] ] SOURCES = [ "`Visualisation_Base_Donnees_Industriels_SRM-RSK.pdf` — 10 pages, juin 2026", "`Base_Donnees_Industriels_SRM-RSK-03-11-2026.csv` — 11 onglets opérationnels", "`valeurs limites du cahier des charges d'assainissement liquide de la SRM-RSK.pdf` — pages 15–16, Articles 42 à 48" ] doc = [] doc.append("# SRM-RSK — Base de Données Industriels\n") doc.append("**Architecture relationnelle 3NF, automatisation de la conformité et recommandations Oracle APEX** ") doc.append("*Document généré le 03 novembre 2026 — Senior Database Architect / Oracle APEX Expert*\n") doc.append("---\n") doc.append("## 0. Documents analysés et livrables\n") doc.append("### Sources\n" + "\n".join(f"- {s}" for s in SOURCES) + "\n") doc.append("### Livrables associés\n") doc.append(md_table(["Fichier", "Rôle"], [ ["`SRM_RSK_Dashboard.html`", "Dashboard autonome : saisie des résultats, KPI, conformité, alertes, cartographie, architecture intégrée"], ["`SRM_RSK_Architecture_APEX.md`", "Le présent document : synthèse, schéma complet, contraintes, APEX, navigation"], ["`schema_oracle.sql`", "DDL Oracle : 32 tables dont DIRECTION, référentiels, vues, package PL/SQL, triggers, matérialisation KPI"], ["`dashboard/app.py`", "Serveur de démonstration + API JSON + base SQLite normalisée (prototype du schéma Oracle)"], ["`dashboard/seed_data.json`", "Jeu de démonstration déterministe + valeurs limites réglementaires"], ["`livrables.html`", "Page d'index accessible en ligne : tous les livrables en un clic"], ["`README.md`", "Guide d'utilisation, numérotation et déploiement"] ]) + "\n") doc.append("### Accès en ligne (aperçu live Arena)\n") doc.append(md_table(["Ressource", "Lien public"], [ ["Miroir statique public (sans jeton)", PUBLIC_STATIC_URL], ["Dashboard interactif", f"{PUBLIC_URL}/"], ["Index de tous les livrables", f"{PUBLIC_URL}/livrables.html"], ["Architecture APEX (Markdown)", f"{PUBLIC_URL}/SRM_RSK_Architecture_APEX.md"], ["DDL Oracle (SQL)", f"{PUBLIC_URL}/schema_oracle.sql"], ["README", f"{PUBLIC_URL}/README.md"], ["Santé de l'API", f"{PUBLIC_URL}/api/health"] ]) + "\n") doc.append(f"> **Portée des liens :** le miroir statique `{PUBLIC_STATIC_URL}` est publié sans jeton et reste en ligne jusqu'au 03 décembre 2026 ; il sert le dashboard en mode navigateur et tous les documents. L'aperçu `{PUBLIC_URL}` expose en plus l'API SQLite interactive, mais seulement **tant que le processus du sandbox est actif**. Aucun des deux n'est un hébergement permanent : une disponibilité durable nécessite un hébergeur statique et un backend Oracle + ORDS / APEX.\n") doc.append("> **Exigence centrale :** *« je ne dois saisir que le résultat trouvé des industriels, le reste doit être systématisé »*. Aucun champ de conformité, d'alerte, de KPI ou de total métaux n'est proposé à la saisie. Tout est dérivé.\n") doc.append("## 1. Synthèse projet & objectifs\n") doc.append(f"**{ps['title']}** — {ps['scope']}\n") doc.append(f"**Vision :** {ps['vision']}\n") doc.append(f"**Ancrage du modèle :** {ps['anchor']}\n") doc.append(f"**Automatisation :** {ps['automation']}\n") doc.append("### Objectifs\n") doc.append(md_table(["Objectif", "Description"], ARCH["OBJECTIVES"]) + "\n") doc.append("### Périmètre fonctionnel — 12 onglets de la présentation / 11 onglets du canevas\n") doc.append(md_table(["N°", "Onglet", "Objectif", "Responsable", "Fréquence MAJ", "Tables cibles", "Composant APEX"], ARCH["CANEVAS"]) + "\n") doc.append("### Parties prenantes\n") doc.append(md_table(["Acteur", "Périmètre", "Espace APEX"], ARCH["STAKEHOLDERS"]) + "\n") doc.append("## 2. Modèle de données & schéma relationnel (3NF)\n") doc.append("### 2.1 Principes d'architecture\n") doc.append("- **Clé d'ancrage :** `CODE_INDUSTRIEL` est généré par direction : `IN + CODE_DP + '-' + NNNN` (ex. `INDPK-0001`). `ANCIEN_CODE` conserve `IND-0001…`. Il relie identification, eau, enquêtes, analyses, conformité, zones, réclamations et documents.\n" "- **Direction référentielle :** la table `DIRECTION` porte les 7 codes `CODE_DP`, la province et le compteur `DERNIER_ORDRE` ; le trigger `TRG_INDUSTRIEL_CODE` réserve l'ordre et compose le code sans saisie.\n" "- **Pas de table large d'analyses :** `pH`, `MES`, `DCO`, `DBO5`, métaux… sont des **lignes** dans `RESULTAT_ANALYSE`, pas des colonnes. Ajouter un polluant = ajouter une ligne de référentiel, jamais modifier le schéma.\n" "- **Seuils versionnés :** `REF_SEUIL_REGLEMENTAIRE` porte `DATE_EFFET`, `DATE_FIN`, `PORTEE`, article et texte de référence. Les anciens prélèvements restent évalués avec le seuil de leur époque.\n" "- **Aucune redondance dérivée :** conformité, vigilance, alertes, total métaux, KPI et statut cartographique sont calculés par vues, colonnes virtuelles, triggers et *materialized views*.\n" "- **Référentiels dédiés + listes génériques :** `REF_LISTE` / `REF_VALEUR_LISTE` pour les petites nomenclatures ; tables `REF_*` dédiées pour les domaines à FK fortes.\n" "- **Audit standard :** `CREE_PAR`, `CREE_LE`, `MODIFIE_PAR`, `MODIFIE_LE`, `DATE_ARCHIVAGE` sur toutes les tables métier ; `JOURNAL_MAJ` append-only.\n" "- **Soft delete :** `DATE_ARCHIVAGE` plutôt que DELETE physique ; FK restrict sur les référentiels.\n") doc.append("### 2.2 Vue d'ensemble des relations\n") doc.append("```mermaid\nerDiagram\n REF_PROVINCE ||--o{ ZONE_INDUSTRIELLE : contient\n REF_PROVINCE ||--o{ DIRECTION : porte\n DIRECTION ||--o{ INDUSTRIEL : numerote\n REF_PROVINCE ||--o{ INDUSTRIEL : situe\n REF_SECTEUR_ACTIVITE ||--o{ INDUSTRIEL : classifie\n ZONE_INDUSTRIELLE ||--o{ INDUSTRIEL : accueille\n INDUSTRIEL ||--o{ CONTACT_INDUSTRIEL : possede\n INDUSTRIEL ||--o{ ABONNEMENT : souscrit\n ABONNEMENT ||--o{ COMPTEUR : equipe\n COMPTEUR ||--o{ RELEVE_COMPTEUR : releve\n INDUSTRIEL ||--o{ BRANCHEMENT : raccorde\n INDUSTRIEL ||--o{ ENQUETE_CONTROLE : visite\n ENQUETE_CONTROLE ||--o{ CONSTAT_ENQUETE : constate\n INDUSTRIEL ||--o{ PRELEVEMENT : echantillonne\n PRELEVEMENT ||--o{ RESULTAT_ANALYSE : mesure\n REF_PARAMETRE ||--o{ RESULTAT_ANALYSE : definit\n REF_PARAMETRE ||--o{ REF_SEUIL_REGLEMENTAIRE : limite\n INDUSTRIEL ||--o{ DEROGATION_SEUIL : deroge\n RESULTAT_ANALYSE ||--o| ALERTE_NON_CONFORMITE : declenche\n INDUSTRIEL ||--o{ DOSSIER_REGLEMENTAIRE : concerne\n INDUSTRIEL ||--o{ RECLAMATION_INCIDENT : signale\n INDUSTRIEL ||--o{ DOCUMENT : archive\n APP_UTILISATEUR ||--o{ AFFECTATION_ROLE : affecte\n APP_ROLE ||--o{ AFFECTATION_ROLE : accorde\n```\n") doc.append("### 2.2.1 Convention de numérotation par direction provinciale / préfectorale\n") doc.append(f"**Format :** `{ps['code_format']}`. `IN` = Industrie ; `CODE_DP` identifie la direction ; `NNNN` est l'ordre d'enregistrement **dans cette direction**, remis à 1 pour chaque direction. Le code est calculé par `TRG_INDUSTRIEL_CODE`, jamais saisi.\n") doc.append(md_table(["Ordre", "CODE_DP", "Direction provinciale / préfectorale", "Province / Préfecture", "Exemple"], ARCH["DIRECTIONS"]) + "\n") doc.append("> **Migration et traçabilité :** la colonne `ANCIEN_CODE` conserve l'identifiant historique `IND-0001…` ; les prélèvements, documents et journaux déjà liés ne sont pas recréés. L'import CSV accepte le nouveau code et l'ancien code. Exemple : `IND-0001` → `INDPK-0001`, `IND-0002` → `INDPK-0002`.\n") doc.append(f"### 2.3 Inventaire des {len(ARCH['SCHEMA_TABLES'])} tables\n") doc.append(md_table(["Table", "Domaine", "PK", "FK"], schema_summary) + "\n") doc.append("### 2.4 Dictionnaire de données détaillé\n") doc.append("> Les cinq colonnes d'audit standard sont répétées dans chaque table ci-dessous. Les types sont optimisés pour Oracle : `NUMBER` identité, `VARCHAR2(... CHAR)` pour les accents, `TIMESTAMP(6) WITH TIME ZONE`, `NUMBER(14,4)` pour les mesures, `CLOB`/`BLOB`, `SDO_GEOMETRY` optionnel pour la Map Region.\n") for i, t in enumerate(ARCH["SCHEMA_TABLES"], 1): doc.append(f"#### 2.4.{i} `{t['n']}` — {t['d']}\n") doc.append(f"**Rôle :** {t['p']} \n**PK :** `{t['pk']}` \n**FK :** {t['fk']}\n") cols = [split_col(c) for c in t["c"]] + [split_col(c) for c in ARCH["AUDIT_COLS"]] doc.append(md_table(["Colonne", "Type Oracle", "Null", "PK / FK / règle"], [[f"`{c[0]}`", f"`{c[1]}`", c[2], c[3]] for c in cols]) + "\n") doc.append("### 2.5 Objets calculés — aucune table de KPI à saisir\n") doc.append(md_table(["Objet", "Type", "Rôle"], [ ["`SRM_PKG_CONFORMITE`", "Package PL/SQL", "Seuil applicable (dérogation > industriel > secteur > global), statut légal, vigilance, journalisation"], ["`V_RESULTAT_CONFORMITE`", "Vue", "Un résultat = une valeur comparée au seuil ; inclut le total métaux lourds dérivé"], ["`V_PRELEVEMENT_CONFORMITE`", "Vue", "Agrégation : CONFORME / NON_CONFORME, vigilance, nb dépassements"], ["`V_DERNIER_STATUT_INDUSTRIEL`", "Vue analytique", "Dernier statut par industriel pour la fiche 360° et la carte"], ["`V_KPI_GENERAL` / `V_KPI_MENSUEL`", "Vues", "Indicateurs de la présentation, recalculés à la demande"], ["`MV_KPI_MENSUEL`", "Materialized view", "Snapshot mensuel rafraîchi par `DBMS_SCHEDULER`"], ["`V_ALERTES_ACTIVES`", "Vue", "Dépassements en cours pour APEX Automations / email"], ["`JOURNAL_MAJ`", "Table append-only", "Onglet 11 automatisé par triggers"] ]) + "\n") doc.append("## 3. Validation & contraintes d'intégrité industrielle\n") doc.append(md_table(["Domaine", "Règle", "Implémentation Oracle / APEX", "Finalité"], ARCH["CONSTRAINTS"]) + "\n") doc.append("### 3.1 Règles réglementaires extraites du cahier des charges\n") doc.append("- **Art. 42 :** pH 5,5–8,5 (exception 5,5–9,5 si neutralisation à la chaux), T° ≤ 30 °C, MES ≤ 500 mg/L, DBO5 ≤ 500 mg/L, DCO < 1000 mg/L, azote total ≤ 150 mg/L *ou* ammonium ≤ 200 mg/L, conductivité 2700 µS/cm, huiles et graisses 150 mg/L, écotoxicité conforme AFNOR T90.301, absence de composés cycliques hydroxylés / dérivés halogénés et de substances nuisant au traitement biologique.\n" "- **Art. 43 :** neutralisation / traitement préalable obligatoire pour acides libres, bases fortes, chromates, cyanures, hydrocarbures, gaz nocifs et matières radioactives.\n" "- **Art. 44 :** liste de valeurs limites **non limitative** ; le référentiel doit donc être versionné et extensible. Total métaux lourds (Cd, Cu, Zn, Cr, Ni, Fe) ≤ 15 mg/L.\n" "- **Art. 45 :** déversements interdits (gaz inflammables/toxiques, hydrocarbures halogénés, boues, déchets solides, substances colorantes, eaux non admissibles, déjections animales, substances radioactives…) modélisés comme contrôles qualitatifs 0/1.\n" "- **Art. 47 :** branchements distincts eaux usées domestiques / eaux pluviales (réseau séparatif) / eaux usées industrielles, avec regard accessible.\n" "- **Art. 48 :** prélèvements et contrôles possibles à tout moment à la charge de l'industriel → traçabilité complète des échantillons.\n") doc.append(f"### 3.2 Référentiel des {len(SEED['parametres'])} paramètres et seuils implémentés\n") doc.append(md_table(["Code", "Libellé", "Unité", "Limite", "Article", "Mode"], thresholds) + "\n") doc.append("> `PH_CHAUX` est une dérogation inactive par défaut ; `TOTAL_METAUX` est calculé automatiquement et ne doit jamais être saisi. La liste de l'Art. 44 pouvant être complétée, toute nouvelle limite se crée comme **nouvelle version datée**, sans modifier l'historique.\n") doc.append("## 4. Formules KPI — calcul automatique\n") doc.append(md_table(["KPI", "Formule", "Tables sources", "Objet Oracle / APEX"], KPI_FORMULAS) + "\n") doc.append("Les valeurs **247 industriels, 78 % de conformité, 92 % de couverture, 14 MED, 4,2 M m³ et 6,2 j** de la présentation sont des **repères de juin 2026**. Elles ne sont pas stockées comme saisies : le dashboard les recalcule à partir des faits.\n") doc.append("## 5. Architecture Oracle APEX recommandée\n") doc.append("### 5.1 Composants par domaine\n") doc.append(md_table(["Zone applicative", "Composant APEX", "Automatisation / usage"], ARCH["APEX_COMPONENTS"]) + "\n") doc.append("### 5.2 Navigation logique / menu\n") doc.append(md_table(["Page", "Groupe menu", "Libellé", "Composants", "Rôles"], ARCH["NAVIGATION"]) + "\n") doc.append("### 5.3 Chaîne d'automatisation de bout en bout\n") for i, (t, d) in enumerate(ARCH["AUTO_RULES"], 1): doc.append(f"{i}. **{t}** — {d}") doc.append("\n**Intégrations :** APEX Automations (email, relances MED, refresh MV), ORDS / REST Data Sources (LIMS, SharePoint), Authorization Schemes par rôle, Page 0 pour LOV et filtres globaux, Approvals pour les dérogations et changements de seuils.\n") doc.append("## 6. Dashboard livré — mode d'emploi\n") doc.append("1. Ouvrir `SRM_RSK_Dashboard.html` (mode fichier local, persistance navigateur) ou l'aperçu live du serveur `dashboard/app.py` (mode SQLite normalisé).\n" "2. Aller dans **Saisie des résultats** : choisir l'industriel, la date, puis saisir **uniquement les valeurs mesurées** par paramètre. Le statut se met à jour en direct.\n" "3. Possibilité de coller / importer un CSV laboratoire (format long ou large) ; les doublons, valeurs invraisemblables et paramètres dérivés sont rejetés.\n" "4. Après enregistrement : conformité, vigilance, alertes, total métaux lourds, KPI, cartographie, journal MAJ et exports se mettent à jour automatiquement.\n" "5. Les onglets **Référentiels & seuils** et **Architecture & APEX** contiennent le référentiel réglementaire et le présent document ; **Journal & automatismes** trace toutes les écritures.\n") doc.append("### Ce que l'utilisateur ne saisit jamais\n") doc.append(md_table(["Champ", "Pourquoi"], [ ["Conformité / Non conformité", "Calculée par `V_RESULTAT_CONFORMITE` à partir du seuil applicable à la date du prélèvement"], ["Alerte de dépassement", "Créée par trigger `TRG_RESULTAT_ALERTE` avec écart %, gravité et article"], ["Total métaux lourds", "Somme automatique de Cd, Cu, Zn, Cr, Ni, Fe"], ["KPI", "Vues + materialized view rafraîchie par scheduler"], ["Statut carte / couleur", "Dérivé du dernier prélèvement"], ["Journal MAJ", "Alimenté par triggers d'audit"] ]) + "\n") doc.append("## 7. Feuille de route d'implémentation\n") doc.append(md_table(["Phase", "Actions", "Livrable"], [ ["1 · Fondation", "Exécuter `schema_oracle.sql`, charger référentiels et seuils, configurer rôles / Authorization Schemes", "Schéma 3NF + données de référence"], ["2 · Saisie", "Créer page Master-Detail + Interactive Grid sur `PRELEVEMENT` / `RESULTAT_ANALYSE`, Dynamic Actions de prévisualisation", "Saisie unique sans statut manuel"], ["3 · Moteur", "Valider package, vues, triggers d'alerte et d'audit, MV KPI + job scheduler", "Conformité et KPI automatiques"], ["4 · Pilotage", "Dashboard, Faceted Search, Map Region, charts, alertes, exports", "Application APEX pages 1–14"], ["5 · Intégrations", "ORDS/LIMS, SharePoint, notifications email, approbations seuils/dérogations", "Flux de données et utilisateurs de la présentation"], ["6 · Recette", "Reprendre les 4 prélèvements de la présentation et vérifier 78 % / 92 % / 14 / 4,2 M / 6,2 j", "Rapport de recette et bascule production"] ]) + "\n") doc.append("## 8. Hypothèses et limites\n") doc.append("- Les seuils sont ceux du PDF joint (pages 15–16). La liste de l'Art. 44 étant non limitative, elle doit être validée juridiquement et complétée si la réglementation évolue.\n" "- La conductivité est traitée comme `≤ 2700 µS/cm`, interprétation opérationnelle du libellé « Conductivité 2700 µS/cm ».\n" "- `AZOTE_TOTAL` (150 mg/L) et `AMMONIUM` (200 mg/L) sont deux expressions alternatives de l'Art. 42 ; ne pas exiger les deux pour un même prélèvement.\n" "- Le total métaux lourds est calculé à partir des codes `CD, CU, ZN, CR6, NI, FE` ; la correspondance exacte Cr total / Cr⁶⁺ doit être confirmée avec le laboratoire.\n" "- Le dashboard utilise un jeu de démonstration déterministe : les 4 prélèvements de la présentation sont reproduits à l'identique, les autres mesures sont illustratives. Les valeurs limites, elles, sont réglementaires.\n" "- Le serveur local utilise SQLite normalisée comme prototype ; la cible de production est Oracle + APEX selon `schema_oracle.sql`.\n") DOC.write_text("\n".join(doc), encoding="utf-8") readme = f"""# SRM-RSK — Base de Données Industriels Dashboard automatisé + architecture Oracle / APEX, générés à partir des trois documents joints. ## Démarrage rapide - **Dashboard autonome :** ouvrir `SRM_RSK_Dashboard.html`. Il fonctionne hors ligne avec une persistance locale dans le navigateur. - **Mode serveur (recommandé, déjà disponible dans l'aperçu live) :** ```bash python3 dashboard/app.py # http://0.0.0.0:8000/ ``` Le serveur expose une API JSON et une base **SQLite normalisée** (`dashboard/srm_rsk.db`) qui prototype le schéma Oracle, avec la table `direction` et la migration v2 des codes. Aucune dépendance tierce n'est requise. ## Accès public en ligne | Ressource | Lien public | | --- | --- | | **Miroir statique public (sans jeton)** | **{PUBLIC_STATIC_URL}** | | Dashboard interactif / API | {PUBLIC_URL}/ | | Index de tous les livrables | {PUBLIC_URL}/livrables.html | | Architecture APEX (Markdown) | {PUBLIC_URL}/SRM_RSK_Architecture_APEX.md | | DDL Oracle (SQL) | {PUBLIC_URL}/schema_oracle.sql | | README | {PUBLIC_URL}/README.md | | Santé de l'API | {PUBLIC_URL}/api/health | > **Portée :** `{PUBLIC_STATIC_URL}` est un miroir statique public sans jeton, valable jusqu'au 03 décembre 2026 ; le dashboard y fonctionne en mode navigateur (localStorage). `{PUBLIC_URL}` est l'aperçu live Arena avec API SQLite, partageable tant que le processus du sandbox est actif. Ce ne sont pas des hébergements permanents : pour une disponibilité durable, publier le HTML/docs sur un hébergeur statique et déployer le backend Oracle + ORDS / APEX. ## Numérotation des industriels **Format :** `IN + CODE_DP + '-' + NNNN` — `IN` = Industrie, `CODE_DP` = direction provinciale / préfectorale, `NNNN` = ordre dans cette direction. Exemple : `INDPK-0001` = 1er industriel de la Direction provinciale de Kénitra. Le code est **généré automatiquement** à la création de la fiche ; l'ancien code `IND-0001…` est conservé dans `ANCIEN_CODE` et accepté à l'import. | CODE_DP | Direction | Province / Préfecture | Exemple | | --- | --- | --- | --- | | DPR | Direction provinciale / préfectorale de Rabat | Rabat | `INDPR-0001` | | DPS | Direction provinciale / préfectorale de Salé | Salé | `INDPS-0001` | | DPST | Direction provinciale de Skhirat-Témara | Skhirat-Témara | `INDPST-0001` | | DPK | Direction provinciale de Kénitra | Kénitra | `INDPK-0001` | | DPKH | Direction provinciale de Khemisset | Khemisset | `INDPKH-0001` | | DPSK | Direction provinciale de Sidi Kacem | Sidi Kacem | `INDPSK-0001` | | DPSS | Direction provinciale de Sidi Slimane | Sidi Slimane | `INDPSS-0001` | ## Principe : une seule saisie Dans **Saisie des résultats**, renseignez uniquement : - l'industriel, la date, le point de prélèvement, le laboratoire ; - les **valeurs mesurées** par paramètre. Le système calcule automatiquement : | Dérivé | Mécanisme | |---|---| | Conformité Art. 42 / 44 / 45 | comparaison à `REF_SEUIL_REGLEMENTAIRE` versionné | | Vigilance | valeur proche du seuil, sans non-conformité juridique | | Alertes de dépassement | trigger / règle automatique avec écart % et article | | Total métaux lourds | somme Cd + Cu + Zn + Cr + Ni + Fe ≤ 15 mg/L | | KPI | vues + materialized view, jamais saisis | | Cartographie | dernier statut de chaque industriel | | Journal MAJ | audit append-only de chaque écriture | | Exports | CSV analyses, sauvegarde JSON, modèle d'import | Un import CSV laboratoire est disponible (format long `code_industriel;date;code_parametre;valeur`, ex. `INDPK-0001;2026-11-03;PH;7.2`, ou format large avec une colonne par paramètre). Les anciens codes `IND-0001…` sont résolus automatiquement via `ANCIEN_CODE`. ## Fichiers | Fichier | Description | |---|---| | `SRM_RSK_Dashboard.html` | Dashboard autonome, 9 vues, sans CDN | | `livrables.html` | Index en ligne de tous les livrables | | `SRM_RSK_Public_Site.zip` | Archive du site statique public | | `SRM_RSK_Architecture_APEX.md` | Livrable Markdown complet : synthèse, {len(ARCH['SCHEMA_TABLES'])} tables 3NF, numérotation par direction, contraintes, APEX, navigation | | `schema_oracle.sql` | DDL Oracle : table DIRECTION, trigger de génération de code, référentiels, vues, package PL/SQL, MV KPI | | `dashboard/app.py` | Serveur HTTP + API + SQLite normalisée + migration v2 des codes | | `dashboard/seed_data.json` | Seuils réglementaires + 7 directions + jeu de démonstration | | `dashboard/build.py` | Reconstruit le HTML autonome à partir des sources CSS/JS | | `dashboard/make_seed.py`, `make_oracle_sql.py`, `make_docs.py` | Générateurs reproductibles | ## Déploiement Oracle APEX 1. Exécuter `schema_oracle.sql` sur le schéma cible (Oracle 12c+ / 19c / 23ai). 2. Créer l'application APEX, les Authorization Schemes et la Page 0 (LOV `DIRECTION.CODE_DP`, items globaux). Le code est généré par `TRG_INDUSTRIEL_CODE` ; ne pas le rendre éditable. 3. Implémenter la page 2 « Saisie des résultats » en Master-Detail + Interactive Grid ; aucune colonne de conformité éditable. 4. Activer APEX Automations pour emails, relances MED et rafraîchissement de `MV_KPI_MENSUEL`. 5. Connecter ORDS / SharePoint / LIMS, puis recetter avec les 4 prélèvements de la présentation (codes `INDPK-0001`, `INDPK-0002`, `INDPK-0003`, `INDPS-0001`). ## Données Les valeurs limites proviennent du cahier des charges joint (Articles 42, 44 et 45). Le jeu d'essai est déterministe : les 4 lignes d'analyses de la présentation sont reproduites à l'identique ; les autres mesures sont illustratives. Les 12 industriels de démonstration portent désormais un code par direction (`INDPK-0001` …), avec correspondance historique dans `ancien_code`. Les repères 247 / 78 % / 92 % / 14 / 4,2 M m³ / 6,2 j sont des objectifs de la présentation, **recalculés** par le dashboard, non stockés. Pour réinitialiser la démonstration (codes par direction provinciale) : bouton **Réinit. démo** ou `POST /api/reset`. --- *Généré le 03 novembre 2026 · Région Rabat-Salé-Kénitra · SRM-RSK* """ README.write_text(readme, encoding="utf-8") print(f"Wrote {DOC} ({DOC.stat().st_size:,} bytes) and {README} ({README.stat().st_size:,} bytes)")