Compare commits

...

2 Commits

Author SHA1 Message Date
Eric FELIXINE
cb45b89f1f fix: JupyterHub Dockerfile - add eric user, sudo, fix DB path (4 slashes) 2026-06-01 10:26:47 -04:00
Eric FELIXINE
9e933fea66 chore: session backup 2026-06-01 final — TODO restructuré, état complet 2026-05-30 08:49:31 -04:00
17 changed files with 879 additions and 51 deletions

87
TODO.md
View File

@@ -1,54 +1,69 @@
# Smart City Digital Twin — TODO List
> Dernière mise à jour : 2026-06-01 07:00
> Dernière mise à jour : 2026-06-01 17:00 (fin de session)
## ✅ Complété (cette session 2026-06-01)
## ✅ Complété (session 2026-06-01)
| ID | Tâche |
|----|-------|
| jupyterhub-fix | JupyterHub DB path fix (absolute path) → healthy ✅ |
| jupyterhub-user | User eric créé + autorisé dans JupyterHub (admin) |
| or-map-bounds | OR mbtiles metadata bounds → monde, center → Martinique ✅ |
| or-map-verify | OR API confirmée: center=[-61,14.5], minZoom=0, bounds=Martinique |
| hermes-dashboard | Hermes Dashboard WebUI + TUI chat activé (localhost:9119, auto-boot) |
| git-push | Commit 008f167 pushé sur Gitea |
| ID | Tâche | Détail |
|----|-------|--------|
| jupyterhub-fix | JupyterHub DB path | `sqlite:////srv/jupyterhub/jupyterhub.sqlite` (absolute path) |
| jupyterhub-rebuild | Rebuild Dockerfile | Supprimé double-nested `/srv/jupyterhub/srv/jupyterhub` |
| jupyterhub-spawner | Spawner config | `SimpleLocalProcessSpawner`, timeout 300s |
| jupyterhub-user | User eric | Créé id=2, admin, authorized |
| hermes-dashboard | Dashboard WebUI+TUI | systemd service, localhost:9119, auto-boot |
| or-mbtiles-metadata | Bounds monde + center Martinique | `sqlite3` UPDATE sur metadata |
| or-map-settings | mapsettings.json vérifié | center=[-61,14.5], bounds=Martinique, minZoom=0 |
| or-mbtiles-location | mbtiles actif = /storage/map/ | PAS /opt/map/ (écrasé par volume) |
| trino-fix | node.properties créé | `node.environment=production` — restart needed |
| skill-update | openremote-overview | Section Map & Tile Configuration ajoutée |
| git-push | Commits | `acdf250` pushé sur Gitea |
## 🔴 Bloqué / En cours
| ID | Tâche | Raison |
|----|-------|--------|
| jupyterhub-spawn | Spawn user eric timeout (30s→120s fixé, mais singleuser lent) | Container resource limit? |
| or-tiles | Carte OR fond gris sur Martinique | mbtiles contient tiles Pays-Bas, pas Martinique |
| kafka-fix | Kafka restart loop | `zookeeper.connect` manquant |
| trino-fix | Trino restart loop | `node.environment` null |
| 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 |
| jupyterhub-spawn | Spawn eric timeout | Container resource limit? | Augmenter CPU/RAM container OU debug logs |
| kafka-restart | Kafka restart loop | Volumes corrimpus (ancien ZK data) | SUPPRIMER volumes kafka-1-data + kafka-2-data, recréer |
| trino-restart | Trino restart loop | node.properties créé mais pas appliqué | `docker restart trino` |
## ⏳ En attente
| ID | Tâche |
|----|-------|
| p1-or-restart | Vérifier OR map tiles après remplacement mbtiles Martinique |
| or-mbtiles-martinique | Générer mbtiles MVT PBF pour Martinique (tippecanoe depuis GeoJSON filtré) |
| p1-or-map | Vérifier carte Martinique après fix bounds |
| p1-contexus-60 | Configurer les 60 devices Contexus |
| p3-analyse | Analyse: GeoMesa + KeplerGL |
| p1-ngsi | NGSI-LD: validation pipeline (basse priorité) |
| p0-chirpstack | ChirpStack: login API gRPC-REST |
| p1-thingsboard | Relayer ThingsBoard (si CPU dispo) |
| smart-app Phase 1 | MVP React Native (dashboard, carte, signalement) |
| smart-app Phase 2 | Transport, Beckn integration, chatbot RAG |
| smart-app Phase 3 | AI Agents, prédictions, réalité augmentée |
| p3-analyse | GeoMesa + KeplerGL |
| p0-chirpstack | ChirpStack login API gRPC-REST |
| p1-thingsboard | Relancer ThingsBoard (si CPU dispo) |
| smart-app Phase 1 | MVP React Native |
## 📝 Notes 2026-06-01
## 📝 Notes techniques 2026-06-01
- **86 conteneurs Docker** au total
- **JupyterHub** : https://jupyter.digitribe.fr — user eric/admin créé, spawn lent
- **OpenRemote** : https://openremote.digitribe.fr — carte centrée Martinique, dézoom libre (minZoom=0), mais tiles Pays-Bas (fond gris)
- **Hermes Dashboard** : http://127.0.0.1:9119 (SSH tunnel) — WebUI + TUI chat, auto-boot
- **OR mbtiles** : metadata bounds monde OK, mais contenu = vector tiles Pays-Bas. Script `scripts/generate_martinique_mbtiles.py` prêt pour génération
- **Pipeline données** : Simulateur → Mosquitto/BunkerM → Telegraf → InfluxDB → Grafana ✅
- **Grafana** : Dashboard smartcity-martinique-complete v7 ✅
- **Superset** : https://superset.digitribe.fr ✅
- **Metabase** : https://metabase.digitribe.fr ✅
- **ODK Central** : https://odk.digitribe.fr ✅
- **MindsDB** : https://mindsdb.digitribe.fr ✅
### OpenRemote mbtiles — Points critiques
- Fichier actif : `/storage/map/mapdata.mbtiles` (volume Docker), PAS `/opt/map/`
- OR 1.24.0 ne sert que du **PBF vectoriel** — PNG raster = 404
- Bug : MapService.java donne priorité aux bounds du mbtiles metadata sur mapsettings.json
- Fix : bounds mbtiles metadata = monde (`-180,-85,180,85`), bounds mapsettings = zone désirée
- Pour mettre à jour : `docker cp file.mbtiles openremote-manager:/storage/map/mapdata.mbtiles`
### JupyterHub
- Port : 8000 (pas 8080)
- User eric : id=2, admin, password=Digitribe972 (hash bcrypt dans users_info)
- Config : `SimpleLocalProcessSpawner`, timeout 300s
- DB : `sqlite:////srv/jupyterhub/jupyterhub.sqlite` (absolute path, 4 slashes)
### Hermes Dashboard
- Service : `hermes-dashboard.service` (systemd user)
- URL : `http://localhost:9119` (accès via SSH tunnel `-L 9119:127.0.0.1:9119`)
- TUI chat intégré dans l'onglet Chat du dashboard
### Infrastructure
- 86 conteneurs Docker au total
- Traefik, OpenRemote, Grafana, InfluxDB, Simulateur, ODK, MindsDB, MapStore, GeoServer, EMQX, Ditto, ChirpStack, Node-RED, MinIO, Flink, Gitea, LocalAI, PHPIPAM, Honcho = UP ✅
- Kafka, Trino = restart loop
- JupyterHub = UP mais spawn lent
## Credentials

View File

@@ -0,0 +1,6 @@
FROM eclipse/ditto-gateway:latest
USER root
# Copy the modified JAR (with open auth in reference.conf)
COPY ditto-gateway-service-3.8.12-allinone.jar /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar
USER ditto

View File

@@ -0,0 +1,22 @@
# Minimal override - authentication settings only
# This file is loaded after reference.conf by Typesafe config
ditto {
gateway {
authentication {
pre-authentication {
enabled = true
}
devops {
secured = false
devops-authentication-method = "basic"
password = "ditto-devops-secret"
password = ${?DEVOPS_PASSWORD}
status-secured = false
status-authentication-method = "basic"
statusPassword = "ditto-status-secret"
statusPassword = ${?STATUS_PASSWORD}
}
}
}
}

View File

@@ -0,0 +1,7 @@
#!/bin/sh
# Fix permissions for mounted config files
if [ -f /opt/ditto/gateway-extension.conf ]; then
chmod 644 /opt/ditto/gateway-extension.conf
fi
# Start the gateway
exec java -jar /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar

View File

@@ -0,0 +1,2 @@
#!/bin/sh
exec java -Dconfig.file=/opt/ditto/gateway.conf -jar /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar

View File

@@ -0,0 +1,76 @@
#!/bin/bash
set -e
WORKDIR=/tmp/ditto-jar-mod
rm -rf $WORKDIR && mkdir -p $WORKDIR
echo "=== Extracting JAR ==="
cd $WORKDIR
docker run --rm eclipse/ditto-gateway:latest cat /opt/ditto/ditto-gateway-service-3.8.12-allinone.jar > ditto-gateway-service-3.8.12-allinone.jar
jar xf ditto-gateway-service-3.8.12-allinone.jar reference.conf
echo "=== Original reference.conf tail ==="
tail -5 reference.conf
echo "=== Adding auth config to reference.conf ==="
# Remove the last closing brace, add our config, re-add the closing brace
# The reference.conf ends with nested braces - find the very last line
python3 << 'PYEOF'
with open("/tmp/ditto-jar-mod/reference.conf", "r") as f:
lines = f.readlines()
# Find the last non-empty line that is just a closing brace
# We need to insert our config before the outermost closing brace
# Simple approach: append before the very last }
# Count total closing braces at the end
content = "".join(lines)
# The reference.conf has a complex nested structure
# We'll add our ditto config as a new root-level block at the end
# We need to close the last block and add a comma, then our new block
# Actually, simpler: just append if the file ends with }
# Find the position of the very last }
last_brace_pos = content.rfind('}')
if last_brace_pos >= 0:
# Check if there's content after the last } (like kamon.conf include)
rest = content[last_brace_pos+1:].strip()
if rest:
# There's content after the last }, our approach is wrong
print(f"Content after last }}: {rest[:100]}")
# Insert before the last } with a comma
new_content = content[:last_brace_pos].rstrip()
# Remove trailing comma if present
if new_content.endswith(','):
new_content = new_content[:-1]
new_content += ',\n\n# Custom auth overrides\n' + rest[:0] + '\n' + ' authentication {\n pre-authentication {\n enabled = true\n }\n devops {\n secured = false\n }\n }\n}\n'
# Just append
new_content = content.rstrip() + '\n\n# Custom auth overrides\nditto {\n gateway {\n authentication {\n pre-authentication {\n enabled = true\n }\n devops {\n secured = false\n }\n }\n }\n}\n'
else:
# Last char is }, replace it with our config + }
new_content = content[:last_brace_pos].rstrip()
# Remove trailing comma
if new_content.endswith(','):
new_content = new_content[:-1]
new_content += ',\n\n# Custom auth overrides\n gateway {\n authentication {\n pre-authentication {\n enabled = true\n }\n devops {\n secured = false\n }\n }\n }\n}\n'
else:
new_content = content
with open("/tmp/ditto-jar-mod/reference.conf", "w") as f:
f.write(new_content)
print("reference.conf modified successfully")
PYEOF
echo "=== Modified reference.conf tail ==="
tail -20 reference.conf
echo "=== Updating JAR (replacing reference.conf) ==="
jar uf ditto-gateway-service-3.8.12-allinone.jar reference.conf
echo "=== Verifying modified reference.conf in JAR ==="
jar xf ditto-gateway-service-3.8.12-allinone.jar reference.conf
grep -c "pre-authentication" reference.conf || echo "NOT FOUND in JAR"
echo "=== Done. JAR is ready at $WORKDIR ==="

View File

@@ -0,0 +1,7 @@
ditto {
gateway {
http {
enablecors = true
}
}
}

View File

@@ -0,0 +1,363 @@
ditto {
version = "3.8.12"
extensions {
jwt-authorization-subjects-provider = {
extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DittoJwtAuthorizationSubjectsProvider
}
jwt-authentication-result-provider = {
extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DefaultJwtAuthenticationResultProvider
extension-config = {
role = regular
jwt-authorization-subjects-provider = {
extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DittoJwtAuthorizationSubjectsProvider
extension-config = {
role = regular
}
}
}
}
jwt-authentication-result-provider-devops = {
extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DefaultJwtAuthenticationResultProvider
extension-config = {
role = devops
jwt-authorization-subjects-provider = {
extension-class = org.eclipse.ditto.gateway.service.security.authentication.jwt.DittoJwtAuthorizationSubjectsProvider
extension-config = {
role = devops
}
}
}
}
signal-enrichment-provider {
extension-class = org.eclipse.ditto.gateway.service.endpoints.utils.DefaultGatewaySignalEnrichmentProvider
extension-config = {
cache {
enabled = true
maximum-size = 20000
expire-after-create = 2m
}
}
}
http-bind-flow-provider = org.eclipse.ditto.gateway.service.endpoints.routes.LoggingHttpBindFlowProvider
websocket-config-provider = org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpWebSocketConfigProvider
gateway-authentication-directive-factory = org.eclipse.ditto.gateway.service.endpoints.directives.auth.DittoGatewayAuthenticationDirectiveFactory
http-request-actor-props-factory = org.eclipse.ditto.gateway.service.endpoints.actors.DefaultHttpRequestActorPropsFactory
sse-event-sniffer = org.eclipse.ditto.gateway.service.endpoints.routes.sse.NoOpSseEventSniffer
streaming-authorization-enforcer = org.eclipse.ditto.gateway.service.streaming.NoOpAuthorizationEnforcer
incoming-websocket-event-sniffer = org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpIncomingWebSocketEventSniffer
outgoing-websocket-event-sniffer = org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpOutgoingWebSocketEventSniffer
custom-api-routes-provider = org.eclipse.ditto.gateway.service.endpoints.routes.NoopCustomApiRoutesProvider
sse-connection-supervisor = org.eclipse.ditto.gateway.service.endpoints.routes.sse.NoOpSseConnectionSupervisor
websocket-connection-supervisor = "org.eclipse.ditto.gateway.service.endpoints.routes.websocket.NoOpWebSocketSupervisor"
connections-retrieval-actor-props-factory = org.eclipse.ditto.gateway.service.endpoints.actors.DefaultConnectionsRetrievalActorPropsFactory
}
service-name = "gateway"
mapping-strategy.implementation = "org.eclipse.ditto.gateway.service.util.GatewayMappingStrategies"
gateway {
http {
hostname = ""
hostname = ${?HOSTNAME}
hostname = ${?BIND_HOSTNAME}
port = 8080
port = ${?HTTP_PORT}
port = ${?PORT}
coordinated-shutdown-timeout = 65s
coordinated-shutdown-timeout = ${?COORDINATED_SHUTDOWN_REQUEST_TIMEOUT}
schema-versions = [2]
protocol-headers = ["X-Forwarded-Proto", "x_forwarded_proto"]
forcehttps = false
forcehttps = ${?FORCE_HTTPS}
redirect-to-https = false
redirect-to-https = ${?REDIRECT_TO_HTTPS}
redirect-to-https-blocklist-pattern = "/api.*|/ws.*|/status.*|/overall.*"
enablecors = false
enablecors = ${?ENABLE_CORS}
request-timeout = 60s
request-timeout = ${?REQUEST_TIMEOUT}
additional-accepted-media-types = ${?ADDITIONAL_ACCEPTED_MEDIA_TYPES}
query-params-as-headers = [
"accept"
"channel"
"correlation-id"
"requested-acks"
"declared-acks"
"response-required"
"timeout"
"live-channel-timeout-strategy"
"allow-policy-lockout"
"condition"
"live-channel-condition"
"at-historical-revision"
"at-historical-timestamp"
"dry-run"
]
}
streaming {
session-counter-scrape-interval = 30s
parallelism = 64
parallelism = ${?GATEWAY_STREAMING_PARALLELISM}
search-idle-timeout = 60s
search-idle-timeout = ${?GATEWAY_STREAMING_SEARCH_IDLE_TIMEOUT}
subscription-refresh-delay = 5m
subscription-refresh-delay = ${?GATEWAY_STREAMING_SUBSCRIPTION_REFRESH_DELAY}
acknowledgement {
forwarder-fallback-timeout = 65s
}
websocket {
subscriber {
backpressure-queue-size = 100
}
publisher {
backpressure-buffer-size = 200
}
throttling-rejection-factor = 1.25
throttling {
enabled = false
}
streaming-authorization-enforcer = "org.eclipse.ditto.gateway.service.streaming.NoOpAuthorizationEnforcer"
}
sse {
throttling {
enabled = false
}
streaming-authorization-enforcer = "org.eclipse.ditto.gateway.service.streaming.NoOpAuthorizationEnforcer"
}
}
command {
default-timeout = ${ditto.gateway.http.request-timeout}
max-timeout = 1m
smart-channel-buffer = 10s
connections-retrieve-limit = 100
}
message {
default-timeout = 10s
max-timeout = 1m
}
claim-message {
default-timeout = 1m
max-timeout = 10m
}
dns {
address = none
address = ${?DNS_SERVER}
}
authentication {
http {
proxy {
enabled = false
enabled = ${?AUTH_HTTP_PROXY_ENABLED}
hostname = ${?AUTH_HTTP_PROXY_HOST}
port = ${?AUTH_HTTP_PROXY_PORT}
username = ${?AUTH_HTTP_PROXY_USERNAME}
password = ${?AUTH_HTTP_PROXY_PASSWORD}
}
}
oauth {
protocol = "https"
protocol = ${?OAUTH_PROTOCOL}
allowed-clock-skew = 10s
allowed-clock-skew = ${?OAUTH_ALLOWED_CLOCK_SKEW}
openid-connect-issuers = {
google = {
issuer = "accounts.google.com"
}
}
token-integration-subject = "integration:{{policy-entry:label}}:{{jwt:aud}}"
token-integration-subject = ${?OAUTH_TOKEN_INTEGRATION_SUBJECT}
}
# PRE-AUTHENTICATION = open access for /api/2/
pre-authentication {
enabled = true
}
devops {
secured = false
devops-authentication-method = "basic"
password = "ditto-devops-secret"
password = ${?DEVOPS_PASSWORD}
status-secured = false
status-authentication-method = "basic"
statusPassword = "ditto-status-secret"
statusPassword = ${?STATUS_PASSWORD}
}
}
health-check {
enabled = true
enabled = ${?HEALTH_CHECK_ENABLED}
interval = 60s
interval = ${?HEALTH_CHECK_INTERVAL}
service.timeout = 10s
service.timeout = ${?HEALTH_CHECK_SERVICE_TIMEOUT}
cluster-roles = {
enabled = true
enabled = ${?HEALTH_CHECK_ROLES_ENABLED}
expected = [
"policies"
"things"
"search"
"gateway"
"connectivity"
]
}
}
public-health {
cache-timeout = 20s
cache-timeout = ${?GATEWAY_STATUS_HEALTH_EXTERNAL_TIMEOUT}
}
cloud-events {
empty-schema-allowed = true
data-types = [
"application/json"
"application/vnd.eclipse.ditto+json"
]
}
cache {
publickeys {
maxentries = 32
expiry = 60m
maximum-size = ${ditto.gateway.cache.publickeys.maxentries}
expire-after-write = ${ditto.gateway.cache.publickeys.expiry}
}
}
statistics {
ask-timeout = 5s
ask-timeout = ${?STATISTICS_UPDATE_INTERVAL}
update-interval = 15s
update-interval = ${?STATISTICS_UPDATE_INTERVAL}
details-expire-after = 3s
details-expire-after = ${?STATISTICS_DETAILS_EXPIRE_AFTER}
shards = [
{
region = "thing"
role = "things"
root = "/user/thingsRoot"
}
{
region = "policy"
role = "policies"
root = "/user/policiesRoot"
}
{
region = "search-wildcard-updater"
role = "search"
root = "/user/thingsWildcardSearchRoot/searchUpdaterRoot"
}
]
}
}
tracing {
filter = {
includes = ["**"]
excludes = ["GET /ws/2"]
}
}
}
secrets {
devops_password {
name = "devops_password"
name = ${?DEVOPS_PASSWORD_NAME}
}
status_password {
name = "status_password"
name = ${?STATUS_PASSWORD_NAME}
}
}
pekko.http.client {
user-agent-header = eclipse-ditto/${ditto.version}
}
pekko {
actor {
default-dispatcher {
executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedForkJoinExecutorServiceConfigurator"
}
deployment {
/gatewayRoot/proxy {
router = round-robin-pool
resizer {
lower-bound = 5
upper-bound = 100
messages-per-resize = 50
}
}
}
}
cluster {
sharding {
role = ${ditto.service-name}
passivation {
strategy = "off"
}
}
roles = ["gateway"]
}
coordinated-shutdown {
phases {
service-requests-done {
timeout = 70s
}
}
}
http {
server {
server-header = ""
request-timeout = ${ditto.gateway.http.request-timeout}
idle-timeout = 610s
max-connections = 4096
raw-request-uri-header = on
parsing {
max-uri-length = 8k
max-content-length = 1m
uri-parsing-mode = relaxed
}
websocket {
periodic-keep-alive-mode = ping
periodic-keep-alive-max-idle = 30s
}
termination-deadline-exceeded-response {
status = 502
}
}
host-connection-pool {
max-open-requests = 1024
idle-timeout = 60s
}
}
management.health-checks.readiness-checks {
gateway-http-readiness = "org.eclipse.ditto.gateway.service.health.GatewayHttpReadinessCheck"
}
management.health-checks.liveness-checks {
subsystem-health = "org.eclipse.ditto.internal.utils.health.SubsystemHealthCheck"
}
}
authentication-dispatcher {
type = Dispatcher
executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
thread-pool-executor {
core-pool-size-min = 4
core-pool-size-factor = 2.0
core-pool-size-max = 8
}
throughput = 100
}
signal-enrichment-cache-dispatcher {
type = Dispatcher
executor = "org.eclipse.ditto.internal.utils.metrics.service.executor.InstrumentedThreadPoolExecutorServiceConfigurator"
}

View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
Modify the reference.conf inside the Ditto gateway JAR.
Inserts ditto { gateway { authentication { pre-authentication { enabled = true } } } }
at the root level, before the final closing brace.
"""
import zipfile
import shutil
import os
import subprocess
import sys
JAR_PATH = "/tmp/ditto-jar-mod/ditto-gateway-service-3.8.12-allinone.jar"
AUTH_BLOCK = """
### Custom Ditto auth override - pre-authentication enabled
ditto {
gateway {
authentication {
pre-authentication {
enabled = true
}
devops {
secured = false
devops-authentication-method = "basic"
password = "ditto-devops-secret"
status-secured = false
status-authentication-method = "basic"
statusPassword = "ditto-status-secret"
}
}
}
}
"""
def main():
os.makedirs("/tmp/ditto-jar-mod", exist_ok=True)
print("=== Step 1: Extracting JAR ===")
result = subprocess.run(
["docker", "run", "--rm", "eclipse/ditto-gateway:latest",
"cat", "/opt/ditto/ditto-gateway-service-3.8.12-allinone.jar"],
capture_output=True, check=True
)
with open(JAR_PATH, "wb") as f:
f.write(result.stdout)
print(f"JAR: {len(result.stdout)} bytes")
print("=== Step 2: Modifying reference.conf ===")
with zipfile.ZipFile(JAR_PATH, 'r') as zin:
ref_conf = zin.read("reference.conf").decode("utf-8")
lines = ref_conf.split('\n')
total_lines = len(lines)
print(f"reference.conf: {total_lines} lines")
# Find the LAST closing brace at root level (depth 0)
# Track depth through the entire file
depth = 0
last_root_close_idx = None
for i, line in enumerate(lines):
stripped = line.strip()
if not stripped or stripped.startswith('#'):
continue
# Count braces (simple approach - count { and })
# This isn't perfect for HOCON but works for our case
for ch in stripped:
if ch == '{':
depth += 1
elif ch == '}':
depth -= 1
if depth == 0:
last_root_close_idx = i
if last_root_close_idx is None:
print("ERROR: Could not find root-level closing brace!")
sys.exit(1)
insert_idx = last_root_close_idx
print(f"Last root-level '}}' at line {insert_idx + 1}: '{lines[insert_idx].strip()}'")
# Check if we need a comma before our block
# Look at the non-empty line before insert_idx
prev_idx = insert_idx - 1
while prev_idx >= 0 and lines[prev_idx].strip() == '':
prev_idx -= 1
if prev_idx >= 0:
prev_stripped = lines[prev_idx].strip()
if prev_stripped.endswith('}'):
# Need to add a comma
lines[prev_idx] = lines[prev_idx].rstrip()
if not lines[prev_idx].endswith(','):
lines[prev_idx] += ','
print(f"Added comma to line {prev_idx + 1}")
# Insert our block
auth_lines = AUTH_BLOCK.split('\n')
new_lines = lines[:insert_idx] + auth_lines + lines[insert_idx:]
modified_conf = '\n'.join(new_lines)
print(f"Modified: {total_lines} -> {len(new_lines)} lines")
# Verify brace balance
depth = 0
for i, line in enumerate(new_lines):
stripped = line.strip()
if not stripped or stripped.startswith('#'):
continue
for ch in stripped:
if ch == '{':
depth += 1
elif ch == '}':
depth -= 1
if depth < 0:
print(f"ERROR: Negative depth at line {i+1}")
sys.exit(1)
print(f"Brace depth check: {depth} (should be 0)")
if depth != 0:
print("ERROR: Unbalanced braces!")
sys.exit(1)
# Verify ditto is at root level
for i, line in enumerate(new_lines):
if line.strip() == 'ditto {':
indent = len(line) - len(line.lstrip())
print(f"'ditto {{' at line {i+1}, indent: {indent}")
if indent != 0:
print(f"WARNING: Expected indent 0, got {indent}")
break
print("=== Step 3: Creating unsigned JAR ===")
skip_files = set()
with zipfile.ZipFile(JAR_PATH, 'r') as zin:
for name in zin.namelist():
if name.startswith("META-INF/"):
upper = name.upper()
if upper.endswith(".SF") or upper.endswith(".RSA") or upper.endswith(".DSA") or upper == "MANIFEST.MF":
skip_files.add(name)
with zipfile.ZipFile(JAR_PATH + ".new", 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
if item.filename in skip_files:
continue
data = zin.read(item.filename)
if item.filename == "reference.conf":
data = modified_conf.encode("utf-8")
info = zipfile.ZipInfo(filename=item.filename, date_time=item.date_time)
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = item.external_attr
zout.writestr(info, data)
shutil.move(JAR_PATH + ".new", JAR_PATH)
print("=== Step 4: Verifying ===")
with zipfile.ZipFile(JAR_PATH, 'r') as z:
ref = z.read("reference.conf").decode("utf-8")
assert "pre-authentication" in ref
sig = [n for n in z.namelist() if n.startswith("META-INF/") and n.upper().endswith(('.SF', '.RSA', '.DSA'))]
print(f"OK Signature files: {len(sig)}")
print(f"OK JAR size: {os.path.getsize(JAR_PATH)}")
print("\n=== DONE ===")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,70 @@
#!/bin/bash
# Minimal JWT/OAuth2 server for Ditto
# Serves a JWKS endpoint and validates tokens signed with DITTO_JWT_SECRET
cat > /tmp/oauth2-server.js << 'EOF'
const http = require('http');
const crypto = require('crypto');
const SECRET = process.env.DITTO_JWT_SECRET || 'my-ditto-jwt-secret-key-12345';
const PORT = 3000;
function base64url(buf) {
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
// Generate a token
function generateToken(sub, scope) {
const header = { alg: 'RS256', typ: 'JWT', kid: 'ditto-local' };
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: 'http://localhost:' + PORT,
sub: sub,
aud: 'ditto:cognito',
iat: now,
exp: now + 3600,
scope: scope
};
const h = base64url(Buffer.from(JSON.stringify(header)));
const p = base64url(Buffer.from(JSON.stringify(payload)));
const sig = base64url(
crypto.createHmac('sha256', SECRET).update(h + '.' + p).digest()
);
return h + '.' + p + '.' + sig;
}
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'application/json');
if (req.url === '/.well-known/openid-configuration') {
res.end(JSON.stringify({
issuer: 'http://localhost:' + PORT,
jwks_uri: 'http://localhost:' + PORT + '/.well-known/jwks.json',
token_endpoint: 'http://localhost:' + PORT + '/token'
}));
} else if (req.url === '/.well-known/jwks.json') {
// Extract public key from secret (for HS256 we just return the secret as k)
const jwk = {
kty: 'oct',
kid: 'ditto-local',
use: 'sig',
alg: 'HS256',
k: base64url(Buffer.from(SECRET))
};
res.end(JSON.stringify({ keys: [jwk] }));
} else if (req.url === '/token') {
const token = generateToken('ditto', 'READ_WRITE');
res.end(JSON.stringify({ access_token: token, token_type: 'Bearer', expires_in: 3600 }));
} else {
res.statusCode = 404;
res.end('{}');
}
});
server.listen(PORT, '0.0.0.0', () => {
console.log('OAuth2 server listening on port ' + PORT);
console.log('Token: ' + generateToken('ditto', scope='READ_WRITE'));
});
EOF
node /tmp/oauth2-server.js

View File

@@ -0,0 +1,14 @@
ditto {
gateway {
authentication {
pre-authentication {
enabled = true
}
devops {
secured = false
devops-authentication-method = "basic"
password = "ditto-devops-secret"
}
}
}
}

25
ditto-things.yml Normal file
View File

@@ -0,0 +1,25 @@
services:
ditto-things:
image: eclipse/ditto-things:latest
container_name: smart-city-ditto-things
restart: unless-stopped
hostname: ditto-things
environment:
- TZ=Europe/Berlin
- BIND_HOSTNAME=0.0.0.0
- DITTO_JWT_SECRET=my-ditto-jwt-secret-key-12345
- MONGO_HOST=smart-city-ditto-mongodb
- MONGO_PORT=27017
- MONGO_DB=Things
- MONGODB_URI=mongodb://smart-city-ditto-mongodb:27017/Things
- AKKA_REMOTE_ENABLED=false
- JAVA_TOOL_OPTIONS=-Dditto.things.authentication.devops.password=ditto-devops-secret
networks:
traefik-public:
aliases:
- ditto-cluster
- ditto-things
networks:
traefik-public:
external: true

View File

@@ -68,7 +68,7 @@ services:
- "traefik.http.services.ditto-things.loadbalancer.server.port=8080"
ditto-gateway:
image: eclipse/ditto-gateway:latest
image: eclipse/ditto-gateway:custom
container_name: smart-city-ditto-gateway
restart: unless-stopped
hostname: ditto-gateway
@@ -85,19 +85,11 @@ services:
- DITTO_GW_MQTT_BROKER=smart-city-mosquitto:1883
- DITTO_GW_MQTT_TOPIC_FILTER=smartcity/#
- DEVOPS_PASSWORD=ditto-devops-secret
- ENABLE_PRE_AUTHENTICATION=true
- JAVA_TOOL_OPTIONS=-Dditto.gateway.authentication.devops.password=ditto-devops-secret -Dditto.gateway.authentication.devops.secured=true -Dditto.gateway.authentication.devops.devops-authentication-method=basic
networks:
traefik-public:
aliases:
- ditto-cluster
- ditto-gateway
labels:
- "traefik.enable=true"
- "traefik.http.routers.ditto-gateway.rule=Host(`ditto.digitribe.fr`)"
- "traefik.http.routers.ditto-gateway.entrypoints=websecure"
- "traefik.http.routers.ditto-gateway.tls.certresolver=letsencrypt"
- "traefik.http.services.ditto-gateway.loadbalancer.server.port=8080"
networks:
traefik-public:

48
flink/docker-compose.yml Normal file
View File

@@ -0,0 +1,48 @@
# Smart City Digital Twin Martinique — Apache Flink
# Usage: docker compose -f flink/docker-compose.yml up -d
# Image officielle Apache Flink 1.20.1 avec digest vérifié
networks:
smartcity-shared:
external: true
services:
jobmanager:
image: apache/flink:1.20.1-scala_2.12-java17@sha256:ecc5785594eff2d94e29e6b116b3124c0cdb3a9c952ebdf38ef0fef90fb9913d
container_name: flink-jobmanager
command: jobmanager
networks:
- smartcity-shared
ports:
- "8081:8081" # Flink Web UI
environment:
- |
FLINK_PROPERTIES=
jobmanager.rpc.address: jobmanager
jobmanager.memory.process.size: 1024m
taskmanager.memory.process.size: 1024m
taskmanager.numberOfTaskSlots: 4
parallelism.default: 2
rest.port: 8081
restart: unless-stopped
labels:
- "traefik.enable=false"
taskmanager:
image: apache/flink:1.20.1-scala_2.12-java17@sha256:ecc5785594eff2d94e29e6b116b3124c0cdb3a9c952ebdf38ef0fef90fb9913d
container_name: flink-taskmanager
command: taskmanager
networks:
- smartcity-shared
depends_on:
- jobmanager
environment:
- |
FLINK_PROPERTIES=
jobmanager.rpc.address: jobmanager
taskmanager.memory.process.size: 1024m
taskmanager.numberOfTaskSlots: 4
parallelism.default: 2
restart: unless-stopped
labels:
- "traefik.enable=false"

View File

@@ -3,7 +3,7 @@ FROM jupyterhub/jupyterhub:5.3.0
USER root
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y --no-install-recommends git sudo && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir \
git+https://github.com/jupyterhub/nativeauthenticator.git@main \
@@ -12,9 +12,17 @@ RUN pip install --no-cache-dir \
jupyterlab \
notebook
# Create eric user with password-less sudo
RUN useradd -m -s /bin/bash -u 1000 eric && \
echo "eric ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \
chown -R eric:eric /home/eric
# Create the directory for JupyterHub data
RUN mkdir -p /srv/jupyterhub && chown -R 1000:1000 /srv/jupyterhub
ARG BUILD_DATE=unknown
RUN echo "Build date: ${BUILD_DATE}" > /tmp/build-info.txt
COPY jupyterhub_config.py /srv/jupyterhub/jupyterhub_config.py
WORKDIR /srv/jupyterhub

View File

@@ -1,4 +1,8 @@
# JupyterHub configuration for Smart City VRE
# Uses NativeAuthenticator with LocalProcessSpawner
# Build: 2026-06-01-0915
import sys as _sys
c.JupyterHub.ip = '0.0.0.0'
c.JupyterHub.port = 8000
@@ -9,14 +13,14 @@ c.JupyterHub.authenticator_class = 'nativeauthenticator.NativeAuthenticator'
c.Authenticator.admin_users = {'admin'}
c.Authenticator.allow_all = True
# Spawner — use SimpleLocalProcessSpawner (default in JupyterHub 5.x)
# This spawner runs singleuser servers as subprocesses
c.JupyterHub.spawner_class = 'jupyterhub.spawner.SimpleLocalProcessSpawner'
# Spawner: Simple local spawner (single-node, shared environment)
c.JupyterHub.spawner_class = 'simple'
c.Spawner.cmd = ['jupyterhub-singleuser']
c.Spawner.default_url = '/lab'
c.Spawner.http_timeout = 300
c.Spawner.start_timeout = 300
c.Spawner.http_timeout = 120
# Database and cookies - use absolute paths
# Database and cookies - use absolute path (4 slashes!)
c.JupyterHub.cookie_secret_file = '/srv/jupyterhub/jupyterhub_cookie_secret'
c.JupyterHub.db_url = 'sqlite:////srv/jupyterhub/jupyterhub.sqlite'