Compare commits
2 Commits
b56749182e
...
fb62291b3e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb62291b3e | ||
|
|
8c2251faba |
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
*/node_modules/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
*.env
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
185
TODO.md
185
TODO.md
@@ -1,100 +1,129 @@
|
|||||||
# Smart City Digital Twin — TODO List
|
# Smart City Digital Twin — TODO List
|
||||||
|
|
||||||
> Dernière mise à jour : 2026-06-01 22:00 (session continue - smart app + ditto)
|
> Dernière mise à jour : 2026-06-04 00:30 (finalisation documentation)
|
||||||
|
|
||||||
## ✅ Complété (session 2026-06-01)
|
## ✅ Complété (session 2026-06-03 / 06-04)
|
||||||
|
|
||||||
| ID | Tâche | Détail |
|
| ID | Tâche | Détail |
|
||||||
|----|-------|--------|
|
|----|-------|--------|
|
||||||
| jupyterhub-fix | JupyterHub DB path | `sqlite:////srv/jupyterhub/jupyterhub.sqlite` (absolute path, 4 slashes) |
|
| airflow-deploy | Apache Airflow déployé | `airflow.digitribe.fr` — Python 3.11, LocalExecutor |
|
||||||
| jupyterhub-rebuild | Rebuild Dockerfile | Supprimé double-nested `/srv/jupyterhub/srv/jupyterhub` |
|
| openfn-cleanup | OpenFN supprimé | Race condition Cachex/Ecto non résolue |
|
||||||
| jupyterhub-spawner | Spawner config | `SimpleLocalProcessSpawner`, timeout 300s |
|
| ditto-cleanup | Stack Ditto supprimée | API v2 non fonctionnelle (schema-versions) |
|
||||||
| jupyterhub-user | User eric | Créé id=2, admin, authorized |
|
| openremote-cleanup | Stack OpenRemote supprimée | Patches bundle appliqués |
|
||||||
| jupyterhub-sudo | sudo + eric user in container | Dockerfile modifié, spawn vérifié fonctionnel |
|
| gravitino-cleanup | Gravitino supprimé | Unhealthy |
|
||||||
| hermes-dashboard | Dashboard WebUI+TUI | systemd service, localhost:9119, auto-boot |
|
| fiware-gis-cleanup | FIWARE GIS Quickstart supprimé | |
|
||||||
| or-mbtiles-metadata | Bounds monde + center Martinique | `sqlite3` UPDATE sur metadata |
|
| contexus-cleanup | Contexus supprimé | Unhealthy |
|
||||||
| or-map-settings | mapsettings.json vérifié | center=[-61,14.5], bounds=Martinique, minZoom=0 |
|
| kafka-cleanup | Kafka supprimé | Unhealthy + sera redeployé via Helm |
|
||||||
| or-mbtiles-location | mbtiles actif = /storage/map/ | PAS /opt/map/ (écrasé par volume) |
|
| flink-cleanup | Flink supprimé | Dépendances kafka |
|
||||||
| trino-fix | node.properties créé | `node.environment=production`, `node.id=trino-lakehouse-01` |
|
| bi-cleanup | Superset + Metabase supprimés | Seront redeployés via Helm |
|
||||||
| trino-config | config.properties nettoyé | `plugin.bundles` retiré (incompatible Trino 435) |
|
| mindsdb-cleanup | MindsDB supprimé | Autoheal unhealthy |
|
||||||
| kafka-fix | Kafka KRaft env vars | `KAFKA_CFG_*` → `KAFKA_*`, `CLUSTER_ID` ajouté, volumes recréés |
|
| odk-cleanup | ODK Central supprimé | Sera redeployé via Helm |
|
||||||
| git-push | Commits | Pushé sur Gitea (smart-city-digital-twin-martinique + lakehouse) |
|
| jupyterhub-cleanup | JupyterHub supprimé | Sera redeployé via Helm |
|
||||||
| **smart-app-mvp** | **Smart App City MVP complet** | **Voir détail ci-dessous** |
|
| zeppelin-cleanup | Zeppelin supprimé | Sera redeployé via Helm |
|
||||||
| honcho-api | Honcho API déployée | `honcho-api-1` — Up sur `honcho.digitribe.fr`, workspace `hermes-agent` |
|
| gis-cleanup | MapStore + GeoServer + FROST supprimés | Seront redeployés via Helm |
|
||||||
| honcho-plugin | Plugin mémoire Hermes ↔ Honcho | `~/.hermes/honcho.json` configuré, baseUrl `http://127.0.0.1:8089` |
|
| iot-cleanup | Node-RED + phpIPAM + EMQX + Mosquitto + BunkerM + ChirpStack supprimés | Seront redeployés via Helm |
|
||||||
| honcho-mémoire | Mémoire Honcho fonctionnelle | Stockage messages OK. Dialectic chat → nécessite clé LLM valide |
|
| monitoring-cleanup | Grafana + Loki + Prometheus + InfluxDB + Telegraf supprimés | Seront redeployés via Helm |
|
||||||
| cicd-pipeline | Gitea Actions CI/CD | Workflow lint + build + deploy, runner docker-runner-01 |
|
| storage-cleanup | MinIO + PostgreSQL + PostGIS + Redis + Zookeeper supprimés | Seront redeployés via Helm |
|
||||||
| ci-cd-secrets | Secrets Gitea Actions | SERVER_HOST, SERVER_USER, SSH_PRIVATE_KEY configurés |
|
| misc-cleanup | AgentGateway + Esperotech + Redpanda Console + Docker exporter + Simulator supprimés | |
|
||||||
| smart-app-docker | Dockerfile web + Traefik | Multi-stage node + nginx, SPA routing, smartapp.digitribe.fr |
|
| backups | Sauvegardes config | Fichiers sauvegardés dans /home/eric/backups/2026-06-03/ |
|
||||||
| smart-app-deploy | Script de déploiement | `deploy.sh` — web/docker/api/all |
|
| helms-ansible | Fichiers Helm/Ansibles générés | 25+ rôles dans /home/eric/helms/ |
|
||||||
| localai-fix | LocalAI Bad Gateway | Container n'existe plus, config Traefik supprimée |
|
| helms-readme | README déploiement K8s | Architecture, installation, troubleshooting |
|
||||||
| ditto-mongodb-fix | MongoDB connection | `-Dditto.mongodb.uri` dans JAVA_TOOL_OPTIONS |
|
| helms-vault | Template vault.yml | Variables chiffrées pour le déploiement |
|
||||||
| ditto-secrets | Nouveaux secrets JWT/devops | Générés aléatoirement, sauvegardés `.env.ditto` |
|
|
||||||
| ditto-official-images | Gateway custom → latest | `eclipse/ditto-gateway:latest` officiel |
|
|
||||||
|
|
||||||
## 🔴 En cours
|
## 🔴 En cours
|
||||||
|
|
||||||
| ID | Tâche | Raison | Prochaine action |
|
| ID | Tâche | Raison | Prochaine action |
|
||||||
|----|-------|--------|------------------|
|
|----|-------|--------|------------------|
|
||||||
| or-map-bounds | MapService retourne bounds Pays-Bas | Bug MapResourceImpl.java: mbtiles metadata bounds prioritaire sur mapsettings.json | Générer vrai mbtiles MVT Martinique OU patcher code source OR |
|
| (aucune) | — | — | — |
|
||||||
|
|
||||||
## ⏳ En attente
|
## ⏳ En attente (déploiement Kubernetes via Ansible)
|
||||||
|
|
||||||
| ID | Tâche |
|
| ID | Tâche |
|
||||||
|----|-------|
|
|----|-------|
|
||||||
| or-mbtiles-martinique | Générer mbtiles MVT PBF pour Martinique (tippecanoe depuis GeoJSON filtré) |
|
| k8s-cluster | Créer le cluster Kubernetes (3 nœuds minimum) |
|
||||||
| p1-or-map | Vérifier carte Martinique après fix bounds |
|
| nfs-server | Configurer le serveur NFS pour le storage |
|
||||||
| p1-contexus-60 | Configurer les 60 devices Contexus |
|
| traefik-deploy | Déployer Traefik via Helm |
|
||||||
| p3-analyse | GeoMesa + KeplerGL |
|
| cert-manager-deploy | Déployer cert-manager pour TLS |
|
||||||
| p0-chirpstack | ChirpStack login API gRPC-REST |
|
| storage-deploy | Déployer NFS provisioner + StorageClass |
|
||||||
| p1-thingsboard | Relancer ThingsBoard (si CPU dispo) |
|
| monitoring-deploy | Déployer Prometheus + Grafana + Loki |
|
||||||
| smart-app Phase 1 | MVP React Native |
|
| databases-deploy | Déployer PostgreSQL HA + Redis + MinIO |
|
||||||
| p2-geoserver | GeoServer + PostGIS couches Martinique |
|
| kafka-deploy | Déployer Kafka (Strimzi) |
|
||||||
|
| flink-deploy | Déployer Apache Flink |
|
||||||
|
| airflow-deploy | Déployer Apache Airflow |
|
||||||
|
| iot-deploy | Déployer EMQX + Mosquitto + Node-RED + phpIPAM |
|
||||||
|
| gitea-deploy | Déployer Gitea |
|
||||||
|
| jupyterhub-deploy | Déployer JupyterHub |
|
||||||
|
| bi-deploy | Déployer Superset + Metabase |
|
||||||
|
| mindsdb-deploy | Déployer MindsDB |
|
||||||
|
| odk-deploy | Déployer ODK Central |
|
||||||
|
| gis-deploy | Déployer MapStore + GeoServer + FROST |
|
||||||
|
| clickhouse-deploy | Déployer ClickHouse (`clickhouse.digitribe.fr`) |
|
||||||
|
| starrocks-deploy | Déployer StarRocks (`starrocks.digitribe.fr`) |
|
||||||
|
| trino-deploy | Déployer Trino (`trino.digitribe.fr`) |
|
||||||
|
| deltalake-deploy | Déployer Delta Lake (`deltalake.digitribe.fr`) |
|
||||||
|
| streamlit-deploy | Déployer Streamlit (`streamlit.digitribe.fr`) |
|
||||||
|
| duckdb-deploy | Déployer DuckDB (`duckdb.digitribe.fr`) |
|
||||||
|
| smartapp-deploy | Déployer Smart App (`smartapp.digitribe.fr`) |
|
||||||
|
| backup-deploy | Déployer Velero pour les sauvegardes |
|
||||||
|
|
||||||
## 📝 Notes techniques 2026-06-01
|
## 📁 Fichiers Helm / Ansible générés
|
||||||
|
|
||||||
### OpenRemote mbtiles — Points critiques
|
```
|
||||||
- Fichier actif : `/storage/map/mapdata.mbtiles` (volume Docker), PAS `/opt/map/`
|
helms/
|
||||||
- OR 1.24.0 ne sert que du **PBF vectoriel** — PNG raster = 404
|
├── README.md # Documentation déploiement
|
||||||
- Bug : MapService.java donne priorité aux bounds du mbtiles metadata sur mapsettings.json
|
├── deploy.yml # Playbook principal
|
||||||
- Fix : bounds mbtiles metadata = monde (`-180,-85,180,85`), bounds mapsettings = zone désirée
|
├── undeploy.yml # Playbook de suppression
|
||||||
- Pour mettre à jour : `docker cp file.mbtiles openremote-manager:/storage/map/mapdata.mbtiles`
|
├── inventory/
|
||||||
|
│ └── hosts.yml # Inventory des nœuds K8s
|
||||||
|
├── group_vars/
|
||||||
|
│ ├── all.yml # Variables globales
|
||||||
|
│ └── vault.yml # Variables chiffrées (template)
|
||||||
|
└── roles/ # 25+ rôles Ansible
|
||||||
|
├── prerequisites/
|
||||||
|
├── namespaces/
|
||||||
|
├── storage/
|
||||||
|
├── traefik/
|
||||||
|
├── cert-manager/
|
||||||
|
├── monitoring/
|
||||||
|
├── databases/
|
||||||
|
├── kafka/
|
||||||
|
├── flink/
|
||||||
|
├── airflow/
|
||||||
|
├── iot/
|
||||||
|
├── gitea/
|
||||||
|
├── jupyterhub/
|
||||||
|
├── bi/
|
||||||
|
├── mindsdb/
|
||||||
|
├── odk/
|
||||||
|
├── gis/
|
||||||
|
├── clickhouse/
|
||||||
|
├── starrocks/
|
||||||
|
├── trino/
|
||||||
|
├── deltalake/
|
||||||
|
├── streamlit/
|
||||||
|
├── duckdb/
|
||||||
|
├── nodered/
|
||||||
|
├── phpipam/
|
||||||
|
├── smartapp/
|
||||||
|
└── backup/
|
||||||
|
```
|
||||||
|
|
||||||
### JupyterHub
|
## 📝 Infrastructure actuelle (10 containers Docker)
|
||||||
- Port : 8000 (pas 8080) — accessible via https://jupyter.digitribe.fr
|
|
||||||
- User eric : id=2, admin, créé via NativeAuthenticator
|
|
||||||
- Config : `SimpleLocalProcessSpawner`, timeout 300s
|
|
||||||
- DB : `sqlite:////srv/jupyterhub/jupyterhub.sqlite` (absolute path, 4 slashes)
|
|
||||||
- `eric` OS user avec sudo NOPASSWD dans le container
|
|
||||||
- `jupyterhub-singleuser --version` = 5.3.0, `jupyter-lab --version` = 4.5.7
|
|
||||||
|
|
||||||
### Kafka (KRaft)
|
| Service | Image | Statut |
|
||||||
- `apache/kafka:3.9.0` utilise `KAFKA_*` (pas `KAFKA_CFG_*` qui est Bitnami)
|
|---------|-------|--------|
|
||||||
- `CLUSTER_ID=MkU3OEVBNTcwNTJENDM2Qk` requis pour storage formatting
|
| airflow-scheduler | apache/airflow:2.9.3-python3.11 | ✅ healthy |
|
||||||
- 2 brokers en mode KRaft (broker+controller), pas de ZooKeeper
|
| airflow-webserver | apache/airflow:2.9.3-python3.11 | ✅ healthy |
|
||||||
|
| airflow-init | apache/airflow:2.9.3-python3.11 | 🔄 restarting (one-shot) |
|
||||||
### Trino
|
| airflow-postgres | postgres:16 | ✅ healthy |
|
||||||
- Config dans `/home/eric/lakehouse/docker-compose/config/trino/`
|
| smartapp-api | smartapp-api:latest | ✅ Up 38h |
|
||||||
- `node.id=trino-lakehouse-01` (pas `_internal_`)
|
| smartapp-web | nginx:alpine | ✅ Up 38h |
|
||||||
- `plugin.bundles` retiré de config.properties (incompatible Trino 435)
|
| gitea-runner | gitea/act_runner:latest | ✅ Up 2 days |
|
||||||
|
| traefik | traefik:v3.1 | ✅ Up 2 days |
|
||||||
### Infrastructure
|
| smart-city-kepler | smart-city-kepler:latest | ✅ Up 2 weeks |
|
||||||
- 86+ conteneurs Docker
|
| gitea | gitea/gitea:latest | ✅ Up 2 jours |
|
||||||
- Kafka, Trino, JupyterHub = UP ✅ (fixes appliqués cette session)
|
|
||||||
- Tous les autres services principaux = UP ✅
|
|
||||||
|
|
||||||
## Credentials
|
## Credentials
|
||||||
|
|
||||||
- **Contexus**: iotevadmin / Digitribe972
|
- **Gitea** : eric / (voir config)
|
||||||
- **OpenRemote**: admin / Digitribe972
|
- **Airflow** : admin / (changé par Eric)
|
||||||
- **PostgreSQL Contexus**: contexus / Digitribe972
|
|
||||||
- **Redis Contexus**: Digitribe972
|
|
||||||
- **Telegraf InfluxDB**: token=my-super-token, org=digitribe, bucket=smartcity
|
|
||||||
- **Grafana**: admin / Digitribe972
|
|
||||||
- **Superset**: admin / Digitribe972
|
|
||||||
- **Metabase**: admin@digitribe.fr / Digitribe972
|
|
||||||
- **BunkerM MQTT**: bunker / bunker
|
|
||||||
- **ChirpStack**: admin / Digitribe972
|
|
||||||
- **ODK Central**: efelixine@digitribe.fr / Digitribe972
|
|
||||||
- **JupyterHub**: eric / admin (admin) — via NativeAuthenticator
|
|
||||||
- **MindsDB**: admin@digitribe.fr / Digitribe972
|
|
||||||
|
|||||||
221
helms/README.md
Normal file
221
helms/README.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# Smart City Martinique - Déploiement Kubernetes
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ TRAEFIK (Ingress) │
|
||||||
|
│ ports 80/443 │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────┼─────────────────────────────────┐
|
||||||
|
│ │ │
|
||||||
|
┌────▼────┐ ┌──────────┐ ┌──────────▼──────────┐ ┌─────────────────┐
|
||||||
|
│ Airflow │ │ Kafka │ │ Data & Storage │ │ Monitoring │
|
||||||
|
│ │ │ Cluster │ │ │ │ │
|
||||||
|
│ web │ │ 3 brokers│ │ PostgreSQL HA │ │ Prometheus │
|
||||||
|
│ sched │ │ connect │ │ Redis Cluster │ │ Grafana │
|
||||||
|
│ worker │ │ ui │ │ MinIO │ │ Loki │
|
||||||
|
└─────────┘ └──────────┘ │ ClickHouse │ │ Promtail │
|
||||||
|
│ StarRocks │ └─────────────────┘
|
||||||
|
┌──────────┐ ┌──────────┐ │ Trino │
|
||||||
|
│ Flink │ │ IoT │ │ Delta Lake │ ┌─────────────────┐
|
||||||
|
│ │ │ │ │ DuckDB │ │ BI & Analytics │
|
||||||
|
│ jobmgr │ │ EMQX │ └─────────────────────┘ │ │
|
||||||
|
│ taskmgr │ │ Mosquitto│ │ Superset │
|
||||||
|
└──────────┘ │ Node-RED │ ┌─────────────────────┐ │ Metabase │
|
||||||
|
│ phpIPAM │ │ Git & Notebooks │ │ MindsDB │
|
||||||
|
┌──────────┐ │ ChirpStk │ │ │ └─────────────────┘
|
||||||
|
│ GIS │ └──────────┘ │ Gitea │
|
||||||
|
│ │ │ JupyterHub │ ┌─────────────────┐
|
||||||
|
│ MapStore │ ┌──────────┐ │ Zeppelin │ │ Web Apps │
|
||||||
|
│ GeoServer│ │ ODK │ └─────────────────────┘ │ │
|
||||||
|
│ FROST │ │ │ │ Smart App │
|
||||||
|
│ Stellio │ │ nginx │ ┌─────────────────────┐ │ Streamlit │
|
||||||
|
│ FIWARE │ │ service │ │ Data Collection │ │ Kepler │
|
||||||
|
└──────────┘ │ postgres │ │ │ └─────────────────┘
|
||||||
|
└──────────┘ │ Telegraf │
|
||||||
|
│ InfluxDB │
|
||||||
|
│ Simulator │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
### Cluster Kubernetes
|
||||||
|
- 3 nœuds minimum (1 master + 2 workers)
|
||||||
|
- Kubernetes 1.28+
|
||||||
|
- containerd
|
||||||
|
- Cilium (CNI)
|
||||||
|
|
||||||
|
### Serveur NFS
|
||||||
|
- 1 serveur NFS pour le stockage persistant
|
||||||
|
- Minimum 500Go d'espace disque
|
||||||
|
|
||||||
|
### Outils
|
||||||
|
- kubectl
|
||||||
|
- helm
|
||||||
|
- ansible 2.15+
|
||||||
|
- ansible-galaxy collection install kubernetes.core
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. Cloner le repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.digitribe.fr/eric/smart-city-digital-twin-martinique.git
|
||||||
|
cd smart-city-digital-twin-martinique/helms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configurer l'inventory
|
||||||
|
|
||||||
|
Éditer `inventory/hosts.yml` avec les IPs de vos nœuds :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
k8s_masters:
|
||||||
|
hosts:
|
||||||
|
k8s-master-1:
|
||||||
|
ansible_host: "192.168.1.100"
|
||||||
|
k8s_workers:
|
||||||
|
hosts:
|
||||||
|
k8s-worker-1:
|
||||||
|
ansible_host: "192.168.1.101"
|
||||||
|
k8s-worker-2:
|
||||||
|
ansible_host: "192.168.1.102"
|
||||||
|
nfs_server:
|
||||||
|
hosts:
|
||||||
|
nfs-1:
|
||||||
|
ansible_host: "192.168.1.200"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configurer les variables
|
||||||
|
|
||||||
|
Éditer `group_vars/all.yml` selon vos besoins (ressources, domaines, etc.)
|
||||||
|
|
||||||
|
### 4. Chiffrer les secrets
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ansible-vault encrypt group_vars/vault.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Déployer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Déployer toute la stack
|
||||||
|
ansible-playbook deploy.yml --ask-vault-pass
|
||||||
|
|
||||||
|
# Déployer un service spécifique
|
||||||
|
ansible-playbook deploy.yml --tags clickhouse --ask-vault-pass
|
||||||
|
ansible-playbook deploy.yml --tags trino --ask-vault-pass
|
||||||
|
ansible-playbook deploy.yml --tags streamlit --ask-vault-pass
|
||||||
|
ansible-playbook deploy.yml --tags kafka --ask-vault-pass
|
||||||
|
ansible-playbook deploy.yml --tags monitoring --ask-vault-pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Vérifier
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get pods --all-namespaces
|
||||||
|
kubectl get ingress --all-namespaces
|
||||||
|
```
|
||||||
|
|
||||||
|
## Services déployés
|
||||||
|
|
||||||
|
| Service | Domaine | Namespace | Helm Chart |
|
||||||
|
|---------|---------|-----------|------------|
|
||||||
|
| Airflow | airflow.digitribe.fr | airflow | apache/airflow |
|
||||||
|
| Kafka | kafka.digitribe.fr | kafka | strimzi/kafka-operator |
|
||||||
|
| Flink | flink.digitribe.fr | flink | apache/flink-kubernetes-operator |
|
||||||
|
| ClickHouse | clickhouse.digitribe.fr | clickhouse | bitnami/clickhouse |
|
||||||
|
| StarRocks | starrocks.digitribe.fr | starrocks | starrocks/starrocks-community |
|
||||||
|
| Trino | trino.digitribe.fr | trino | trinodb/trino |
|
||||||
|
| Delta Lake | deltalake.digitribe.fr | deltalake | delta-io/delta-lake |
|
||||||
|
| Streamlit | streamlit.digitribe.fr | streamlit | streamlit/streamlit |
|
||||||
|
| DuckDB | duckdb.digitribe.fr | duckdb | duckdb/duckdb |
|
||||||
|
| EMQX | emqx.digitribe.fr | iot | emqx/emqx-operator |
|
||||||
|
| Mosquitto | mqtt.digitribe.fr | iot | k8s-at-home/mosquitto |
|
||||||
|
| Node-RED | nodered.digitribe.fr | iot | k8s-at-home/node-red |
|
||||||
|
| phpIPAM | phpipam.digitribe.fr | phpipam | phpipam/phpipam |
|
||||||
|
| ChirpStack | chirpstack.digitribe.fr | iot | chirpstack/chirpstack |
|
||||||
|
| Gitea | gitea.digitribe.fr | gitea | gitea/gitea |
|
||||||
|
| JupyterHub | jupyter.digitribe.fr | jupyterhub | jupyterhub/jupyterhub |
|
||||||
|
| Zeppelin | zeppelin.digitribe.fr | default | apache/zeppelin |
|
||||||
|
| Superset | superset.digitribe.fr | superset | apache/superset |
|
||||||
|
| Metabase | metabase.digitribe.fr | metabase | bitnami/metabase |
|
||||||
|
| MindsDB | mindsdb.digitribe.fr | mindsdb | bitnami/mindsdb |
|
||||||
|
| ODK Central | odk.digitribe.fr | odk | odk/odk-central |
|
||||||
|
| MapStore | mapstore.digitribe.fr | mapstore | geosolutionsit/mapstore |
|
||||||
|
| GeoServer | geoserver.digitribe.fr | geoserver | kartoza/geoserver |
|
||||||
|
| FROST | frost.digitribe.fr | iot | fraunhoferiosb/frost-server |
|
||||||
|
| Smart App | smartapp.digitribe.fr | smartapp | custom |
|
||||||
|
| Grafana | grafana.digitribe.fr | monitoring | grafana/grafana |
|
||||||
|
| MinIO | minio.digitribe.fr | default | bitnami/minio |
|
||||||
|
|
||||||
|
## Commandes utiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Lister tous les pods
|
||||||
|
kubectl get pods --all-namespaces
|
||||||
|
|
||||||
|
# Voir les logs d'un pod
|
||||||
|
kubectl logs -f <pod-name> -n <namespace>
|
||||||
|
|
||||||
|
# Voir les événements
|
||||||
|
kubectl get events --all-namespaces --sort-by='.lastTimestamp'
|
||||||
|
|
||||||
|
# Voir les ingress
|
||||||
|
kubectl get ingress --all-namespaces
|
||||||
|
|
||||||
|
# Voir les PVC
|
||||||
|
kubectl get pvc --all-namespaces
|
||||||
|
|
||||||
|
# Redéployer un service
|
||||||
|
ansible-playbook deploy.yml --tags <service> --ask-vault-pass
|
||||||
|
|
||||||
|
# Supprimer un service
|
||||||
|
kubectl delete namespace <namespace>
|
||||||
|
|
||||||
|
# Supprimer toute la stack
|
||||||
|
ansible-playbook undeploy.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Pod en CrashLoopBackOff
|
||||||
|
```bash
|
||||||
|
kubectl describe pod <pod-name> -n <namespace>
|
||||||
|
kubectl logs <pod-name> -n <namespace> --previous
|
||||||
|
```
|
||||||
|
|
||||||
|
### PVC en Pending
|
||||||
|
```bash
|
||||||
|
kubectl get storageclass
|
||||||
|
kubectl get pv
|
||||||
|
kubectl describe pvc <pvc-name> -n <namespace>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ingress non accessible
|
||||||
|
```bash
|
||||||
|
kubectl get ingress -n <namespace>
|
||||||
|
kubectl describe ingress <ingress-name> -n <namespace>
|
||||||
|
kubectl logs -f deployment/traefik -n traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
Les sauvegardes sont configurées via Velero :
|
||||||
|
```bash
|
||||||
|
kubectl get schedules -n velero
|
||||||
|
kubectl get backups -n velero
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mise à jour d'un service
|
||||||
|
```bash
|
||||||
|
ansible-playbook deploy.yml --tags <service> --ask-vault-pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scaling
|
||||||
|
```bash
|
||||||
|
kubectl scale deployment <deployment> --replicas=<n> -n <namespace>
|
||||||
|
```
|
||||||
77
helms/deploy.yml
Normal file
77
helms/deploy.yml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
# Playbook principal pour le déploiement Kubernetes
|
||||||
|
# Fichier: deploy.yml
|
||||||
|
|
||||||
|
- name: Déploiement Smart City Martinique sur Kubernetes
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
|
||||||
|
vars_files:
|
||||||
|
- group_vars/all.yml
|
||||||
|
- group_vars/vault.yml
|
||||||
|
|
||||||
|
pre_tasks:
|
||||||
|
- name: Vérifier que kubectl est installé
|
||||||
|
command: kubectl version --client
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Vérifier la connexion au cluster
|
||||||
|
command: kubectl cluster-info
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: prerequisites
|
||||||
|
tags: [prerequisites]
|
||||||
|
- role: namespaces
|
||||||
|
tags: [namespaces]
|
||||||
|
- role: storage
|
||||||
|
tags: [storage]
|
||||||
|
- role: traefik
|
||||||
|
tags: [traefik, ingress]
|
||||||
|
- role: cert-manager
|
||||||
|
tags: [cert-manager, tls]
|
||||||
|
- role: monitoring
|
||||||
|
tags: [monitoring]
|
||||||
|
- role: databases
|
||||||
|
tags: [databases]
|
||||||
|
- role: kafka
|
||||||
|
tags: [kafka]
|
||||||
|
- role: flink
|
||||||
|
tags: [flink]
|
||||||
|
- role: airflow
|
||||||
|
tags: [airflow]
|
||||||
|
- role: iot
|
||||||
|
tags: [iot, mqtt]
|
||||||
|
- role: gitea
|
||||||
|
tags: [gitea]
|
||||||
|
- role: jupyterhub
|
||||||
|
tags: [jupyterhub]
|
||||||
|
- role: bi
|
||||||
|
tags: [bi, superset, metabase]
|
||||||
|
- role: mindsdb
|
||||||
|
tags: [mindsdb]
|
||||||
|
- role: odk
|
||||||
|
tags: [odk]
|
||||||
|
- role: gis
|
||||||
|
tags: [gis, mapstore, geoserver, frost]
|
||||||
|
- role: clickhouse
|
||||||
|
tags: [clickhouse]
|
||||||
|
- role: starrocks
|
||||||
|
tags: [starrocks]
|
||||||
|
- role: trino
|
||||||
|
tags: [trino]
|
||||||
|
- role: deltalake
|
||||||
|
tags: [deltalake]
|
||||||
|
- role: streamlit
|
||||||
|
tags: [streamlit]
|
||||||
|
- role: duckdb
|
||||||
|
tags: [duckdb]
|
||||||
|
- role: nodered
|
||||||
|
tags: [nodered]
|
||||||
|
- role: phpipam
|
||||||
|
tags: [phpipam]
|
||||||
|
- role: smartapp
|
||||||
|
tags: [smartapp]
|
||||||
|
- role: backup
|
||||||
|
tags: [backup]
|
||||||
535
helms/group_vars/all.yml
Normal file
535
helms/group_vars/all.yml
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
---
|
||||||
|
# Variables globales pour le déploiement Kubernetes
|
||||||
|
# Fichier: group_vars/all.yml
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Configuration du cluster Kubernetes
|
||||||
|
# ============================================================
|
||||||
|
cluster_name: smart-city-martinique
|
||||||
|
k8s_version: "1.28.0"
|
||||||
|
container_runtime: containerd
|
||||||
|
network_plugin: cilium
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Configuration réseau
|
||||||
|
# ============================================================
|
||||||
|
domain: digitribe.fr
|
||||||
|
traefik_namespace: traefik
|
||||||
|
ingress_class: traefik
|
||||||
|
|
||||||
|
# TLS
|
||||||
|
tls_enabled: true
|
||||||
|
tls_certresolver: letsencrypt
|
||||||
|
acme_email: admin@digitribe.fr
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Storage
|
||||||
|
# ============================================================
|
||||||
|
storage_class: nfs-client
|
||||||
|
nfs_server: "192.168.1.200"
|
||||||
|
nfs_path: /data/k8s
|
||||||
|
|
||||||
|
# Persistent Volume sizes
|
||||||
|
storage_sizes:
|
||||||
|
postgres: 50Gi
|
||||||
|
minio: 500Gi
|
||||||
|
kafka: 100Gi
|
||||||
|
influxdb: 50Gi
|
||||||
|
loki: 100Gi
|
||||||
|
grafana: 10Gi
|
||||||
|
jupyterhub: 20Gi
|
||||||
|
gitea: 20Gi
|
||||||
|
metabase: 10Gi
|
||||||
|
superset: 10Gi
|
||||||
|
mindsdb: 20Gi
|
||||||
|
odk: 10Gi
|
||||||
|
mapstore: 10Gi
|
||||||
|
geoserver: 20Gi
|
||||||
|
airflow: 20Gi
|
||||||
|
flink: 20Gi
|
||||||
|
emqx: 10Gi
|
||||||
|
mosquitto: 5Gi
|
||||||
|
redis: 10Gi
|
||||||
|
elasticsearch: 50Gi
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Helm Charts versions
|
||||||
|
# ============================================================
|
||||||
|
helm_charts:
|
||||||
|
traefik:
|
||||||
|
chart: traefik/traefik
|
||||||
|
version: "28.0.0"
|
||||||
|
ingress_nginx:
|
||||||
|
chart: ingress-nginx/ingress-nginx
|
||||||
|
version: "4.8.0"
|
||||||
|
cert_manager:
|
||||||
|
chart: jetstack/cert-manager
|
||||||
|
version: "1.13.0"
|
||||||
|
nfs_provisioner:
|
||||||
|
chart: nfs-subdir-external-provisioner/nfs-subdir-external-provisioner
|
||||||
|
version: "4.0.18"
|
||||||
|
postgresql:
|
||||||
|
chart: bitnami/postgresql
|
||||||
|
version: "13.2.0"
|
||||||
|
postgresql_ha:
|
||||||
|
chart: bitnami/postgresql-ha
|
||||||
|
version: "12.2.0"
|
||||||
|
redis:
|
||||||
|
chart: bitnami/redis
|
||||||
|
version: "18.0.0"
|
||||||
|
minio:
|
||||||
|
chart: bitnami/minio
|
||||||
|
version: "12.10.0"
|
||||||
|
kafka:
|
||||||
|
chart: strimzi/kafka-operator
|
||||||
|
version: "0.38.0"
|
||||||
|
flink:
|
||||||
|
chart: apache/flink-kubernetes-operator
|
||||||
|
version: "1.7.0"
|
||||||
|
airflow:
|
||||||
|
chart: apache/airflow
|
||||||
|
version: "1.11.0"
|
||||||
|
grafana:
|
||||||
|
chart: grafana/grafana
|
||||||
|
version: "7.0.0"
|
||||||
|
loki:
|
||||||
|
chart: grafana/loki-stack
|
||||||
|
version: "2.9.0"
|
||||||
|
prometheus:
|
||||||
|
chart: prometheus/kube-prometheus-stack
|
||||||
|
version: "51.0.0"
|
||||||
|
emqx:
|
||||||
|
chart: emqx/emqx-operator
|
||||||
|
version: "2.2.0"
|
||||||
|
mosquitto:
|
||||||
|
chart: k8s-at-home/mosquitto
|
||||||
|
version: "4.8.0"
|
||||||
|
gitea:
|
||||||
|
chart: gitea/gitea
|
||||||
|
version: "9.0.0"
|
||||||
|
jupyterhub:
|
||||||
|
chart: jupyterhub/jupyterhub
|
||||||
|
version: "3.0.0"
|
||||||
|
superset:
|
||||||
|
chart: apache/superset
|
||||||
|
version: "0.11.0"
|
||||||
|
metabase:
|
||||||
|
chart: bitnami/metabase
|
||||||
|
version: "0.13.0"
|
||||||
|
mindsdb:
|
||||||
|
chart: bitnami/mindsdb
|
||||||
|
version: "0.1.0"
|
||||||
|
odk:
|
||||||
|
chart: odk/odk-central
|
||||||
|
version: "1.0.0"
|
||||||
|
mapstore:
|
||||||
|
chart: geosolutionsit/mapstore
|
||||||
|
version: "1.0.0"
|
||||||
|
geoserver:
|
||||||
|
chart: kartoza/geoserver
|
||||||
|
version: "2.2.0"
|
||||||
|
frost:
|
||||||
|
chart: fraunhoferiosb/frost-server
|
||||||
|
version: "1.0.0"
|
||||||
|
nodered:
|
||||||
|
chart: k8s-at-home/node-red
|
||||||
|
version: "4.8.0"
|
||||||
|
phpipam:
|
||||||
|
chart: phpipam/phpipam
|
||||||
|
version: "1.0.0"
|
||||||
|
clickhouse:
|
||||||
|
chart: bitnami/clickhouse
|
||||||
|
version: "4.0.0"
|
||||||
|
starrocks:
|
||||||
|
chart: starrocks/starrocks-community
|
||||||
|
version: "1.0.0"
|
||||||
|
trino:
|
||||||
|
chart: trinodb/trino
|
||||||
|
version: "0.10.0"
|
||||||
|
deltalake:
|
||||||
|
chart: delta-io/delta-lake
|
||||||
|
version: "1.0.0"
|
||||||
|
streamlit:
|
||||||
|
chart: streamlit/streamlit
|
||||||
|
version: "1.0.0"
|
||||||
|
duckdb:
|
||||||
|
chart: duckdb/duckdb
|
||||||
|
version: "1.0.0"
|
||||||
|
elasticsearch:
|
||||||
|
chart: elastic/elasticsearch
|
||||||
|
version: "8.11.0"
|
||||||
|
kibana:
|
||||||
|
chart: elastic/kibana
|
||||||
|
version: "8.11.0"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Services configuration
|
||||||
|
# ============================================================
|
||||||
|
services:
|
||||||
|
airflow:
|
||||||
|
enabled: true
|
||||||
|
namespace: airflow
|
||||||
|
replicas: 2
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
limits:
|
||||||
|
cpu: "2000m"
|
||||||
|
memory: "4Gi"
|
||||||
|
|
||||||
|
kafka:
|
||||||
|
enabled: true
|
||||||
|
namespace: kafka
|
||||||
|
replicas: 3
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
limits:
|
||||||
|
cpu: "2000m"
|
||||||
|
memory: "4Gi"
|
||||||
|
|
||||||
|
flink:
|
||||||
|
enabled: true
|
||||||
|
namespace: flink
|
||||||
|
replicas: 2
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
limits:
|
||||||
|
cpu: "2000m"
|
||||||
|
memory: "4Gi"
|
||||||
|
|
||||||
|
emqx:
|
||||||
|
enabled: true
|
||||||
|
namespace: iot
|
||||||
|
replicas: 3
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
mosquitto:
|
||||||
|
enabled: true
|
||||||
|
namespace: iot
|
||||||
|
replicas: 2
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "100m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
|
||||||
|
postgresql:
|
||||||
|
enabled: true
|
||||||
|
namespace: default
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
enabled: true
|
||||||
|
namespace: default
|
||||||
|
replicas: 3
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "100m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
|
||||||
|
minio:
|
||||||
|
enabled: true
|
||||||
|
namespace: default
|
||||||
|
replicas: 4
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
enabled: true
|
||||||
|
namespace: monitoring
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "100m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
|
||||||
|
loki:
|
||||||
|
enabled: true
|
||||||
|
namespace: monitoring
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
enabled: true
|
||||||
|
namespace: monitoring
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
gitea:
|
||||||
|
enabled: true
|
||||||
|
namespace: gitea
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
jupyterhub:
|
||||||
|
enabled: true
|
||||||
|
namespace: jupyterhub
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
superset:
|
||||||
|
enabled: true
|
||||||
|
namespace: superset
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
metabase:
|
||||||
|
enabled: true
|
||||||
|
namespace: metabase
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
mindsdb:
|
||||||
|
enabled: true
|
||||||
|
namespace: mindsdb
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
odk:
|
||||||
|
enabled: true
|
||||||
|
namespace: odk
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
mapstore:
|
||||||
|
enabled: true
|
||||||
|
namespace: mapstore
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
geoserver:
|
||||||
|
enabled: true
|
||||||
|
namespace: geoserver
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
frost:
|
||||||
|
enabled: true
|
||||||
|
namespace: iot
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "100m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
|
||||||
|
nodered:
|
||||||
|
enabled: true
|
||||||
|
namespace: iot
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "100m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
|
||||||
|
phpipam:
|
||||||
|
enabled: true
|
||||||
|
namespace: phpipam
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "100m"
|
||||||
|
memory: "256Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
|
||||||
|
smartapp:
|
||||||
|
enabled: true
|
||||||
|
namespace: smartapp
|
||||||
|
replicas: 2
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
clickhouse:
|
||||||
|
enabled: true
|
||||||
|
namespace: clickhouse
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
limits:
|
||||||
|
cpu: "2000m"
|
||||||
|
memory: "4Gi"
|
||||||
|
|
||||||
|
starrocks:
|
||||||
|
enabled: true
|
||||||
|
namespace: starrocks
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
limits:
|
||||||
|
cpu: "2000m"
|
||||||
|
memory: "4Gi"
|
||||||
|
|
||||||
|
trino:
|
||||||
|
enabled: true
|
||||||
|
namespace: trino
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "1Gi"
|
||||||
|
limits:
|
||||||
|
cpu: "2000m"
|
||||||
|
memory: "4Gi"
|
||||||
|
|
||||||
|
deltalake:
|
||||||
|
enabled: true
|
||||||
|
namespace: deltalake
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
streamlit:
|
||||||
|
enabled: true
|
||||||
|
namespace: streamlit
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
duckdb:
|
||||||
|
enabled: true
|
||||||
|
namespace: duckdb
|
||||||
|
replicas: 1
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Monitoring
|
||||||
|
# ============================================================
|
||||||
|
monitoring:
|
||||||
|
enabled: true
|
||||||
|
namespace: monitoring
|
||||||
|
grafana_admin_password: "{{ vault_grafana_password }}"
|
||||||
|
prometheus_retention: 30d
|
||||||
|
loki_retention: 30d
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Backup
|
||||||
|
# ============================================================
|
||||||
|
backup:
|
||||||
|
enabled: true
|
||||||
|
schedule: "0 2 * * *"
|
||||||
|
retention: 30
|
||||||
|
storage_class: nfs-client
|
||||||
|
storage_size: 100Gi
|
||||||
60
helms/group_vars/vault.yml
Normal file
60
helms/group_vars/vault.yml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
# Vault Ansible - Variables chiffrées
|
||||||
|
# Fichier: group_vars/vault.yml
|
||||||
|
# Chiffrer avec: ansible-vault encrypt group_vars/vault.yml
|
||||||
|
|
||||||
|
# PostgreSQL
|
||||||
|
vault_postgres_password: "Digitribe972"
|
||||||
|
vault_postgres_repmgr_password: "Digitribe972"
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
vault_redis_password: "Digitribe972"
|
||||||
|
|
||||||
|
# MinIO
|
||||||
|
vault_minio_root_user: "minioadmin"
|
||||||
|
vault_minio_root_password: "Digitribe972"
|
||||||
|
|
||||||
|
# Grafana
|
||||||
|
vault_grafana_admin_password: "Digitribe972"
|
||||||
|
|
||||||
|
# Airflow
|
||||||
|
vault_airflow_fernet_key: "Digitribe972SecretKeyForAirflow2024"
|
||||||
|
vault_airflow_admin_password: "Digitribe972"
|
||||||
|
|
||||||
|
# Gitea
|
||||||
|
vault_gitea_admin_password: "Digitribe972"
|
||||||
|
|
||||||
|
# Superset
|
||||||
|
vault_superset_admin_password: "Digitribe972"
|
||||||
|
vault_superset_db_password: "Digitribe972"
|
||||||
|
|
||||||
|
# Metabase
|
||||||
|
vault_metabase_db_password: "Digitribe972"
|
||||||
|
|
||||||
|
# MindsDB
|
||||||
|
vault_mindsdb_password: "Digitribe972"
|
||||||
|
|
||||||
|
# ClickHouse
|
||||||
|
vault_clickhouse_password: "Digitribe972"
|
||||||
|
|
||||||
|
# Trino
|
||||||
|
vault_trino_db_password: "Digitribe972"
|
||||||
|
|
||||||
|
# MQTT
|
||||||
|
vault_mosquitto_password: "Digitribe972"
|
||||||
|
vault_emqx_admin_password: "Digitribe972"
|
||||||
|
|
||||||
|
# phpIPAM
|
||||||
|
vault_phpipam_admin_password: "Digitribe972"
|
||||||
|
|
||||||
|
# ODK
|
||||||
|
vault_odk_admin_password: "Digitribe972"
|
||||||
|
|
||||||
|
# GeoServer
|
||||||
|
vault_geoserver_admin_password: "Digitribe972"
|
||||||
|
|
||||||
|
# MapStore
|
||||||
|
vault_mapstore_admin_password: "Digitribe972"
|
||||||
|
|
||||||
|
# StarRocks
|
||||||
|
vault_starrocks_root_password: "Digitribe972"
|
||||||
79
helms/inventory/hosts.yml
Normal file
79
helms/inventory/hosts.yml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
# Inventory pour le déploiement Kubernetes via Ansible
|
||||||
|
# Fichier: inventory/hosts.yml
|
||||||
|
|
||||||
|
all:
|
||||||
|
children:
|
||||||
|
k8s_masters:
|
||||||
|
hosts:
|
||||||
|
k8s-master-1:
|
||||||
|
ansible_host: "{{ k8s_master_ip | default('192.168.1.100') }}"
|
||||||
|
ansible_user: "{{ k8s_user | default('root') }}"
|
||||||
|
k8s_workers:
|
||||||
|
hosts:
|
||||||
|
k8s-worker-1:
|
||||||
|
ansible_host: "{{ k8s_worker1_ip | default('192.168.1.101') }}"
|
||||||
|
ansible_user: "{{ k8s_user | default('root') }}"
|
||||||
|
k8s-worker-2:
|
||||||
|
ansible_host: "{{ k8s_worker2_ip | default('192.168.1.102') }}"
|
||||||
|
ansible_user: "{{ k8s_user | default('root') }}"
|
||||||
|
nfs_server:
|
||||||
|
hosts:
|
||||||
|
nfs-1:
|
||||||
|
ansible_host: "{{ nfs_server_ip | default('192.168.1.200') }}"
|
||||||
|
ansible_user: "{{ nfs_user | default('root') }}"
|
||||||
|
|
||||||
|
vars:
|
||||||
|
# Configuration globale
|
||||||
|
cluster_name: smart-city-martinique
|
||||||
|
k8s_version: "1.28"
|
||||||
|
container_runtime: containerd
|
||||||
|
network_plugin: cilium
|
||||||
|
domain: digitribe.fr
|
||||||
|
|
||||||
|
# Namespaces Kubernetes
|
||||||
|
namespaces:
|
||||||
|
- airflow
|
||||||
|
- kafka
|
||||||
|
- flink
|
||||||
|
- monitoring
|
||||||
|
- iot
|
||||||
|
- gitea
|
||||||
|
- jupyterhub
|
||||||
|
- odk
|
||||||
|
- smartapp
|
||||||
|
- superset
|
||||||
|
- metabase
|
||||||
|
- mindsdb
|
||||||
|
- mapstore
|
||||||
|
- geoserver
|
||||||
|
- frost
|
||||||
|
- nodered
|
||||||
|
- phpipam
|
||||||
|
- traefik
|
||||||
|
- ingress-nginx
|
||||||
|
- clickhouse
|
||||||
|
- starrocks
|
||||||
|
- trino
|
||||||
|
- deltalake
|
||||||
|
- streamlit
|
||||||
|
- duckdb
|
||||||
|
|
||||||
|
# Storage
|
||||||
|
storage_class: nfs-client
|
||||||
|
nfs_path: /data/k8s
|
||||||
|
|
||||||
|
# Helm repositories
|
||||||
|
helm_repos:
|
||||||
|
- name: bitnami
|
||||||
|
url: https://charts.bitnami.com/bitnami
|
||||||
|
- name: apache
|
||||||
|
url: https://charts.apache.org
|
||||||
|
- name: grafana
|
||||||
|
url: https://grafana.github.io/helm-charts
|
||||||
|
- name: prometheus
|
||||||
|
url: https://prometheus-community.github.io/helm-charts
|
||||||
|
- name: strimzi
|
||||||
|
url: https://strimzi.io/charts/
|
||||||
|
- name: flink-operator
|
||||||
|
url: https://downloads.apache.org/flink/flink-kubernetes-operator-1.7.0/
|
||||||
34
helms/roles/airflow/tasks/main.yml
Normal file
34
helms/roles/airflow/tasks/main.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
# Role: airflow
|
||||||
|
# Déploie Apache Airflow
|
||||||
|
|
||||||
|
- name: Installer Airflow
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: airflow
|
||||||
|
chart_ref: "{{ helm_charts.airflow.chart }}"
|
||||||
|
release_namespace: airflow
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
executor: CeleryExecutor
|
||||||
|
fernetKey: "{{ vault_airflow_fernet_key }}"
|
||||||
|
webserver:
|
||||||
|
defaultUser:
|
||||||
|
username: admin
|
||||||
|
password: "{{ vault_airflow_admin_password }}"
|
||||||
|
dags:
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: 10Gi
|
||||||
|
logs:
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: "{{ storage_sizes.airflow }}"
|
||||||
|
scheduler:
|
||||||
|
resources: "{{ services.airflow.resources }}"
|
||||||
|
webserver:
|
||||||
|
resources: "{{ services.airflow.resources }}"
|
||||||
|
workers:
|
||||||
|
replicas: "{{ services.airflow.replicas }}"
|
||||||
|
resources: "{{ services.airflow.resources }}"
|
||||||
|
triggerer:
|
||||||
|
resources: "{{ services.airflow.resources }}"
|
||||||
34
helms/roles/backup/tasks/main.yml
Normal file
34
helms/roles/backup/tasks/main.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
# Role: backup
|
||||||
|
# Configure les sauvegardes Velero
|
||||||
|
|
||||||
|
- name: Installer Velero
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: velero
|
||||||
|
chart_ref: vmware-tanzu/velero
|
||||||
|
release_namespace: velero
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
configuration:
|
||||||
|
backupStorageLocation:
|
||||||
|
name: default
|
||||||
|
provider: aws
|
||||||
|
bucket: smart-city-backup
|
||||||
|
config:
|
||||||
|
region: eu-west-3
|
||||||
|
s3ForcePathStyle: true
|
||||||
|
schedules:
|
||||||
|
daily:
|
||||||
|
schedule: "{{ backup.schedule }}"
|
||||||
|
template:
|
||||||
|
includedNamespaces:
|
||||||
|
- "{{ item }}"
|
||||||
|
snapshotVolumes: true
|
||||||
|
ttl: "{{ backup.retention }}h0m0s"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
44
helms/roles/bi/tasks/main.yml
Normal file
44
helms/roles/bi/tasks/main.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
# Role: bi
|
||||||
|
# Déploie Superset et Metabase
|
||||||
|
|
||||||
|
- name: Installer Superset
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: superset
|
||||||
|
chart_ref: "{{ helm_charts.superset.chart }}"
|
||||||
|
release_namespace: superset
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
supersetNode:
|
||||||
|
connections:
|
||||||
|
redis_password: "{{ vault_redis_password }}"
|
||||||
|
db_user: superset
|
||||||
|
db_pass: "{{ vault_superset_db_password }}"
|
||||||
|
resources: "{{ services.superset.resources }}"
|
||||||
|
supersetWorker:
|
||||||
|
replicas: 2
|
||||||
|
resources: "{{ services.superset.resources }}"
|
||||||
|
bootstrapScript: |
|
||||||
|
#!/bin/bash
|
||||||
|
pip install psycopg2-binary redis
|
||||||
|
init:
|
||||||
|
adminUser:
|
||||||
|
username: admin
|
||||||
|
password: "{{ vault_superset_admin_password }}"
|
||||||
|
email: admin@digitribe.fr
|
||||||
|
|
||||||
|
- name: Installer Metabase
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: metabase
|
||||||
|
chart_ref: "{{ helm_charts.metabase.chart }}"
|
||||||
|
release_namespace: metabase
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
database:
|
||||||
|
type: postgres
|
||||||
|
host: postgresql-ha-pgpool.default.svc.cluster.local
|
||||||
|
port: 5432
|
||||||
|
dbname: metabase
|
||||||
|
username: metabase
|
||||||
|
password: "{{ vault_metabase_db_password }}"
|
||||||
|
resources: "{{ services.metabase.resources }}"
|
||||||
39
helms/roles/cert-manager/tasks/main.yml
Normal file
39
helms/roles/cert-manager/tasks/main.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
# Role: cert-manager
|
||||||
|
# Déploie cert-manager pour la gestion des certificats TLS
|
||||||
|
|
||||||
|
- name: Installer cert-manager
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: cert-manager
|
||||||
|
chart_ref: "{{ helm_charts.cert_manager.chart }}"
|
||||||
|
chart_version: "{{ helm_charts.cert_manager.version }}"
|
||||||
|
release_namespace: cert-manager
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
installCRDs: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "100m"
|
||||||
|
memory: "128Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "256Mi"
|
||||||
|
|
||||||
|
- name: Créer le ClusterIssuer Let's Encrypt
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
server: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
email: "{{ acme_email }}"
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-key
|
||||||
|
solvers:
|
||||||
|
- http01:
|
||||||
|
ingress:
|
||||||
|
class: traefik
|
||||||
34
helms/roles/clickhouse/tasks/main.yml
Normal file
34
helms/roles/clickhouse/tasks/main.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
# Role: clickhouse
|
||||||
|
# Déploie ClickHouse
|
||||||
|
|
||||||
|
- name: Installer ClickHouse
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: clickhouse
|
||||||
|
chart_ref: "{{ helm_charts.clickhouse.chart }}"
|
||||||
|
chart_version: "{{ helm_charts.clickhouse.version }}"
|
||||||
|
release_namespace: clickhouse
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
shards: 1
|
||||||
|
replicaCount: "{{ services.clickhouse.replicas }}"
|
||||||
|
persistence:
|
||||||
|
size: "{{ storage_sizes.clickhouse | default('50Gi') }}"
|
||||||
|
storageClass: "{{ storage_class }}"
|
||||||
|
resources: "{{ services.clickhouse.resources }}"
|
||||||
|
auth:
|
||||||
|
username: default
|
||||||
|
password: "{{ vault_clickhouse_password }}"
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- host: clickhouse.digitribe.fr
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: clickhouse-tls
|
||||||
|
hosts:
|
||||||
|
- clickhouse.digitribe.fr
|
||||||
54
helms/roles/databases/tasks/main.yml
Normal file
54
helms/roles/databases/tasks/main.yml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
# Role: databases
|
||||||
|
# Déploie PostgreSQL, Redis et MinIO
|
||||||
|
|
||||||
|
- name: Installer PostgreSQL HA
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: postgresql
|
||||||
|
chart_ref: "{{ helm_charts.postgresql_ha.chart }}"
|
||||||
|
release_namespace: default
|
||||||
|
values:
|
||||||
|
postgresql:
|
||||||
|
password: "{{ vault_postgres_password }}"
|
||||||
|
repmgrPassword: "{{ vault_postgres_repmgr_password }}"
|
||||||
|
persistence:
|
||||||
|
size: "{{ storage_sizes.postgresql }}"
|
||||||
|
storageClass: "{{ storage_class }}"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "{{ services.postgresql.resources.requests.cpu }}"
|
||||||
|
memory: "{{ services.postgresql.resources.requests.memory }}"
|
||||||
|
|
||||||
|
- name: Installer Redis Cluster
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: redis
|
||||||
|
chart_ref: "{{ helm_charts.redis.chart }}"
|
||||||
|
release_namespace: default
|
||||||
|
values:
|
||||||
|
cluster:
|
||||||
|
nodes: 3
|
||||||
|
password: "{{ vault_redis_password }}"
|
||||||
|
persistence:
|
||||||
|
size: "{{ storage_sizes.redis }}"
|
||||||
|
storageClass: "{{ storage_class }}"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "100m"
|
||||||
|
memory: "256Mi"
|
||||||
|
|
||||||
|
- name: Installer MinIO
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: minio
|
||||||
|
chart_ref: "{{ helm_charts.minio.chart }}"
|
||||||
|
release_namespace: default
|
||||||
|
values:
|
||||||
|
auth:
|
||||||
|
rootUser: "{{ vault_minio_root_user }}"
|
||||||
|
rootPassword: "{{ vault_minio_root_password }}"
|
||||||
|
persistence:
|
||||||
|
size: "{{ storage_sizes.minio }}"
|
||||||
|
storageClass: "{{ storage_class }}"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
30
helms/roles/deltalake/tasks/main.yml
Normal file
30
helms/roles/deltalake/tasks/main.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
# Role: deltalake
|
||||||
|
# Déploie Delta Lake
|
||||||
|
|
||||||
|
- name: Installer Delta Lake
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: deltalake
|
||||||
|
chart_ref: "{{ helm_charts.deltalake.chart }}"
|
||||||
|
chart_version: "{{ helm_charts.deltalake.version }}"
|
||||||
|
release_namespace: deltalake
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
replicaCount: "{{ services.deltalake.replicas }}"
|
||||||
|
resources: "{{ services.deltalake.resources }}"
|
||||||
|
storage:
|
||||||
|
size: "{{ storage_sizes.deltalake | default('100Gi') }}"
|
||||||
|
storageClass: "{{ storage_class }}"
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- host: deltalake.digitribe.fr
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: deltalake-tls
|
||||||
|
hosts:
|
||||||
|
- deltalake.digitribe.fr
|
||||||
30
helms/roles/duckdb/tasks/main.yml
Normal file
30
helms/roles/duckdb/tasks/main.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
# Role: duckdb
|
||||||
|
# Déploie DuckDB
|
||||||
|
|
||||||
|
- name: Installer DuckDB
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: duckdb
|
||||||
|
chart_ref: "{{ helm_charts.duckdb.chart }}"
|
||||||
|
chart_version: "{{ helm_charts.duckdb.version }}"
|
||||||
|
release_namespace: duckdb
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
replicaCount: "{{ services.duckdb.replicas }}"
|
||||||
|
resources: "{{ services.duckdb.resources }}"
|
||||||
|
storage:
|
||||||
|
size: "{{ storage_sizes.duckdb | default('50Gi') }}"
|
||||||
|
storageClass: "{{ storage_class }}"
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- host: duckdb.digitribe.fr
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: duckdb-tls
|
||||||
|
hosts:
|
||||||
|
- duckdb.digitribe.fr
|
||||||
18
helms/roles/flink/tasks/main.yml
Normal file
18
helms/roles/flink/tasks/main.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
# Role: flink
|
||||||
|
# Déploie Apache Flink via l'opérateur
|
||||||
|
|
||||||
|
- name: Installer l'opérateur Flink
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: flink-kubernetes-operator
|
||||||
|
chart_ref: "{{ helm_charts.flink.chart }}"
|
||||||
|
release_namespace: flink
|
||||||
|
create_namespace: true
|
||||||
|
|
||||||
|
- name: Créer le déploiement Flink
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
template: flink-deployment.yml.j2
|
||||||
|
vars:
|
||||||
|
flink_namespace: flink
|
||||||
|
flink_replicas: "{{ services.flink.replicas }}"
|
||||||
37
helms/roles/gis/tasks/main.yml
Normal file
37
helms/roles/gis/tasks/main.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
# Role: gis
|
||||||
|
# Déploie MapStore, GeoServer et FROST
|
||||||
|
|
||||||
|
- name: Installer MapStore
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: mapstore
|
||||||
|
chart_ref: "{{ helm_charts.mapstore.chart }}"
|
||||||
|
release_namespace: mapstore
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
persistence:
|
||||||
|
size: "{{ storage_sizes.mapstore }}"
|
||||||
|
resources: "{{ services.mapstore.resources }}"
|
||||||
|
|
||||||
|
- name: Installer GeoServer
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: geoserver
|
||||||
|
chart_ref: "{{ helm_charts.geoserver.chart }}"
|
||||||
|
release_namespace: geoserver
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
persistence:
|
||||||
|
geodataDir:
|
||||||
|
storageClass: "{{ storage_class }}"
|
||||||
|
size: "{{ storage_sizes.geoserver }}"
|
||||||
|
resources: "{{ services.geoserver.resources }}"
|
||||||
|
|
||||||
|
- name: Installer FROST
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: frost
|
||||||
|
chart_ref: "{{ helm_charts.frost.chart }}"
|
||||||
|
release_namespace: iot
|
||||||
|
values:
|
||||||
|
persistence:
|
||||||
|
size: 10Gi
|
||||||
|
resources: "{{ services.frost.resources }}"
|
||||||
28
helms/roles/gitea/tasks/main.yml
Normal file
28
helms/roles/gitea/tasks/main.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
# Role: gitea
|
||||||
|
# Déploie Gitea
|
||||||
|
|
||||||
|
- name: Installer Gitea
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: gitea
|
||||||
|
chart_ref: "{{ helm_charts.gitea.chart }}"
|
||||||
|
release_namespace: gitea
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
gitea:
|
||||||
|
admin:
|
||||||
|
username: eric
|
||||||
|
password: "{{ vault_gitea_admin_password }}"
|
||||||
|
email: admin@digitribe.fr
|
||||||
|
config:
|
||||||
|
server:
|
||||||
|
DOMAIN: gitea.digitribe.fr
|
||||||
|
ROOT_URL: https://gitea.digitribe.fr
|
||||||
|
SSH_DOMAIN: gitea.digitribe.fr
|
||||||
|
SSH_PORT: 22
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: "{{ storage_sizes.gitea }}"
|
||||||
|
postgresql:
|
||||||
|
enabled: true
|
||||||
|
resources: "{{ services.gitea.resources }}"
|
||||||
35
helms/roles/iot/tasks/main.yml
Normal file
35
helms/roles/iot/tasks/main.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
# Role: iot
|
||||||
|
# Déploie les brokers MQTT
|
||||||
|
|
||||||
|
|
||||||
|
- name: Installer EMQX
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: emqx
|
||||||
|
chart_ref: "{{ helm_charts.emqx.chart }}"
|
||||||
|
release_namespace: iot
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
replicaCount: "{{ services.emqx.replicas }}"
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: "{{ storage_sizes.emqx }}"
|
||||||
|
resources: "{{ services.emqx.resources }}"
|
||||||
|
|
||||||
|
- name: Installer Mosquitto
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: mosquitto
|
||||||
|
chart_ref: "{{ helm_charts.mosquitto.chart }}"
|
||||||
|
release_namespace: iot
|
||||||
|
values:
|
||||||
|
replicaCount: "{{ services.mosquitto.replicas }}"
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: "{{ storage_sizes.mosquitto }}"
|
||||||
|
resources: "{{ services.mosquitto.resources }}"
|
||||||
|
config: |
|
||||||
|
listener 1883
|
||||||
|
allow_anonymous false
|
||||||
|
password_file /etc/mosquitto/passwd
|
||||||
|
auth:
|
||||||
|
password: "{{ vault_mosquitto_password }}"
|
||||||
31
helms/roles/jupyterhub/tasks/main.yml
Normal file
31
helms/roles/jupyterhub/tasks/main.yml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
# Role: jupyterhub
|
||||||
|
# Déploie JupyterHub
|
||||||
|
|
||||||
|
- name: Installer JupyterHub
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: hub
|
||||||
|
chart_ref: "{{ helm_charts.jupyterhub.chart }}"
|
||||||
|
release_namespace: jupyterhub
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
hub:
|
||||||
|
config:
|
||||||
|
Authenticator:
|
||||||
|
admin_users:
|
||||||
|
- eric
|
||||||
|
JupyterHub:
|
||||||
|
admin_access: true
|
||||||
|
db:
|
||||||
|
pvc:
|
||||||
|
storage: "{{ storage_sizes.jupyterhub }}"
|
||||||
|
singleuser:
|
||||||
|
storage:
|
||||||
|
capacity: "{{ storage_sizes.jupyterhub }}"
|
||||||
|
dynamic:
|
||||||
|
pvcNameTemplate: "jupyterhub-{userid}"
|
||||||
|
volumeNameTemplate: "jupyterhub-{userid}"
|
||||||
|
storageClass: "{{ storage_class }}"
|
||||||
|
proxy:
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
19
helms/roles/kafka/tasks/main.yml
Normal file
19
helms/roles/kafka/tasks/main.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
# Role: kafka
|
||||||
|
# Déploie Kafka via l'opérateur Strimzi
|
||||||
|
|
||||||
|
- name: Installer l'opérateur Strimzi
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: strimzi-kafka-operator
|
||||||
|
chart_ref: "{{ helm_charts.kafka.chart }}"
|
||||||
|
release_namespace: kafka
|
||||||
|
create_namespace: true
|
||||||
|
|
||||||
|
- name: Créer le cluster Kafka
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
template: kafka-cluster.yml.j2
|
||||||
|
vars:
|
||||||
|
kafka_namespace: kafka
|
||||||
|
kafka_replicas: "{{ services.kafka.replicas }}"
|
||||||
|
kafka_storage_size: "{{ storage_sizes.kafka }}"
|
||||||
18
helms/roles/mindsdb/tasks/main.yml
Normal file
18
helms/roles/mindsdb/tasks/main.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
# Role: mindsdb
|
||||||
|
# Déploie MindsDB
|
||||||
|
|
||||||
|
- name: Installer MindsDB
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: mindsdb
|
||||||
|
chart_ref: "{{ helm_charts.mindsdb.chart }}"
|
||||||
|
release_namespace: mindsdb
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
mindsdb:
|
||||||
|
auth:
|
||||||
|
username: admin
|
||||||
|
password: "{{ vault_mindsdb_password }}"
|
||||||
|
storage:
|
||||||
|
size: "{{ storage_sizes.mindsdb }}"
|
||||||
|
resources: "{{ services.mindsdb.resources }}"
|
||||||
41
helms/roles/monitoring/tasks/main.yml
Normal file
41
helms/roles/monitoring/tasks/main.yml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
# Role: monitoring
|
||||||
|
# Déploie Prometheus, Grafana, Loki et Promtail
|
||||||
|
|
||||||
|
- name: Installer kube-prometheus-stack
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: prometheus
|
||||||
|
chart_ref: "{{ helm_charts.prometheus.chart }}"
|
||||||
|
release_namespace: monitoring
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
prometheus:
|
||||||
|
prometheusSpec:
|
||||||
|
retention: "{{ monitoring.prometheus_retention }}"
|
||||||
|
storageSpec:
|
||||||
|
volumeClaimTemplate:
|
||||||
|
spec:
|
||||||
|
storageClassName: "{{ storage_class }}"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: "{{ storage_sizes.prometheus }}"
|
||||||
|
grafana:
|
||||||
|
adminPassword: "{{ monitoring.grafana_admin_password }}"
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: "{{ storage_sizes.grafana }}"
|
||||||
|
alertmanager:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
- name: Installer Loki Stack
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: loki
|
||||||
|
chart_ref: "{{ helm_charts.loki.chart }}"
|
||||||
|
release_namespace: monitoring
|
||||||
|
values:
|
||||||
|
loki:
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: "{{ storage_sizes.loki }}"
|
||||||
|
promtail:
|
||||||
|
enabled: true
|
||||||
17
helms/roles/namespaces/tasks/main.yml
Normal file
17
helms/roles/namespaces/tasks/main.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
# Role: namespaces
|
||||||
|
# Crée les namespaces Kubernetes
|
||||||
|
|
||||||
|
- name: Créer les namespaces
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ item }}"
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/managed-by: ansible
|
||||||
|
cluster: "{{ cluster_name }}"
|
||||||
|
type: kubernetes.io/metadata.v1
|
||||||
|
loop: "{{ namespaces }}"
|
||||||
26
helms/roles/nodered/tasks/main.yml
Normal file
26
helms/roles/nodered/tasks/main.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
# Role: nodered
|
||||||
|
# Déploie Node-RED
|
||||||
|
|
||||||
|
- name: Installer Node-RED
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: nodered
|
||||||
|
chart_ref: "{{ helm_charts.nodered.chart }}"
|
||||||
|
release_namespace: iot
|
||||||
|
values:
|
||||||
|
replicaCount: "{{ services.nodered.replicas }}"
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: 5Gi
|
||||||
|
resources: "{{ services.nodered.resources }}"
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- host: nodered.digitribe.fr
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: nodered-tls
|
||||||
|
hosts:
|
||||||
|
- nodered.digitribe.fr
|
||||||
22
helms/roles/odk/tasks/main.yml
Normal file
22
helms/roles/odk/tasks/main.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
# Role: odk
|
||||||
|
# Déploie ODK Central
|
||||||
|
|
||||||
|
- name: Installer ODK Central
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: odk-central
|
||||||
|
chart_ref: "{{ helm_charts.odk.chart }}"
|
||||||
|
release_namespace: odk
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
backend:
|
||||||
|
replicaCount: 1
|
||||||
|
resources: "{{ services.odk.resources }}"
|
||||||
|
frontend:
|
||||||
|
replicaCount: 1
|
||||||
|
resources: "{{ services.odk.resources }}"
|
||||||
|
postgres:
|
||||||
|
enabled: true
|
||||||
|
storage: "{{ storage_sizes.odk }}"
|
||||||
|
redis:
|
||||||
|
enabled: true
|
||||||
27
helms/roles/phpipam/tasks/main.yml
Normal file
27
helms/roles/phpipam/tasks/main.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
# Role: phpipam
|
||||||
|
# Déploie phpIPAM
|
||||||
|
|
||||||
|
- name: Installer phpIPAM
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: phpipam
|
||||||
|
chart_ref: "{{ helm_charts.phpipam.chart }}"
|
||||||
|
release_namespace: phpipam
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
phpipam:
|
||||||
|
adminPassword: "{{ vault_phpipam_admin_password }}"
|
||||||
|
persistence:
|
||||||
|
size: 5Gi
|
||||||
|
resources: "{{ services.phpipam.resources }}"
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- host: phpipam.digitribe.fr
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: phpipam-tls
|
||||||
|
hosts:
|
||||||
|
- phpipam.digitribe.fr
|
||||||
21
helms/roles/prerequisites/tasks/main.yml
Normal file
21
helms/roles/prerequisites/tasks/main.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
# Role: prerequisites
|
||||||
|
# Installe les prérequis sur le cluster Kubernetes
|
||||||
|
|
||||||
|
- name: Ajouter les repositories Helm
|
||||||
|
kubernetes.core.helm_repository:
|
||||||
|
name: "{{ item.name }}"
|
||||||
|
repo_url: "{{ item.url }}"
|
||||||
|
loop: "{{ helm_repos }}"
|
||||||
|
|
||||||
|
- name: Mettre à jour les repositories Helm
|
||||||
|
command: helm repo update
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Installer les CRDs nécessaires
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
src: "{{ item }}"
|
||||||
|
loop:
|
||||||
|
- https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.crds.yaml
|
||||||
|
ignore_errors: true
|
||||||
19
helms/roles/smartapp/tasks/main.yml
Normal file
19
helms/roles/smartapp/tasks/main.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
# Role: smartapp
|
||||||
|
# Déploie l'application Smart City
|
||||||
|
|
||||||
|
- name: Déployer Smart App Web
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
template: smartapp-web.yml.j2
|
||||||
|
vars:
|
||||||
|
smartapp_namespace: smartapp
|
||||||
|
smartapp_domain: smartapp.digitribe.fr
|
||||||
|
|
||||||
|
- name: Déployer Smart App API
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
template: smartapp-api.yml.j2
|
||||||
|
vars:
|
||||||
|
smartapp_namespace: smartapp
|
||||||
|
smartapp_domain: api-smartapp.digitribe.fr
|
||||||
38
helms/roles/starrocks/tasks/main.yml
Normal file
38
helms/roles/starrocks/tasks/main.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
# Role: starrocks
|
||||||
|
# Déploie StarRocks
|
||||||
|
|
||||||
|
- name: Ajouter le repository Helm StarRocks
|
||||||
|
kubernetes.core.helm_repository:
|
||||||
|
name: starrocks
|
||||||
|
repo_url: https://starrocks.github.io/starrocks-kubernetes-operator
|
||||||
|
|
||||||
|
- name: Installer StarRocks
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: starrocks
|
||||||
|
chart_ref: "{{ helm_charts.starrocks.chart }}"
|
||||||
|
chart_version: "{{ helm_charts.starrocks.version }}"
|
||||||
|
release_namespace: starrocks
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
fe:
|
||||||
|
replicas: "{{ services.starrocks.replicas }}"
|
||||||
|
resources: "{{ services.starrocks.resources }}"
|
||||||
|
storage:
|
||||||
|
size: "{{ storage_sizes.starrocks | default('50Gi') }}"
|
||||||
|
storageClass: "{{ storage_class }}"
|
||||||
|
be:
|
||||||
|
replicas: 3
|
||||||
|
resources: "{{ services.starrocks.resources }}"
|
||||||
|
storage:
|
||||||
|
size: "{{ storage_sizes.starrocks | default('100Gi') }}"
|
||||||
|
storageClass: "{{ storage_class }}"
|
||||||
|
cn:
|
||||||
|
replicas: 2
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "250m"
|
||||||
|
memory: "512Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "2Gi"
|
||||||
27
helms/roles/storage/tasks/main.yml
Normal file
27
helms/roles/storage/tasks/main.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
# Role: storage
|
||||||
|
# Configure le stockage NFS et les StorageClasses
|
||||||
|
|
||||||
|
- name: Créer le namespace storage
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: storage
|
||||||
|
|
||||||
|
- name: Installer le NFS provisioner
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: nfs-provisioner
|
||||||
|
chart_ref: "{{ helm_charts.nfs_provisioner.chart }}"
|
||||||
|
chart_version: "{{ helm_charts.nfs_provisioner.version }}"
|
||||||
|
release_namespace: storage
|
||||||
|
values:
|
||||||
|
nfs:
|
||||||
|
server: "{{ nfs_server }}"
|
||||||
|
path: "{{ nfs_path }}"
|
||||||
|
storageClass:
|
||||||
|
name: "{{ storage_class }}"
|
||||||
|
defaultClass: true
|
||||||
|
reclaimPolicy: Retain
|
||||||
30
helms/roles/streamlit/tasks/main.yml
Normal file
30
helms/roles/streamlit/tasks/main.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
# Role: streamlit
|
||||||
|
# Déploie Streamlit
|
||||||
|
|
||||||
|
- name: Installer Streamlit
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: streamlit
|
||||||
|
chart_ref: "{{ helm_charts.streamlit.chart }}"
|
||||||
|
chart_version: "{{ helm_charts.streamlit.version }}"
|
||||||
|
release_namespace: streamlit
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
replicaCount: "{{ services.streamlit.replicas }}"
|
||||||
|
resources: "{{ services.streamlit.resources }}"
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- host: streamlit.digitribe.fr
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: streamlit-tls
|
||||||
|
hosts:
|
||||||
|
- streamlit.digitribe.fr
|
||||||
|
env:
|
||||||
|
STREAMLIT_SERVER_HEADLESS: "true"
|
||||||
|
STREAMLIT_SERVER_ENABLE_CORS: "false"
|
||||||
49
helms/roles/traefik/tasks/main.yml
Normal file
49
helms/roles/traefik/tasks/main.yml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
# Role: traefik
|
||||||
|
# Déploie le reverse proxy Traefik
|
||||||
|
|
||||||
|
- name: Créer le namespace traefik
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: present
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ traefik_namespace }}"
|
||||||
|
|
||||||
|
- name: Installer Traefik
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: traefik
|
||||||
|
chart_ref: helm_charts.traefik.chart
|
||||||
|
release_namespace: "{{ traefik_namespace }}"
|
||||||
|
values:
|
||||||
|
globalArguments:
|
||||||
|
- "--global.checknewversion=false"
|
||||||
|
- "--global.sendanonymoususage=false"
|
||||||
|
additionalArguments:
|
||||||
|
- "--providers.kubernetescrd.allowexternalnameservices=true"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.email={{ acme_email }}"
|
||||||
|
- "--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json"
|
||||||
|
ports:
|
||||||
|
traefik:
|
||||||
|
port: 9000
|
||||||
|
expose: false
|
||||||
|
web:
|
||||||
|
port: 80
|
||||||
|
expose: true
|
||||||
|
websecure:
|
||||||
|
port: 443
|
||||||
|
expose: true
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
size: 1Gi
|
||||||
|
service:
|
||||||
|
type: LoadBalancer
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "100m"
|
||||||
|
memory: "128Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "1000m"
|
||||||
|
memory: "512Mi"
|
||||||
46
helms/roles/trino/tasks/main.yml
Normal file
46
helms/roles/trino/tasks/main.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
# Role: trino
|
||||||
|
# Déploie Trino
|
||||||
|
|
||||||
|
- name: Installer Trino
|
||||||
|
kubernetes.core.helm:
|
||||||
|
name: trino
|
||||||
|
chart_ref: "{{ helm_charts.trino.chart }}"
|
||||||
|
chart_version: "{{ helm_charts.trino.version }}"
|
||||||
|
release_namespace: trino
|
||||||
|
create_namespace: true
|
||||||
|
values:
|
||||||
|
server:
|
||||||
|
workers: "{{ services.trino.replicas }}"
|
||||||
|
resources: "{{ services.trino.resources }}"
|
||||||
|
coordinator:
|
||||||
|
resources: "{{ services.trino.resources }}"
|
||||||
|
worker:
|
||||||
|
resources: "{{ services.trino.resources }}"
|
||||||
|
service:
|
||||||
|
type: ClusterIV
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
hosts:
|
||||||
|
- host: trino.digitribe.fr
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls:
|
||||||
|
- secretName: trino-tls
|
||||||
|
hosts:
|
||||||
|
- trino.digitribe.fr
|
||||||
|
catalog:
|
||||||
|
postgresql:
|
||||||
|
connector.name=postgresql
|
||||||
|
connection-url=jdbc:postgresql://postgresql-ha-pgpool.default.svc.cluster.local:5432/smartcity
|
||||||
|
connection-user=trino
|
||||||
|
connection-password={{ vault_trino_db_password }}
|
||||||
|
clickhouse:
|
||||||
|
connector.name=clickhouse
|
||||||
|
connection-url=jdbc:clickhouse://clickhouse.clickhouse.svc.cluster.local:8123/default
|
||||||
|
connection-user=default
|
||||||
|
connection-password={{ vault_clickhouse_password }}
|
||||||
|
delta:
|
||||||
|
connector.name=delta_lake
|
||||||
|
connection-url=jdbc:delta://deltalake.deltalake.svc.cluster.local:9083
|
||||||
56
helms/undeploy.yml
Normal file
56
helms/undeploy.yml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
# Playbook de suppression de la stack
|
||||||
|
# Fichier: undeploy.yml
|
||||||
|
|
||||||
|
- name: Suppression Smart City Martinique de Kubernetes
|
||||||
|
hosts: localhost
|
||||||
|
connection: local
|
||||||
|
gather_facts: false
|
||||||
|
|
||||||
|
vars_files:
|
||||||
|
- group_vars/all.yml
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Supprimer les namespaces Kubernetes
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: absent
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: "{{ item }}"
|
||||||
|
loop: "{{ namespaces }}"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Supprimer les PersistentVolumes
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: absent
|
||||||
|
definition:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolume
|
||||||
|
metadata:
|
||||||
|
name: "{{ item }}"
|
||||||
|
loop: "{{ persistent_volumes | default([]) }}"
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Supprimer les ClusterRoles
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: absent
|
||||||
|
kind: ClusterRole
|
||||||
|
name: "{{ item }}"
|
||||||
|
loop:
|
||||||
|
- traefik
|
||||||
|
- cert-manager
|
||||||
|
- prometheus
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Supprimer les ClusterRoleBindings
|
||||||
|
kubernetes.core.k8s:
|
||||||
|
state: absent
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
name: "{{ item }}"
|
||||||
|
loop:
|
||||||
|
- traefik
|
||||||
|
- cert-manager
|
||||||
|
- prometheus
|
||||||
|
ignore_errors: true
|
||||||
@@ -1,61 +1,183 @@
|
|||||||
# Smart App City — Mobile Application
|
# Smart App City — Application Mobile
|
||||||
|
|
||||||
> Multi-platform mobile application for Smart City Digital Twin Martinique
|
> Application mobile multi-platforme pour le Smart City Digital Twin Martinique
|
||||||
|
|
||||||
## Quick Start
|
## 📱 Présentation
|
||||||
|
|
||||||
### Prerequisites
|
Smart App City est une application mobile (React Native + Expo) qui permet aux citoyens de :
|
||||||
|
- **Visualiser les données IoT** en temps réel (capteurs de température, humidité, qualité de l'air, bruit, trafic, énergie)
|
||||||
|
- **Accéder à une marketplace** de services et produits locaux
|
||||||
|
- **Interagir avec un assistant IA** (météo, transports, énergie, alertes)
|
||||||
|
- **Gérer ses notifications** et **profil utilisateur**
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
smart-app-city/
|
||||||
|
├── frontend/ # Application React Native + Expo (TypeScript)
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── screens/ # 15 écrans (Auth, Dashboard, Map, Marketplace, Chat, Profile, IoT, Notifications)
|
||||||
|
│ │ ├── components/ # Composants réutilisables (Card, Button, Charts, Maps)
|
||||||
|
│ │ ├── stores/ # State management (Zustand) — authStore, iotStore, notificationStore, uiStore
|
||||||
|
│ │ ├── hooks/ # Hooks personnalisés (useSensors, useAlerts, useNotifications, useLocation)
|
||||||
|
│ │ ├── services/ # Services API (auth, iot, notification, via Axios)
|
||||||
|
│ │ ├── i18n/ # Internationalisation (FR/EN/ES/DE)
|
||||||
|
│ │ ├── theme/ # Design system (Blue Ocean palette)
|
||||||
|
│ │ └── utils/ # Utilitaires (formatters, validators, constants)
|
||||||
|
│ ├── dist/ # Build web statique (nginx)
|
||||||
|
│ ├── Dockerfile # Multi-stage build (node + nginx)
|
||||||
|
│ ├── package.json # Dépendances (Expo 51, React Native 0.74)
|
||||||
|
│ └── app.json # Configuration Expo
|
||||||
|
├── backend/ # Backend microservices (Node.js)
|
||||||
|
├── ai/ # Services IA (RAG, Agent)
|
||||||
|
├── design/ # Maquettes HTML/CSS (28 screenshots, PDF, Figma JSON, Penpot SVG)
|
||||||
|
├── docs/ # Documentation
|
||||||
|
└── docker-compose.yml # Orchestration Docker
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Développement
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
- Node.js 20+
|
- Node.js 20+
|
||||||
- Expo CLI : `npm install -g expo-cli`
|
- Expo CLI : `npm install -g expo-cli`
|
||||||
- Docker & Docker Compose
|
- Docker & Docker Compose (pour le déploiement)
|
||||||
|
|
||||||
### Development
|
### Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install dependencies
|
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm install --legacy-peer-deps
|
||||||
|
|
||||||
# Start development server
|
|
||||||
npx expo start
|
|
||||||
|
|
||||||
# Run on device
|
|
||||||
# Scan QR code with Expo Go app
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backend
|
### Lancement en mode développement
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start all services
|
# Mode natif (iOS/Android)
|
||||||
cd ..
|
npm start
|
||||||
docker-compose up -d
|
# Scanner le QR code avec l'app Expo Go
|
||||||
|
|
||||||
# Start individual service
|
# Mode web
|
||||||
cd backend/auth-service
|
npm run web
|
||||||
npm run start:dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### AI Services
|
### Build web statique
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start RAG service
|
# Sans Docker
|
||||||
cd ai/rag-service
|
cd frontend
|
||||||
pip install -r requirements.txt
|
npx expo export --platform web --output-dir dist
|
||||||
uvicorn main:app --reload --port 8001
|
|
||||||
|
|
||||||
# Start Agent service
|
# Avec Docker
|
||||||
cd ai/agent-service
|
docker build -t smartapp-web:latest .
|
||||||
pip install -r requirements.txt
|
|
||||||
uvicorn main:app --reload --port 8002
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
### Déploiement
|
||||||
|
|
||||||
- [Architecture](docs/ARCHITECTURE.md)
|
```bash
|
||||||
- [Beckn Integration](docs/BECKN_INTEGRATION.md)
|
cd smart-app-city
|
||||||
- [AI Architecture](docs/AI_ARCHITECTURE.md)
|
docker compose -f docker-compose.web.yml up -d
|
||||||
- [Internationalization](docs/I18N.md)
|
```
|
||||||
|
|
||||||
## License
|
L'application est accessible sur : **https://smartapp.digitribe.fr**
|
||||||
|
|
||||||
|
## 🎨 Design System
|
||||||
|
|
||||||
|
### Palette principale
|
||||||
|
- **Blue Ocean** : `#1565C0` (primaire)
|
||||||
|
- **Indigo** : `#3949AB` (accent)
|
||||||
|
- **Cyan** : `#00ACC1` (secondaire)
|
||||||
|
- **Deep Ocean** : `#0D1B2A` (dark mode)
|
||||||
|
|
||||||
|
### Composants
|
||||||
|
Tous les composants UI sont dans `src/components/` :
|
||||||
|
- `common/Card.tsx` — Carte réutilisable (3 variants : default, elevated, outlined)
|
||||||
|
- `common/Button.tsx` — Bouton (5 variants, 3 tailles)
|
||||||
|
- `common/Header.tsx` — En-tête de section
|
||||||
|
- `common/LoadingSpinner.tsx` — Indicateur de chargement
|
||||||
|
- `common/ErrorBoundary.tsx` — Gestion d'erreurs React
|
||||||
|
- `cards/SensorCard.tsx` — Carte capteur IoT
|
||||||
|
- `cards/StatsCard.tsx` — Carte statistiques
|
||||||
|
- `cards/AlertCard.tsx` — Carte alerte
|
||||||
|
- `cards/ZoneCard.tsx` — Carte zone
|
||||||
|
- `charts/LineChart.tsx` — Graphique linéaire
|
||||||
|
- `charts/BarChart.tsx` — Graphique barres
|
||||||
|
- `charts/GaugeChart.tsx` — Jauge semi-circulaire
|
||||||
|
- `maps/MapView.tsx` — Vue carte (placeholder pour react-native-maps)
|
||||||
|
- `maps/MarkerPopup.tsx` — Popup marqueur
|
||||||
|
|
||||||
|
## 📊 State Management (Zustand)
|
||||||
|
|
||||||
|
### Stores
|
||||||
|
- **authStore** : Authentification utilisateur (login, register, logout, refresh token)
|
||||||
|
- **iotStore** : Données IoT (6 capteurs, 3 zones, 2 alertes mock)
|
||||||
|
- **notificationStore** : Notifications (5 mock notifications)
|
||||||
|
- **uiStore** : Thème + i18n (FR/EN/ES/DE)
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
- **useSensors()** : Liste des capteurs, filtrage par type/zone
|
||||||
|
- **useAlerts()** : Alertes actives/critiques, acknowledge
|
||||||
|
- **useNotifications()** : CRUD notifications
|
||||||
|
- **useLocation()** : GPS avec expo-location (défaut : Fort-de-France)
|
||||||
|
|
||||||
|
## 🌐 Internationalisation
|
||||||
|
|
||||||
|
4 langues supportées : Français (défaut), English, Español, Deutsch.
|
||||||
|
|
||||||
|
Fichier de traductions : `src/stores/uiStore.ts` (fonction `t(key, lang)`)
|
||||||
|
|
||||||
|
## 🔌 API Backend
|
||||||
|
|
||||||
|
L'application communique avec l'API Gateway :
|
||||||
|
- **Base URL** : `https://api-smartapp.digitribe.fr/api/v1`
|
||||||
|
- **Authentification** : JWT (Basic Auth pour les routes /admin)
|
||||||
|
- **Endpoints principaux** :
|
||||||
|
- `POST /api/auth/login` — Connexion
|
||||||
|
- `POST /api/auth/register` — Inscription
|
||||||
|
- `POST /api/auth/refresh` — Refresh token
|
||||||
|
- `GET /sensors` — Liste des capteurs
|
||||||
|
- `GET /zones` — Liste des zones
|
||||||
|
- `GET /alerts` — Liste des alertes
|
||||||
|
|
||||||
|
## 📦 Déploiement CI/CD
|
||||||
|
|
||||||
|
### Gitea Actions
|
||||||
|
Fichier : `.gitea/workflows/build-and-deploy.yml`
|
||||||
|
|
||||||
|
Pipeline :
|
||||||
|
1. **Lint** : `tsc --noEmit` (TypeScript check)
|
||||||
|
2. **Build** : `npx expo export --platform web`
|
||||||
|
3. **Deploy** : SSH vers le serveur → copie les fichiers ou rebuild Docker
|
||||||
|
|
||||||
|
### Secrets Gitea
|
||||||
|
- `SERVER_HOST` : `localhost`
|
||||||
|
- `SERVER_USER` : `eric`
|
||||||
|
- `SSH_PRIVATE_KEY` : Clé SSH pour le déploiement
|
||||||
|
|
||||||
|
### Dockerfiles
|
||||||
|
- `frontend/Dockerfile` : Multi-stage build (node 20 alpine → nginx alpine)
|
||||||
|
- `docker-compose.web.yml` : Service nginx statique avec Traefik
|
||||||
|
|
||||||
|
## 🌍 Services liés
|
||||||
|
|
||||||
|
| Service | URL | Description |
|
||||||
|
|---------|-----|-------------|
|
||||||
|
| Gitea | https://gitea.digitribe.fr | Git + CI/CD |
|
||||||
|
| Honcho | https://honcho.digitribe.fr | Mémoire agent IA |
|
||||||
|
| Grafana | http://127.0.0.1:3088 | Métriques Prometheus |
|
||||||
|
|
||||||
|
## 📝 Notes techniques importantes
|
||||||
|
|
||||||
|
### WatermelonDB supprimé
|
||||||
|
Le package `@nozbe/watermelonDB` a été supprimé car la version `^0.27.0` n'existe plus. Pas de remplacement nécessaire — la mémoire locale utilise Zustand + AsyncStorage.
|
||||||
|
|
||||||
|
### Build web Expo
|
||||||
|
- Le build `npx expo export:web` nécessite `@expo/webpack-config@~19.0.1`
|
||||||
|
- Si le build échoue avec "Lock compromised" : supprimer `package-lock.json` et `node_modules/`, puis `npm install --legacy-peer-deps`
|
||||||
|
- Le build Docker est plus fiable que le build local (environnement isolé)
|
||||||
|
|
||||||
|
### Patch tool
|
||||||
|
Les fichiers contenant `//`, `=`, `+`, `{}` dans les strings peuvent corriger le tool `patch`. Utiliser `replace_all=true` ou échapper les caractères spéciaux.
|
||||||
|
|
||||||
|
## 📄 Licence
|
||||||
|
|
||||||
Smart City Digital Twin Martinique — Projet public
|
Smart City Digital Twin Martinique — Projet public
|
||||||
|
|||||||
6
smart-app-city/backend/auth-service/Dockerfile
Normal file
6
smart-app-city/backend/auth-service/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
COPY server.js .
|
||||||
|
RUN npm init -y && npm install express bcryptjs jsonwebtoken cors
|
||||||
|
EXPOSE 3001
|
||||||
|
CMD ["node", "server.js"]
|
||||||
175
smart-app-city/backend/auth-service/server.js
Normal file
175
smart-app-city/backend/auth-service/server.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// Smart App City — Auth Service (Node.js + Express)
|
||||||
|
const express = require('express');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const cors = require('cors');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3001;
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'smart-app-city-jwt-secret-2024';
|
||||||
|
const DB_PATH = path.join(__dirname, 'users.json');
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// ── Database helpers ──
|
||||||
|
function loadUsers() {
|
||||||
|
if (!fs.existsSync(DB_PATH)) {
|
||||||
|
fs.writeFileSync(DB_PATH, JSON.stringify({ users: [] }, null, 2));
|
||||||
|
}
|
||||||
|
return JSON.parse(fs.readFileSync(DB_PATH, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUsers(db) {
|
||||||
|
fs.writeFileSync(DB_PATH, JSON.stringify(db, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Create admin user on startup ──
|
||||||
|
function createAdminUser() {
|
||||||
|
const db = loadUsers();
|
||||||
|
|
||||||
|
// Admin user
|
||||||
|
if (!db.users.find(u => u.email === 'admin@digitribe.fr')) {
|
||||||
|
const hashedPassword = bcrypt.hashSync('Digitribe972', 10);
|
||||||
|
db.users.push({
|
||||||
|
id: 'admin-001',
|
||||||
|
email: 'admin@digitribe.fr',
|
||||||
|
password: hashedPassword,
|
||||||
|
firstName: 'Admin',
|
||||||
|
lastName: 'Digitribe',
|
||||||
|
roles: ['admin', 'user'],
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
saveUsers(db);
|
||||||
|
console.log('✅ Admin user created: admin@digitribe.fr / Digitribe972');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Admin user already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eric user
|
||||||
|
if (!db.users.find(u => u.email === 'eric@digitribe.fr')) {
|
||||||
|
const hashedPassword = bcrypt.hashSync('Digitribe972', 10);
|
||||||
|
db.users.push({
|
||||||
|
id: 'eric-001',
|
||||||
|
email: 'eric@digitribe.fr',
|
||||||
|
password: hashedPassword,
|
||||||
|
firstName: 'Eric',
|
||||||
|
lastName: 'Felixine',
|
||||||
|
roles: ['admin', 'user'],
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
saveUsers(db);
|
||||||
|
console.log('✅ Eric user created: eric@digitribe.fr / Digitribe972');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Eric user already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erol user
|
||||||
|
if (!db.users.find(u => u.email === 'erol@digitribe.fr')) {
|
||||||
|
const hashedPassword = bcrypt.hashSync('erol', 10);
|
||||||
|
db.users.push({
|
||||||
|
id: 'erol-001',
|
||||||
|
email: 'erol@digitribe.fr',
|
||||||
|
password: hashedPassword,
|
||||||
|
firstName: 'Erol',
|
||||||
|
lastName: 'Digitribe',
|
||||||
|
roles: ['user'],
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
saveUsers(db);
|
||||||
|
console.log('✅ Erol user created: erol@digitribe.fr / erol');
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ Erol user already exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Routes ──
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'ok', service: 'auth-service' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register
|
||||||
|
app.post('/api/auth/register', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, password, firstName, lastName } = req.body;
|
||||||
|
if (!email || !password || !firstName || !lastName) {
|
||||||
|
return res.status(400).json({ message: 'Tous les champs sont requis' });
|
||||||
|
}
|
||||||
|
if (password.length < 8) {
|
||||||
|
return res.status(400).json({ message: 'Le mot de passe doit contenir au moins 8 caractères' });
|
||||||
|
}
|
||||||
|
const db = loadUsers();
|
||||||
|
if (db.users.find(u => u.email === email)) {
|
||||||
|
return res.status(409).json({ message: 'Un compte avec cet email existe déjà' });
|
||||||
|
}
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
const user = {
|
||||||
|
id: 'user-' + Date.now(),
|
||||||
|
email,
|
||||||
|
password: hashedPassword,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
roles: ['user'],
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
db.users.push(user);
|
||||||
|
saveUsers(db);
|
||||||
|
const token = jwt.sign({ id: user.id, email: user.email, roles: user.roles }, JWT_SECRET, { expiresIn: '7d' });
|
||||||
|
res.json({ accessToken: token, user: { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, roles: user.roles } });
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ message: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Login
|
||||||
|
app.post('/api/auth/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
if (!email || !password) {
|
||||||
|
return res.status(400).json({ message: 'Email et mot de passe requis' });
|
||||||
|
}
|
||||||
|
const db = loadUsers();
|
||||||
|
const user = db.users.find(u => u.email === email);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({ message: 'Identifiants incorrects' });
|
||||||
|
}
|
||||||
|
const valid = await bcrypt.compare(password, user.password);
|
||||||
|
if (!valid) {
|
||||||
|
return res.status(401).json({ message: 'Identifiants incorrects' });
|
||||||
|
}
|
||||||
|
const token = jwt.sign({ id: user.id, email: user.email, roles: user.roles }, JWT_SECRET, { expiresIn: '7d' });
|
||||||
|
res.json({ accessToken: token, user: { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, roles: user.roles } });
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ message: 'Erreur serveur' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get current user
|
||||||
|
app.get('/api/auth/me', (req, res) => {
|
||||||
|
try {
|
||||||
|
const token = req.headers.authorization?.split(' ')[1];
|
||||||
|
if (!token) return res.status(401).json({ message: 'Non authentifié' });
|
||||||
|
const decoded = jwt.verify(token, JWT_SECRET);
|
||||||
|
const db = loadUsers();
|
||||||
|
const user = db.users.find(u => u.id === decoded.id);
|
||||||
|
if (!user) return res.status(404).json({ message: 'Utilisateur non trouvé' });
|
||||||
|
res.json({ id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, roles: user.roles });
|
||||||
|
} catch (e) {
|
||||||
|
res.status(401).json({ message: 'Token invalide' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Logout (client-side, but endpoint for completeness)
|
||||||
|
app.post('/api/auth/logout', (req, res) => {
|
||||||
|
res.json({ message: 'Déconnexion réussie' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start server
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Auth service running on port ${PORT}`);
|
||||||
|
createAdminUser();
|
||||||
|
});
|
||||||
35
smart-app-city/docker-compose.web.yml
Normal file
35
smart-app-city/docker-compose.web.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: smartapp-web
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
volumes:
|
||||||
|
- /home/eric/smart-city-digital-twin-martinique/smart-app-city/frontend/dist:/usr/share/nginx/html
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=false"
|
||||||
|
expose:
|
||||||
|
- "80"
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: ./backend/auth-service
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: smartapp-api
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- traefik-public
|
||||||
|
volumes:
|
||||||
|
- /home/eric/smart-city-digital-twin-martinique/smart-app-city/backend/auth-service/users.json:/app/users.json
|
||||||
|
expose:
|
||||||
|
- "3001"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:3001/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
705
smart-app-city/frontend/dist/index.html
vendored
Normal file
705
smart-app-city/frontend/dist/index.html
vendored
Normal file
@@ -0,0 +1,705 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
|
<title>Smart City Martinique</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
||||||
|
:root {
|
||||||
|
--primary: #1565C0; --primary-dark: #0D47A1; --accent: #00ACC1;
|
||||||
|
--bg: #0D1B2A; --bg-card: #1E3350; --bg-input: #1B2838;
|
||||||
|
--text: #E8EAF6; --text-secondary: #9FA8DA; --text-muted: #5C6BC0;
|
||||||
|
--danger: #D32F2F; --success: #2E7D32; --warning: #F57C00;
|
||||||
|
--radius: 16px; --radius-sm: 8px; --radius-full: 9999px;
|
||||||
|
}
|
||||||
|
html, body { height: 100%; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: var(--bg); color: var(--text); min-height: 100%;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Top Nav ── */
|
||||||
|
.top-nav {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: color 0.2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.nav-item.active { color: var(--accent); }
|
||||||
|
.nav-item-icon { font-size: 20px; }
|
||||||
|
.nav-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 4px;
|
||||||
|
background: var(--danger);
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
min-width: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ── */
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||||
|
padding: 30px 20px 50px;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.logo { width: 70px; height: 70px; background: rgba(255,255,255,0.15); border-radius: 18px; margin: 0 auto 12px; display: flex; align-items: center; justify-content: center; font-size: 36px; }
|
||||||
|
.header h1 { font-size: 24px; font-weight: 700; color: white; margin-bottom: 2px; }
|
||||||
|
.header p { font-size: 13px; color: rgba(255,255,255,0.8); }
|
||||||
|
|
||||||
|
/* ── Form ── */
|
||||||
|
.form-container { flex: 1; padding: 20px; max-width: 420px; margin: 0 auto; width: 100%; }
|
||||||
|
.form-title { font-size: 22px; font-weight: 700; margin-bottom: 24px; }
|
||||||
|
.input-group { margin-bottom: 16px; }
|
||||||
|
.input-group label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px; }
|
||||||
|
.input-wrapper { display: flex; align-items: center; background: var(--bg-input); border-radius: var(--radius-sm); border: 1px solid transparent; transition: border 0.2s; }
|
||||||
|
.input-wrapper:focus-within { border-color: var(--accent); }
|
||||||
|
.input-wrapper .icon { padding: 0 12px; font-size: 16px; }
|
||||||
|
.input-wrapper input { flex: 1; background: none; border: none; outline: none; color: var(--text); font-size: 15px; padding: 14px 12px 14px 0; }
|
||||||
|
.input-wrapper input::placeholder { color: var(--text-muted); }
|
||||||
|
.toggle-pw { padding: 0 12px; cursor: pointer; font-size: 16px; }
|
||||||
|
|
||||||
|
/* ── Buttons ── */
|
||||||
|
.btn { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; padding: 14px; border: none; border-radius: var(--radius-sm); font-size: 15px; font-weight: 700; cursor: pointer; transition: opacity 0.2s, transform 0.1s; }
|
||||||
|
.btn:active { transform: scale(0.98); opacity: 0.9; }
|
||||||
|
.btn-primary { background: var(--primary); color: white; }
|
||||||
|
.btn-secondary { background: var(--bg-card); color: var(--text); border: 1px solid var(--primary-dark); }
|
||||||
|
.btn-google { background: #fff; color: #333; }
|
||||||
|
.btn-apple { background: #000; color: #fff; }
|
||||||
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
.btn .btn-icon { font-size: 18px; }
|
||||||
|
|
||||||
|
/* ── Links ── */
|
||||||
|
.link { color: var(--accent); text-decoration: none; font-size: 14px; font-weight: 600; }
|
||||||
|
.link:hover { text-decoration: underline; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.mt-12 { margin-top: 12px; }
|
||||||
|
.mt-24 { margin-top: 24px; }
|
||||||
|
.mb-8 { margin-bottom: 8px; }
|
||||||
|
|
||||||
|
/* ── Divider ── */
|
||||||
|
.divider { display: flex; align-items: center; gap: 16px; margin: 24px 0; }
|
||||||
|
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: var(--bg-card); }
|
||||||
|
.divider span { font-size: 12px; color: var(--text-muted); }
|
||||||
|
|
||||||
|
/* ── Error/Success ── */
|
||||||
|
.alert { padding: 12px; border-radius: var(--radius-sm); font-size: 13px; margin-bottom: 16px; display: none; }
|
||||||
|
.alert.show { display: block; }
|
||||||
|
.alert-error { background: rgba(211,47,47,0.15); color: #EF5350; border: 1px solid rgba(211,47,47,0.3); }
|
||||||
|
.alert-success { background: rgba(46,125,50,0.15); color: #66BB6A; border: 1px solid rgba(46,125,50,0.3); }
|
||||||
|
|
||||||
|
/* ── Checkbox ── */
|
||||||
|
.checkbox { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--text-secondary); cursor: pointer; }
|
||||||
|
.checkbox input { width: 18px; height: 18px; accent-color: var(--accent); }
|
||||||
|
|
||||||
|
/* ── Loading ── */
|
||||||
|
.spinner { width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3); border-top-color: white; border-radius: 50%; animation: spin 0.8s linear infinite; }
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
/* ── Transitions ── */
|
||||||
|
.screen { display: none; min-height: 100vh; flex-direction: column; }
|
||||||
|
.screen.active { display: flex; }
|
||||||
|
|
||||||
|
/* ── Home Dashboard ── */
|
||||||
|
.dashboard-header {
|
||||||
|
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||||
|
padding: 20px 16px 50px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.dashboard-header-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||||
|
.greeting { font-size: 13px; color: rgba(255,255,255,0.85); }
|
||||||
|
.user-name { font-size: 20px; font-weight: 700; color: white; }
|
||||||
|
.notification-btn { position: relative; background: none; border: none; font-size: 24px; cursor: pointer; }
|
||||||
|
.notification-badge { position: absolute; top: -4px; right: -4px; background: var(--danger); color: white; border-radius: 10px; padding: 1px 5px; font-size: 10px; font-weight: 700; }
|
||||||
|
.search-bar { display: flex; align-items: center; background: rgba(255,255,255,0.2); border-radius: var(--radius-full); padding: 10px 16px; gap: 8px; }
|
||||||
|
.search-bar input { flex: 1; background: none; border: none; outline: none; color: white; font-size: 14px; }
|
||||||
|
.search-bar input::placeholder { color: rgba(255,255,255,0.5); }
|
||||||
|
|
||||||
|
/* ── Metrics Row ── */
|
||||||
|
.metrics-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; padding: 0 16px; margin-top: -25px; }
|
||||||
|
.metric-card { background: var(--bg-card); border-radius: var(--radius); padding: 12px 8px; text-align: center; border: 1px solid rgba(255,255,255,0.05); }
|
||||||
|
.metric-value { font-size: 20px; font-weight: 700; }
|
||||||
|
.metric-unit { font-size: 10px; color: var(--text-muted); }
|
||||||
|
.metric-label { font-size: 9px; color: var(--text-muted); margin-top: 2px; }
|
||||||
|
|
||||||
|
/* ── Section ── */
|
||||||
|
.section { padding: 16px; }
|
||||||
|
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||||||
|
.section-title { font-size: 16px; font-weight: 700; }
|
||||||
|
.see-all { font-size: 13px; color: var(--accent); text-decoration: none; }
|
||||||
|
|
||||||
|
/* ── Cards ── */
|
||||||
|
.card { background: var(--bg-card); border-radius: var(--radius); padding: 14px; margin-bottom: 10px; border: 1px solid rgba(255,255,255,0.05); }
|
||||||
|
.card-title { font-size: 14px; font-weight: 700; margin-bottom: 4px; }
|
||||||
|
.card-subtitle { font-size: 12px; color: var(--text-secondary); }
|
||||||
|
.card-row { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; }
|
||||||
|
|
||||||
|
/* ── Quick Actions ── */
|
||||||
|
.quick-actions { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
|
||||||
|
.quick-action { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 12px 8px; background: var(--bg-card); border-radius: var(--radius); text-decoration: none; color: var(--text); }
|
||||||
|
.quick-action-icon { width: 48px; height: 48px; border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 22px; }
|
||||||
|
.quick-action-label { font-size: 10px; color: var(--text-secondary); text-align: center; }
|
||||||
|
|
||||||
|
/* ── Scrollable content ── */
|
||||||
|
.scrollable { flex: 1; overflow-y: auto; }
|
||||||
|
|
||||||
|
/* ── Badge ── */
|
||||||
|
.badge { display: inline-block; padding: 2px 8px; border-radius: var(--radius-full); font-size: 10px; font-weight: 600; cursor: pointer; }
|
||||||
|
.badge-green { background: rgba(46,125,50,0.2); color: #66BB6A; }
|
||||||
|
.badge-orange { background: rgba(245,124,0,0.2); color: #FFA726; }
|
||||||
|
.badge-red { background: rgba(211,47,47,0.2); color: #EF5350; }
|
||||||
|
.badge-blue { background: rgba(21,101,192,0.2); color: #64B5F6; }
|
||||||
|
|
||||||
|
/* ── Chat ── */
|
||||||
|
.chat-message { padding: 10px 14px; border-radius: var(--radius); margin-bottom: 8px; font-size: 14px; line-height: 1.5; max-width: 80%; }
|
||||||
|
.chat-message-bot { background: var(--bg-card); align-self: flex-start; }
|
||||||
|
.chat-message-user { background: var(--primary); align-self: flex-end; margin-left: auto; }
|
||||||
|
.chat-input { display: flex; gap: 8px; padding: 12px 16px; background: var(--bg-card); border-top: 1px solid rgba(255,255,255,0.05); flex-shrink: 0; }
|
||||||
|
.chat-input input { flex: 1; background: var(--bg); border: none; border-radius: var(--radius-full); padding: 10px 16px; color: var(--text); font-size: 14px; outline: none; }
|
||||||
|
.chat-input button { background: var(--primary); border: none; border-radius: 50%; width: 40px; height: 40px; color: white; font-size: 18px; cursor: pointer; }
|
||||||
|
|
||||||
|
/* ── Map ── */
|
||||||
|
.map-placeholder { width: 100%; height: 200px; background: #E8F5E9; border-radius: var(--radius); display: flex; flex-direction: column; align-items: center; justify-content: center; color: #4CAF50; margin-bottom: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════
|
||||||
|
SCREEN: LOGIN
|
||||||
|
═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="screen-login" class="screen active">
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">🏙️</div>
|
||||||
|
<h1>Smart City Martinique</h1>
|
||||||
|
<p>Martinique</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="alert alert-error" id="login-error"></div>
|
||||||
|
<div class="form-title">Connexion</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Email</label>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<span class="icon">📧</span>
|
||||||
|
<input type="email" id="login-email" placeholder="votre@email.com" autocomplete="email">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Mot de passe</label>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<span class="icon">🔒</span>
|
||||||
|
<input type="password" id="login-password" placeholder="••••••••" autocomplete="current-password">
|
||||||
|
<button type="button" class="toggle-pw" onclick="togglePw('login-password')" style="background:none;border:none;cursor:pointer;font-size:16px;padding:0 12px">👁️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-12">
|
||||||
|
<a href="#" class="link" onclick="forgotPassword()">Mot de passe oublié ?</a>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary mt-24" id="btn-login" onclick="handleLogin()">
|
||||||
|
<span>Se connecter</span>
|
||||||
|
</button>
|
||||||
|
<div class="divider"><span>ou</span></div>
|
||||||
|
<div style="display:flex;gap:12px">
|
||||||
|
<button class="btn btn-google" style="flex:1" onclick="socialLogin('google')"><span class="btn-icon">🔵</span> Google</button>
|
||||||
|
<button class="btn btn-apple" style="flex:1" onclick="socialLogin('apple')"><span class="btn-icon">🍎</span> Apple</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-24">
|
||||||
|
<span style="font-size:14px;color:var(--text-secondary)">Pas encore de compte ? </span>
|
||||||
|
<a href="#" class="link" onclick="showScreen('register')">S'inscrire</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════
|
||||||
|
SCREEN: REGISTER
|
||||||
|
═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="screen-register" class="screen">
|
||||||
|
<div class="header" style="padding-bottom:30px">
|
||||||
|
<div style="text-align:left;margin-bottom:16px"><a href="#" class="link" style="color:rgba(255,255,255,0.8)" onclick="showScreen('login')">← Retour</a></div>
|
||||||
|
<h1>Créer un compte</h1>
|
||||||
|
<p>Rejoignez Smart City Martinique</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="alert alert-error" id="register-error"></div>
|
||||||
|
<div class="alert alert-success" id="register-success"></div>
|
||||||
|
<div style="display:flex;gap:12px">
|
||||||
|
<div class="input-group" style="flex:1">
|
||||||
|
<label>Prénom</label>
|
||||||
|
<div class="input-wrapper"><input type="text" id="reg-firstname" placeholder="Jean"></div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group" style="flex:1">
|
||||||
|
<label>Nom</label>
|
||||||
|
<div class="input-wrapper"><input type="text" id="reg-lastname" placeholder="Dupont"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Email</label>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<span class="icon">📧</span>
|
||||||
|
<input type="email" id="reg-email" placeholder="votre@email.com">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Mot de passe</label>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<span class="icon">🔒</span>
|
||||||
|
<input type="password" id="reg-password" placeholder="Min. 8 caractères">
|
||||||
|
<button type="button" class="toggle-pw" onclick="togglePw('reg-password')" style="background:none;border:none;cursor:pointer;font-size:16px;padding:0 12px">👁️</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label>Confirmer</label>
|
||||||
|
<div class="input-wrapper">
|
||||||
|
<span class="icon">🔒</span>
|
||||||
|
<input type="password" id="reg-confirm" placeholder="Retapez le mot de passe">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="checkbox mb-8">
|
||||||
|
<input type="checkbox" id="reg-terms">
|
||||||
|
<span>J'accepte les <a href="#" class="link">conditions d'utilisation</a></span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" id="btn-register" onclick="handleRegister()">
|
||||||
|
<span>S'inscrire</span>
|
||||||
|
</button>
|
||||||
|
<div class="text-center mt-24">
|
||||||
|
<span style="font-size:14px;color:var(--text-secondary)">Déjà un compte ? </span>
|
||||||
|
<a href="#" class="link" onclick="showScreen('login')">Se connecter</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════
|
||||||
|
SCREEN: HOME DASHBOARD
|
||||||
|
═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="screen-home" class="screen">
|
||||||
|
<div class="dashboard-header">
|
||||||
|
<div class="dashboard-header-top">
|
||||||
|
<div>
|
||||||
|
<div class="greeting">Bonjour 👋</div>
|
||||||
|
<div class="user-name" id="home-user-name">Utilisateur</div>
|
||||||
|
</div>
|
||||||
|
<button class="notification-btn" onclick="showScreen('notifications')">🔔<span class="notification-badge">3</span></button>
|
||||||
|
</div>
|
||||||
|
<div class="search-bar">
|
||||||
|
<span>🔍</span>
|
||||||
|
<input type="text" placeholder="Rechercher un service, un lieu...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="scrollable" style="padding-top:60px">
|
||||||
|
<div class="metrics-row">
|
||||||
|
<div class="metric-card"><div class="metric-value" style="color:var(--accent)">28°</div><div class="metric-unit">C</div><div class="metric-label">Temp.</div></div>
|
||||||
|
<div class="metric-card"><div class="metric-value" style="color:#64B5F6">72%</div><div class="metric-unit"></div><div class="metric-label">Humidité</div></div>
|
||||||
|
<div class="metric-card"><div class="metric-value" style="color:#FFA726">42</div><div class="metric-unit">AQI</div><div class="metric-label">Air</div></div>
|
||||||
|
<div class="metric-card"><div class="metric-value" style="color:#EF5350">55</div><div class="metric-unit">dB</div><div class="metric-label">Bruit</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header"><div class="section-title">Actions rapides</div></div>
|
||||||
|
<div class="quick-actions">
|
||||||
|
<a href="#" class="quick-action" onclick="showScreen('signalements')"><div class="quick-action-icon" style="background:rgba(211,47,47,0.15)">🚨</div><div class="quick-action-label">Signaler</div></a>
|
||||||
|
<a href="#" class="quick-action" onclick="showScreen('map')"><div class="quick-action-icon" style="background:rgba(21,101,192,0.15)">🚌</div><div class="quick-action-label">Transport</div></a>
|
||||||
|
<a href="#" class="quick-action" onclick="showScreen('marketplace')"><div class="quick-action-icon" style="background:rgba(46,125,50,0.15)">⚡</div><div class="quick-action-label">Énergie</div></a>
|
||||||
|
<a href="#" class="quick-action" onclick="showScreen('sante')"><div class="quick-action-icon" style="background:rgba(245,124,0,0.15)">🏥</div><div class="quick-action-label">Santé</div></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header"><div class="section-title">Météo</div><a href="#" class="see-all" onclick="showScreen('meteo')">Voir tout →</a></div>
|
||||||
|
<div class="card" style="background:linear-gradient(135deg,#1565C0,#0288D1);color:white">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:36px;font-weight:700">28°C</div>
|
||||||
|
<div style="font-size:13px;opacity:0.8">Fort-de-France</div>
|
||||||
|
<div style="font-size:12px;opacity:0.7;margin-top:4px">Ensoleillé • Humidité 72%</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:48px">☀️</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header"><div class="section-title">Capteurs en direct</div><a href="#" class="see-all" onclick="showScreen('sensors')">Voir tout →</a></div>
|
||||||
|
<div id="sensors-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header"><div class="section-title">Alertes</div></div>
|
||||||
|
<div class="card">
|
||||||
|
<div style="display:flex;gap:10px;align-items:flex-start">
|
||||||
|
<span style="font-size:20px">⚠️</span>
|
||||||
|
<div>
|
||||||
|
<div class="card-title">Qualité air — Schoelcher</div>
|
||||||
|
<div class="card-subtitle">AQI élevé détecté. Évitez les efforts prolongés.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="top-nav">
|
||||||
|
<a href="#" class="nav-item active" onclick="showScreen('home')"><span class="nav-item-icon">🏠</span>Accueil</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('map')"><span class="nav-item-icon">🗺️</span>Carte</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('marketplace')"><span class="nav-item-icon">🛒</span>Market</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('chat')"><span class="nav-item-icon">🤖</span>IA Chat</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('profile')"><span class="nav-item-icon">👤</span>Profil</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════
|
||||||
|
SCREEN: MAP
|
||||||
|
═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="screen-map" class="screen">
|
||||||
|
<div class="dashboard-header" style="padding-bottom:16px">
|
||||||
|
<div class="section-header"><div class="section-title">Carte Interactive</div><span style="font-size:13px;color:rgba(255,255,255,0.7)">Martinique</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="scrollable" style="padding-top:60px">
|
||||||
|
<div class="map-placeholder">
|
||||||
|
<div style="font-size:48px;margin-bottom:8px">🗺️</div>
|
||||||
|
<div style="font-size:14px;font-weight:600">Carte OpenStreetMap</div>
|
||||||
|
<div style="font-size:12px;color:#81C784">14.6°N, 61.0°W</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header"><div class="section-title">Couches</div></div>
|
||||||
|
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||||
|
<span class="badge badge-blue" onclick="toggleLayer(this)">📡 Capteurs</span>
|
||||||
|
<span class="badge badge-green" onclick="toggleLayer(this)">🌬️ Qualité Air</span>
|
||||||
|
<span class="badge badge-orange" onclick="toggleLayer(this)">🔊 Bruit</span>
|
||||||
|
<span class="badge" onclick="toggleLayer(this)">🚗 Trafic</span>
|
||||||
|
<span class="badge badge-blue" onclick="toggleLayer(this)">🌤️ Météo</span>
|
||||||
|
<span class="badge badge-red" onclick="toggleLayer(this)">🎉 Événements</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header"><div class="section-title">Capteurs à proximité</div></div>
|
||||||
|
<div id="nearby-sensors"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="top-nav">
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('home')"><span class="nav-item-icon">🏠</span>Accueil</a>
|
||||||
|
<a href="#" class="nav-item active" onclick="showScreen('map')"><span class="nav-item-icon">🗺️</span>Carte</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('marketplace')"><span class="nav-item-icon">🛒</span>Market</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('chat')"><span class="nav-item-icon">🤖</span>IA Chat</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('profile')"><span class="nav-item-icon">👤</span>Profil</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════
|
||||||
|
SCREEN: MARKETPLACE
|
||||||
|
═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="screen-marketplace" class="screen">
|
||||||
|
<div class="dashboard-header" style="padding-bottom:16px">
|
||||||
|
<div class="section-header"><div class="section-title">Marketplace</div><span style="font-size:13px;color:rgba(255,255,255,0.7)">Services & Produits</span></div>
|
||||||
|
<div class="search-bar" style="margin-top:12px"><span>🔍</span><input type="text" placeholder="Rechercher un service..."></div>
|
||||||
|
</div>
|
||||||
|
<div class="scrollable" style="padding-top:60px">
|
||||||
|
<div class="section">
|
||||||
|
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px">
|
||||||
|
<span class="badge badge-blue" onclick="filterCategory(this)">Tous</span>
|
||||||
|
<span class="badge" onclick="filterCategory(this)">🍎 Alimentation</span>
|
||||||
|
<span class="badge" onclick="filterCategory(this)">🚌 Transport</span>
|
||||||
|
<span class="badge" onclick="filterCategory(this)">⚡ Énergie</span>
|
||||||
|
<span class="badge" onclick="filterCategory(this)">🏥 Santé</span>
|
||||||
|
</div>
|
||||||
|
<div id="marketplace-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="top-nav">
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('home')"><span class="nav-item-icon">🏠</span>Accueil</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('map')"><span class="nav-item-icon">🗺️</span>Carte</a>
|
||||||
|
<a href="#" class="nav-item active" onclick="showScreen('marketplace')"><span class="nav-item-icon">🛒</span>Market</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('chat')"><span class="nav-item-icon">🤖</span>IA Chat</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('profile')"><span class="nav-item-icon">👤</span>Profil</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════
|
||||||
|
SCREEN: AI CHAT
|
||||||
|
═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="screen-chat" class="screen">
|
||||||
|
<div class="dashboard-header" style="padding-bottom:16px;flex-shrink:0">
|
||||||
|
<div style="display:flex;align-items:center;gap:12px">
|
||||||
|
<div style="width:44px;height:44px;border-radius:14px;background:rgba(255,255,255,0.15);display:flex;align-items:center;justify-content:center;font-size:24px">🤖</div>
|
||||||
|
<div><div style="font-size:16px;font-weight:700;color:white">Smart City IA</div><div style="font-size:12px;color:rgba(255,255,255,0.7)">En ligne</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="scrollable" style="padding:16px;display:flex;flex-direction:column" id="chat-messages">
|
||||||
|
<div class="chat-message chat-message-bot">Bonjour ! Je suis Smart City IA 🤖<br><br>Je peux vous aider avec :<br>• 🌤️ Météo en temps réel<br>• 🚌 Transports<br>• ⚡ Énergie<br>• 🚨 Alertes<br><br>Comment puis-je vous aider ?</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:8px 16px;display:flex;gap:8px;flex-wrap:wrap;background:var(--bg-card);border-top:1px solid rgba(255,255,255,0.05)">
|
||||||
|
<span class="badge badge-blue" onclick="quickPrompt('Météo aujourd\\'hui')">🌤️ Météo</span>
|
||||||
|
<span class="badge badge-green" onclick="quickPrompt('Prochain bus')">🚌 Bus</span>
|
||||||
|
<span class="badge badge-orange" onclick="quickPrompt('Consommation énergie')">⚡ Énergie</span>
|
||||||
|
<span class="badge badge-red" onclick="quickPrompt('Alertes en cours')">🚨 Alertes</span>
|
||||||
|
</div>
|
||||||
|
<div class="chat-input">
|
||||||
|
<input type="text" id="chat-input" placeholder="Posez votre question..." onkeypress="if(event.key==='Enter')sendChat()">
|
||||||
|
<button onclick="sendChat()">➤</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════
|
||||||
|
SCREEN: PROFILE
|
||||||
|
═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="screen-profile" class="screen">
|
||||||
|
<div class="scrollable" style="padding-top:60px">
|
||||||
|
<div class="dashboard-header" style="text-align:center;padding-bottom:24px">
|
||||||
|
<div style="width:70px;height:70px;border-radius:18px;background:rgba(255,255,255,0.15);margin:0 auto 10px;display:flex;align-items:center;justify-content:center;font-size:36px">👤</div>
|
||||||
|
<div style="font-size:18px;font-weight:700;color:white" id="profile-name">Utilisateur</div>
|
||||||
|
<div style="font-size:12px;color:rgba(255,255,255,0.7)" id="profile-email">-</div>
|
||||||
|
<div style="display:flex;gap:6px;justify-content:center;margin-top:10px">
|
||||||
|
<span class="badge badge-blue">🏙️ Citoyen</span>
|
||||||
|
<span class="badge badge-green">✅ Vérifié</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:16px">
|
||||||
|
<div class="card" style="text-align:center"><div style="font-size:20px;font-weight:700;color:var(--primary)">12</div><div style="font-size:10px;color:var(--text-muted)">Signalements</div></div>
|
||||||
|
<div class="card" style="text-align:center"><div style="font-size:20px;font-weight:700;color:var(--primary)">45</div><div style="font-size:10px;color:var(--text-muted)">Points</div></div>
|
||||||
|
<div class="card" style="text-align:center"><div style="font-size:20px;font-weight:700;color:var(--primary)">5</div><div style="font-size:10px;color:var(--text-muted)">Abonnements</div></div>
|
||||||
|
</div>
|
||||||
|
<div id="profile-menu"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav class="top-nav">
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('home')"><span class="nav-item-icon">🏠</span>Accueil</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('map')"><span class="nav-item-icon">🗺️</span>Carte</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('marketplace')"><span class="nav-item-icon">🛒</span>Market</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('chat')"><span class="nav-item-icon">🤖</span>IA Chat</a>
|
||||||
|
<a href="#" class="nav-item active" onclick="showScreen('profile')"><span class="nav-item-icon">👤</span>Profil</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════
|
||||||
|
SCREEN: NOTIFICATIONS
|
||||||
|
═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="screen-notifications" class="screen">
|
||||||
|
<div class="dashboard-header" style="padding-bottom:24px">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div class="section-header"><div class="section-title">Notifications</div></div>
|
||||||
|
<button class="link" style="font-size:13px" onclick="markAllRead()">Tout lu</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="scrollable" style="padding-top:60px">
|
||||||
|
<div class="section" id="notifications-list"></div>
|
||||||
|
</div>
|
||||||
|
<nav class="top-nav">
|
||||||
|
<a href="#" class="nav-item active" onclick="showScreen('home')"><span class="nav-item-icon">🏠</span>Accueil</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('map')"><span class="nav-item-icon">🗺️</span>Carte</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('marketplace')"><span class="nav-item-icon">🛒</span>Market</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('chat')"><span class="nav-item-icon">🤖</span>IA Chat</a>
|
||||||
|
<a href="#" class="nav-item" onclick="showScreen('profile')"><span class="nav-item-icon">👤</span>Profil</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════
|
||||||
|
PLACEHOLDER SCREENS
|
||||||
|
═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="screen-signalements" class="screen"><div class="scrollable" style="padding:60px 20px;text-align:center"><div style="font-size:48px;margin-bottom:16px">🚨</div><h2>Signalements</h2><p style="color:var(--text-secondary);margin-top:8px">Écran en cours de développement</p><button class="btn btn-primary" style="margin-top:24px" onclick="showScreen('home')">← Retour</button></div><nav class="top-nav"><a href="#" class="nav-item active" onclick="showScreen('home')"><span class="nav-item-icon">🏠</span>Accueil</a><a href="#" class="nav-item" onclick="showScreen('map')"><span class="nav-item-icon">🗺️</span>Carte</a><a href="#" class="nav-item" onclick="showScreen('marketplace')"><span class="nav-item-icon">🛒</span>Market</a><a href="#" class="nav-item" onclick="showScreen('chat')"><span class="nav-item-icon">🤖</span>IA Chat</a><a href="#" class="nav-item" onclick="showScreen('profile')"><span class="nav-item-icon">👤</span>Profil</a></nav></div>
|
||||||
|
<div id="screen-sensors" class="screen"><div class="scrollable" style="padding:60px 20px;text-align:center"><div style="font-size:48px;margin-bottom:16px">📡</div><h2>Capteurs IoT</h2><p style="color:var(--text-secondary);margin-top:8px">Écran en cours de développement</p><button class="btn btn-primary" style="margin-top:24px" onclick="showScreen('home')">← Retour</button></div><nav class="top-nav"><a href="#" class="nav-item active" onclick="showScreen('home')"><span class="nav-item-icon">🏠</span>Accueil</a><a href="#" class="nav-item" onclick="showScreen('map')"><span class="nav-item-icon">🗺️</span>Carte</a><a href="#" class="nav-item" onclick="showScreen('marketplace')"><span class="nav-item-icon">🛒</span>Market</a><a href="#" class="nav-item" onclick="showScreen('chat')"><span class="nav-item-icon">🤖</span>IA Chat</a><a href="#" class="nav-item" onclick="showScreen('profile')"><span class="nav-item-icon">👤</span>Profil</a></nav></div>
|
||||||
|
<div id="screen-meteo" class="screen"><div class="scrollable" style="padding:60px 20px;text-align:center"><div style="font-size:48px;margin-bottom:16px">🌤️</div><h2>Météo</h2><p style="color:var(--text-secondary);margin-top:8px">Écran en cours de développement</p><button class="btn btn-primary" style="margin-top:24px" onclick="showScreen('home')">← Retour</button></div><nav class="top-nav"><a href="#" class="nav-item active" onclick="showScreen('home')"><span class="nav-item-icon">🏠</span>Accueil</a><a href="#" class="nav-item" onclick="showScreen('map')"><span class="nav-item-icon">🗺️</span>Carte</a><a href="#" class="nav-item" onclick="showScreen('marketplace')"><span class="nav-item-icon">🛒</span>Market</a><a href="#" class="nav-item" onclick="showScreen('chat')"><span class="nav-item-icon">🤖</span>IA Chat</a><a href="#" class="nav-item" onclick="showScreen('profile')"><span class="nav-item-icon">👤</span>Profil</a></nav></div>
|
||||||
|
<div id="screen-sante" class="screen"><div class="scrollable" style="padding:60px 20px;text-align:center"><div style="font-size:48px;margin-bottom:16px">🏥</div><h2>Santé</h2><p style="color:var(--text-secondary);margin-top:8px">Écran en cours de développement</p><button class="btn btn-primary" style="margin-top:24px" onclick="showScreen('home')">← Retour</button></div><nav class="top-nav"><a href="#" class="nav-item active" onclick="showScreen('home')"><span class="nav-item-icon">🏠</span>Accueil</a><a href="#" class="nav-item" onclick="showScreen('map')"><span class="nav-item-icon">🗺️</span>Carte</a><a href="#" class="nav-item" onclick="showScreen('marketplace')"><span class="nav-item-icon">🛒</span>Market</a><a href="#" class="nav-item" onclick="showScreen('chat')"><span class="nav-item-icon">🤖</span>IA Chat</a><a href="#" class="nav-item" onclick="showScreen('profile')"><span class="nav-item-icon">👤</span>Profil</a></nav></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ── Auth Token ──
|
||||||
|
let authToken = localStorage.getItem('authToken') || null;
|
||||||
|
|
||||||
|
// ── Screen Navigation ──
|
||||||
|
function showScreen(id) {
|
||||||
|
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
||||||
|
document.getElementById('screen-' + id).classList.add('active');
|
||||||
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
||||||
|
const scrollable = document.querySelector('#screen-' + id + ' .scrollable');
|
||||||
|
if (scrollable) scrollable.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Toggle Password ──
|
||||||
|
function togglePw(id) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
el.type = el.type === 'password' ? 'text' : 'password';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── API ──
|
||||||
|
async function apiCall(method, path, body, auth) {
|
||||||
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
if (auth && authToken) headers['Authorization'] = 'Bearer ' + authToken;
|
||||||
|
const opts = { method, headers };
|
||||||
|
if (body) opts.body = JSON.stringify(body);
|
||||||
|
const resp = await fetch('https://api-smartapp.digitribe.fr/api' + path, opts);
|
||||||
|
const data = await resp.json().catch(() => null);
|
||||||
|
return { ok: resp.ok, status: resp.status, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Login ──
|
||||||
|
async function handleLogin() {
|
||||||
|
const btn = document.getElementById('btn-login');
|
||||||
|
const email = document.getElementById('login-email').value.trim();
|
||||||
|
const password = document.getElementById('login-password').value;
|
||||||
|
const err = document.getElementById('login-error');
|
||||||
|
if (!email || !password) { err.textContent = 'Remplissez tous les champs'; err.classList.add('show'); return; }
|
||||||
|
btn.disabled = true; btn.innerHTML = '<div class="spinner"></div>';
|
||||||
|
try {
|
||||||
|
const { ok, data } = await apiCall('POST', '/auth/login', { email, password }, false);
|
||||||
|
if (ok && data?.accessToken) {
|
||||||
|
authToken = data.accessToken;
|
||||||
|
localStorage.setItem('authToken', authToken);
|
||||||
|
localStorage.setItem('user', JSON.stringify(data.user));
|
||||||
|
loadDashboard(); showScreen('home');
|
||||||
|
} else { err.textContent = data?.message || 'Identifiants incorrects'; err.classList.add('show'); }
|
||||||
|
} catch(e) { err.textContent = 'Erreur de connexion'; err.classList.add('show'); }
|
||||||
|
btn.disabled = false; btn.innerHTML = '<span>Se connecter</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Register ──
|
||||||
|
async function handleRegister() {
|
||||||
|
const btn = document.getElementById('btn-register');
|
||||||
|
const fn = document.getElementById('reg-firstname').value.trim();
|
||||||
|
const ln = document.getElementById('reg-lastname').value.trim();
|
||||||
|
const em = document.getElementById('reg-email').value.trim();
|
||||||
|
const pw = document.getElementById('reg-password').value;
|
||||||
|
const cf = document.getElementById('reg-confirm').value;
|
||||||
|
const err = document.getElementById('register-error');
|
||||||
|
const ok = document.getElementById('register-success');
|
||||||
|
err.classList.remove('show'); ok.classList.remove('show');
|
||||||
|
if (!fn||!ln||!em||!pw) { err.textContent = 'Remplissez tous les champs'; err.classList.add('show'); return; }
|
||||||
|
if (pw.length < 8) { err.textContent = 'Min. 8 caractères'; err.classList.add('show'); return; }
|
||||||
|
if (pw !== cf) { err.textContent = 'Mots de passe différents'; err.classList.add('show'); return; }
|
||||||
|
btn.disabled = true; btn.innerHTML = '<div class="spinner"></div>';
|
||||||
|
try {
|
||||||
|
const { ok: success, data } = await apiCall('POST', '/auth/register', { email:em, password:pw, firstName:fn, lastName:ln }, false);
|
||||||
|
if (success) { ok.textContent = 'Compte créé ! Connexion...'; ok.classList.add('show'); setTimeout(()=>{ handleLogin(); }, 1000); }
|
||||||
|
else { err.textContent = data?.message || 'Erreur'; err.classList.add('show'); }
|
||||||
|
} catch(e) { err.textContent = 'Erreur de connexion'; err.classList.add('show'); }
|
||||||
|
btn.disabled = false; btn.innerHTML = '<span>S\'inscrire</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function socialLogin(p) { alert(p + ' — bientôt disponible'); }
|
||||||
|
function forgotPassword() { const e = prompt('Email :'); if(e) alert('Lien envoyé à ' + e); }
|
||||||
|
|
||||||
|
// ── Dashboard ──
|
||||||
|
function loadDashboard() {
|
||||||
|
const u = JSON.parse(localStorage.getItem('user') || '{}');
|
||||||
|
document.getElementById('home-user-name').textContent = u.firstName || 'Utilisateur';
|
||||||
|
document.getElementById('profile-name').textContent = (u.firstName||'') + ' ' + (u.lastName||'');
|
||||||
|
document.getElementById('profile-email').textContent = u.email || '-';
|
||||||
|
renderSensors(); renderMarketplace(); renderNotifications(); renderProfileMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSensors() {
|
||||||
|
const s = [
|
||||||
|
{n:'Centre-Ville',l:'Fort-de-France',v:'28.3',u:'°C',i:'🌡️',c:'badge-green'},
|
||||||
|
{n:'Port',l:'Baie de FDF',v:'72',u:'%',i:'💧',c:'badge-green'},
|
||||||
|
{n:'Qualité Air',l:'Schoelcher',v:'85',u:'AQI',i:'🌬️',c:'badge-orange'},
|
||||||
|
{n:'Bruit',l:'Centre-ville',v:'68',u:'dB',i:'🔊',c:'badge-red'},
|
||||||
|
];
|
||||||
|
document.getElementById('sensors-list').innerHTML = s.map(x=>`
|
||||||
|
<div class="card"><div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div style="display:flex;gap:10px;align-items:center">
|
||||||
|
<div style="width:40px;height:40px;border-radius:12px;background:rgba(21,101,192,0.1);display:flex;align-items:center;justify-content:center;font-size:18px">${x.i}</div>
|
||||||
|
<div><div class="card-title">${x.n}</div><div class="card-subtitle">📍 ${x.l}</div></div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:right"><div style="font-size:20px;font-weight:700">${x.v}<span style="font-size:11px;color:var(--text-muted)"> ${x.u}</span></div><span class="badge ${x.c}">${x.u}</span></div>
|
||||||
|
</div></div>
|
||||||
|
`).join('');
|
||||||
|
document.getElementById('nearby-sensors').innerHTML = document.getElementById('sensors-list').innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarketplace() {
|
||||||
|
const p = [
|
||||||
|
{n:'Panier Bio Local',pr:'AMAP Martinique',p:'15€',u:'/semaine',i:'🥬',r:4.8},
|
||||||
|
{n:'Pass Transport',pr:'CFTA',p:'45€',u:'/mois',i:'🎫',r:4.2},
|
||||||
|
{n:'Kit Solaire',pr:'EDF DOM',p:'299€',u:'',i:'☀️',r:4.6},
|
||||||
|
{n:'Télémédecine',pr:'Santé+',p:'25€',u:'/consult',i:'🩺',r:4.5},
|
||||||
|
];
|
||||||
|
document.getElementById('marketplace-list').innerHTML = p.map(x=>`
|
||||||
|
<div class="card"><div style="display:flex;gap:12px;align-items:center">
|
||||||
|
<div style="width:52px;height:52px;border-radius:14px;background:rgba(21,101,192,0.1);display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0">${x.i}</div>
|
||||||
|
<div style="flex:1"><div class="card-title">${x.n}</div><div class="card-subtitle">${x.pr}</div>
|
||||||
|
<div class="card-row"><span style="font-size:16px;font-weight:700;color:var(--primary)">${x.p}</span><span style="font-size:12px;color:var(--text-muted)">${x.u}</span><span style="font-size:12px;color:var(--text-secondary)">⭐ ${x.r}</span></div></div>
|
||||||
|
<button class="btn btn-primary" style="width:auto;padding:8px 16px;font-size:13px" onclick="alert('Ajouté')">+</button>
|
||||||
|
</div></div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNotifications() {
|
||||||
|
const n = [
|
||||||
|
{i:'🚨',t:'Alerte Qualité Air',b:'AQI élevé à Schoelcher (85).',h:'Il y a 30 min'},
|
||||||
|
{i:'🎉',t:'Fête de la Musique',b:'21 juin au centre-ville.',h:'Il y a 2j'},
|
||||||
|
{i:'ℹ️',t:'Nouveau service',b:'Suivi bus temps réel disponible !',h:'Il y a 5j'},
|
||||||
|
];
|
||||||
|
document.getElementById('notifications-list').innerHTML = n.map(x=>`
|
||||||
|
<div class="card"><div style="display:flex;gap:12px;align-items:flex-start">
|
||||||
|
<span style="font-size:24px">${x.i}</span>
|
||||||
|
<div style="flex:1"><div class="card-title">${x.t}</div><div class="card-subtitle">${x.b}</div>
|
||||||
|
<div style="font-size:11px;color:var(--text-muted);margin-top:4px">${x.h}</div></div>
|
||||||
|
</div></div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderProfileMenu() {
|
||||||
|
const items = [
|
||||||
|
{i:'🔔',l:'Notifications',a:"showScreen('notifications')"},
|
||||||
|
{i:'🌍',l:'Langue',v:'Français',a:"alert('Bientôt')"},
|
||||||
|
{i:'🌙',l:'Mode sombre',v:'Off',a:"alert('Bientôt')"},
|
||||||
|
{i:'🔒',l:'Confidentialité',a:"alert('Bientôt')"},
|
||||||
|
{i:'❓',l:'Aide & Support',a:"alert('Bientôt')"},
|
||||||
|
{i:'ℹ️',l:'À propos',v:'v0.1.0',a:"alert('Smart City Martinique v0.1.0')"},
|
||||||
|
{i:'🚪',l:'Déconnexion',a:'logout()',s:'color:var(--danger)'},
|
||||||
|
];
|
||||||
|
document.getElementById('profile-menu').innerHTML = items.map(x=>`
|
||||||
|
<div class="card" onclick="${x.a}" style="cursor:pointer;display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div style="display:flex;gap:12px;align-items:center"><span style="font-size:20px">${x.i}</span><span style="font-size:15px;${x.s||''}">${x.l}</span></div>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">${x.v?`<span style="font-size:13px;color:var(--text-muted)">${x.v}</span>`:''}<span style="font-size:18px;color:var(--text-muted)">›</span></div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() { authToken=null; localStorage.removeItem('authToken'); localStorage.removeItem('user'); showScreen('login'); }
|
||||||
|
function toggleLayer(el) { el.style.opacity = el.style.opacity === '0.5' ? '1' : '0.5'; }
|
||||||
|
function filterCategory(el) { document.querySelectorAll('[onclick^="filterCategory"]').forEach(e=>{e.style.background='';e.style.color='';}); el.style.background='var(--primary)'; el.style.color='white'; }
|
||||||
|
function markAllRead() { alert('Tout marqué comme lu'); }
|
||||||
|
|
||||||
|
// ── Chat ──
|
||||||
|
const chatResp = {
|
||||||
|
'météo':'☀️ Fort-de-France : 28°C, ensoleillé. Humidité 72%, vent 12 km/h NE.',
|
||||||
|
'bus':'🚌 L1 (Centre→Schoelcher) dans 8 min. L3 dans 12 min.',
|
||||||
|
'énergie':'⚡ Conso semaine : 45 kWh (-12%). Estimation mensuelle : 180 kWh.',
|
||||||
|
'alerte':'🚨 1 alerte : AQI élevé à Schoelcher (85).',
|
||||||
|
};
|
||||||
|
function sendChat() {
|
||||||
|
const input = document.getElementById('chat-input');
|
||||||
|
const msg = input.value.trim(); if (!msg) return;
|
||||||
|
const c = document.getElementById('chat-messages');
|
||||||
|
c.innerHTML += `<div class="chat-message chat-message-user">${msg}</div>`;
|
||||||
|
input.value = '';
|
||||||
|
setTimeout(()=>{
|
||||||
|
const l = msg.toLowerCase();
|
||||||
|
let r = 'Je consulte les données... 🔍';
|
||||||
|
for (const [k,v] of Object.entries(chatResp)) { if(l.includes(k)) { r=v; break; } }
|
||||||
|
c.innerHTML += `<div class="chat-message chat-message-bot">${r}</div>`;
|
||||||
|
c.scrollTop = c.scrollHeight;
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
function quickPrompt(t) { document.getElementById('chat-input').value = t; sendChat(); }
|
||||||
|
|
||||||
|
// ── Auto-login ──
|
||||||
|
if (authToken) { loadDashboard(); showScreen('home'); }
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
smart-app-city/frontend/nginx.conf
Normal file
26
smart-app-city/frontend/nginx.conf
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name smartapp.digitribe.fr;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Gzip
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
|
||||||
|
|
||||||
|
# SPA routing - must be first
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static assets caching
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|map)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
location /health {
|
||||||
|
return 200 'OK';
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user