diff --git a/.agents/agent_assets_metadata.toml b/.agents/agent_assets_metadata.toml new file mode 100644 index 0000000..34fa4de --- /dev/null +++ b/.agents/agent_assets_metadata.toml @@ -0,0 +1,8 @@ +uploads = [] +generated = [] + +[[outputs]] +id = "x31gBYdQKnMGZriV7IJ1v" +uri = "file://DEPLOIEMENT.md" +type = "text" +title = "Guide d'auto-hébergement RockyLinux" diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b99c222 --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# ─── Base de données PostgreSQL ────────────────────────────────────────────── +# Format : postgresql://utilisateur:motdepasse@hote:port/nomdb +DATABASE_URL=postgresql://voixdupeuple:CHANGEME@localhost:5432/voixdupeuple + +# ─── Clé API OpenAI ────────────────────────────────────────────────────────── +# Obtenez votre clé sur https://platform.openai.com/api-keys +OPENAI_API_KEY=sk-... + +# ─── (Optionnel) Proxy OpenAI compatible ───────────────────────────────────── +# Décommentez si vous utilisez un proxy (Ollama avec OpenAI compat, Azure, etc.) +# OPENAI_BASE_URL=https://votre-proxy.example.com/v1 + +# ─── Modèles IA (optionnel — valeurs par défaut recommandées) ──────────────── +# OPENAI_FILTER_MODEL=gpt-4o-mini # Modèle de filtrage (rapide, économique) +# OPENAI_SYNTHESIS_MODEL=gpt-4o # Modèle de synthèse (haute qualité) + +# ─── Flask ──────────────────────────────────────────────────────────────────── +FLASK_ENV=production +PORT=8000 + +# ─── Session (générez avec: python3 -c "import secrets; print(secrets.token_hex(32))") ─── +SESSION_SECRET=CHANGEZ_CE_SECRET_AVEC_UNE_VALEUR_ALEATOIRE_LONGUE diff --git a/.gitignore b/.gitignore index 12bc7fa..5eac9a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,49 +1,55 @@ -# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files. +# Dépendances +node_modules/ +.pythonlibs/ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +pip-log.txt -# compiled output -dist -tmp -out-tsc +# Build +dist/ +tmp/ +out-tsc/ *.tsbuildinfo -.expo -.expo-shared +build/ -# dependencies -node_modules +# Environnement +.env +.env.local +.env.*.local -# IDEs and editors -/.idea -.project -.classpath -.c9/ -*.launch -.settings/ -*.sublime-workspace +# Cache Replit +.cache/ +.local/ +.upm/ +uv.lock -# IDE - VSCode +# Expo / mobile +.expo/ +.expo-shared/ + +# Logs +*.log +logs/ + +# IDEs +.idea/ .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +*.sublime-workspace +.project +.classpath +.c9/ +*.launch +.settings/ -# misc -/.sass-cache -/connect.lock -/coverage -/libpeerconnection.log -npm-debug.log -yarn-error.log -testem.log -/typings - -# System Files +# Système .DS_Store Thumbs.db - -.cursor/rules/nx-rules.mdc -.github/instructions/nx.instructions.md - -# Replit -.cache/ -.local/ +*.sass-cache +coverage/ diff --git a/DEPLOIEMENT.md b/DEPLOIEMENT.md new file mode 100644 index 0000000..976b9b4 --- /dev/null +++ b/DEPLOIEMENT.md @@ -0,0 +1,424 @@ +# Guide d'auto-hébergement — La Voix du Peuple +## RockyLinux 9 (ou RHEL 9, AlmaLinux 9) + +--- + +## Architecture + +``` +Internet + │ + ▼ +[Nginx] ─── /api/* ──► [Gunicorn + Flask] ──► [PostgreSQL] + │ │ + └─── /* ──► [React SPA] └──► [OpenAI API] + (fichiers statiques) +``` + +- **Frontend** : React + Vite, servi comme fichiers statiques par Nginx +- **Backend** : Flask + Gunicorn (4 workers), accessible uniquement via Nginx +- **Base de données** : PostgreSQL 15+ +- **IA** : API OpenAI (clé standard, pas de proxy Replit) + +--- + +## Prérequis + +```bash +# Mettre à jour le système +sudo dnf update -y + +# Outils de base +sudo dnf install -y git curl wget epel-release +``` + +--- + +## 1. Installer les dépendances système + +### Python 3.11 +```bash +sudo dnf install -y python3.11 python3.11-pip python3.11-devel +python3.11 --version # doit afficher Python 3.11.x +``` + +### Node.js 20 (LTS) +```bash +curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - +sudo dnf install -y nodejs +node --version # doit afficher v20.x.x +``` + +### pnpm +```bash +npm install -g pnpm +pnpm --version +``` + +### PostgreSQL 15 +```bash +sudo dnf install -y postgresql15-server postgresql15 +sudo /usr/pgsql-15/bin/postgresql-15-setup initdb +sudo systemctl enable --now postgresql-15 +``` + +### Nginx +```bash +sudo dnf install -y nginx +sudo systemctl enable nginx +``` + +--- + +## 2. Configurer PostgreSQL + +```bash +# Connexion en tant que postgres +sudo -u postgres psql + +-- Dans psql : +CREATE USER voixdupeuple WITH PASSWORD 'CHANGEME_MOT_DE_PASSE_FORT'; +CREATE DATABASE voixdupeuple OWNER voixdupeuple; +GRANT ALL PRIVILEGES ON DATABASE voixdupeuple TO voixdupeuple; +\q +``` + +Éditez `/var/lib/pgsql/15/data/pg_hba.conf` pour autoriser la connexion locale par mot de passe : + +``` +# Remplacez "ident" par "md5" sur la ligne 127.0.0.1/32 : +host all all 127.0.0.1/32 md5 +``` + +```bash +sudo systemctl restart postgresql-15 + +# Test de connexion +psql -U voixdupeuple -h 127.0.0.1 -d voixdupeuple -c "\dt" +``` + +--- + +## 3. Créer l'utilisateur système + +```bash +sudo useradd -r -s /bin/bash -d /opt/voix-du-peuple voixdupeuple +sudo mkdir -p /opt/voix-du-peuple +sudo chown voixdupeuple:voixdupeuple /opt/voix-du-peuple +``` + +--- + +## 4. Cloner le dépôt depuis Gitea + +```bash +sudo -u voixdupeuple bash -c " + cd /opt + git clone https://votre-gitea.example.com/utilisateur/voix-du-peuple.git voix-du-peuple +" +``` + +--- + +## 5. Configurer les variables d'environnement + +```bash +sudo -u voixdupeuple bash -c " + cp /opt/voix-du-peuple/.env.example /opt/voix-du-peuple/.env +" + +# Éditer le fichier .env +sudo nano /opt/voix-du-peuple/.env +``` + +Remplissez au minimum : + +```env +DATABASE_URL=postgresql://voixdupeuple:CHANGEME_MOT_DE_PASSE_FORT@127.0.0.1:5432/voixdupeuple +OPENAI_API_KEY=sk-VOTRE_CLE_OPENAI +SESSION_SECRET=GENEREZ_UNE_VALEUR_ALEATOIRE_ICI +PORT=8000 +FLASK_ENV=production +``` + +Générez le `SESSION_SECRET` : +```bash +python3.11 -c "import secrets; print(secrets.token_hex(32))" +``` + +Sécurisez le fichier : +```bash +sudo chmod 600 /opt/voix-du-peuple/.env +sudo chown voixdupeuple:voixdupeuple /opt/voix-du-peuple/.env +``` + +--- + +## 6. Installer les dépendances Python + +```bash +sudo -u voixdupeuple bash -c " + cd /opt/voix-du-peuple + python3.11 -m venv .venv + .venv/bin/pip install --upgrade pip + .venv/bin/pip install -r artifacts/flask-api/requirements.txt +" +``` + +--- + +## 7. Initialiser la base de données + +```bash +sudo -u voixdupeuple bash -c " + cd /opt/voix-du-peuple/artifacts/flask-api + source /opt/voix-du-peuple/.env + export DATABASE_URL OPENAI_API_KEY SESSION_SECRET FLASK_ENV PORT + /opt/voix-du-peuple/.venv/bin/python3 -c ' +from database import init_db +init_db() +print(\"Base de données initialisée.\") +' +" +``` + +--- + +## 8. Construire le frontend React + +```bash +sudo -u voixdupeuple bash -c " + cd /opt/voix-du-peuple + pnpm install --frozen-lockfile + + cd artifacts/voix-du-peuple + pnpm exec vite build --config vite.config.selfhost.ts +" +``` + +Les fichiers statiques sont générés dans `artifacts/voix-du-peuple/dist/public/`. + +--- + +## 9. Configurer le service systemd (Gunicorn) + +```bash +# Créer le répertoire de logs +sudo mkdir -p /var/log/voix-du-peuple +sudo chown voixdupeuple:voixdupeuple /var/log/voix-du-peuple + +# Copier le fichier de service +sudo cp /opt/voix-du-peuple/deploy/voix-du-peuple-api.service \ + /etc/systemd/system/voix-du-peuple-api.service + +# Activer et démarrer +sudo systemctl daemon-reload +sudo systemctl enable --now voix-du-peuple-api + +# Vérifier +sudo systemctl status voix-du-peuple-api +``` + +Test de l'API : +```bash +curl http://127.0.0.1:8000/api/healthz +# {"status": "ok"} +``` + +--- + +## 10. Configurer Nginx + +```bash +# Copier la config Nginx +sudo cp /opt/voix-du-peuple/deploy/nginx.conf \ + /etc/nginx/conf.d/voix-du-peuple.conf + +# Éditez le fichier pour mettre votre nom de domaine +sudo nano /etc/nginx/conf.d/voix-du-peuple.conf +# Remplacez voix-du-peuple.example.com par votre domaine + +# Tester la configuration +sudo nginx -t + +# Recharger Nginx +sudo systemctl reload nginx +``` + +--- + +## 11. Ouvrir le pare-feu + +```bash +sudo firewall-cmd --permanent --add-service=http +sudo firewall-cmd --permanent --add-service=https +sudo firewall-cmd --reload +``` + +SELinux — autoriser Nginx à se connecter à Gunicorn : +```bash +sudo setsebool -P httpd_can_network_connect 1 +``` + +--- + +## 12. (Recommandé) HTTPS avec Let's Encrypt + +```bash +sudo dnf install -y certbot python3-certbot-nginx + +sudo certbot --nginx -d voix-du-peuple.example.com \ + --email admin@example.com --agree-tos --no-eff-email + +# Renouvellement automatique (déjà configuré par certbot) +sudo systemctl status certbot-renew.timer +``` + +Puis décommentez le bloc HTTPS dans `/etc/nginx/conf.d/voix-du-peuple.conf`. + +--- + +## Structure du projet + +``` +voix-du-peuple/ +├── artifacts/ +│ ├── flask-api/ # Backend Python Flask +│ │ ├── app.py # Application principale + routes +│ │ ├── ai_agent.py # Agent IA (filtrage + synthèse) +│ │ ├── legal_framework.py # Prompts ancrés dans le droit international +│ │ ├── database.py # Accès PostgreSQL (psycopg2) +│ │ ├── requirements.txt # Dépendances Python +│ │ └── start.sh # Démarrage développement +│ └── voix-du-peuple/ # Frontend React + Vite +│ ├── src/ +│ │ ├── pages/ # home.tsx, about.tsx +│ │ ├── components/ # Composants UI (shadcn/ui + radix) +│ │ └── App.tsx # Routing principal +│ ├── vite.config.ts # Config Replit (développement) +│ └── vite.config.selfhost.ts # Config auto-hébergement (production) +├── lib/ +│ ├── api-spec/ # Spécification OpenAPI +│ ├── api-client-react/ # Hooks React Query générés +│ └── db/ # Schéma Drizzle (référence) +├── deploy/ +│ ├── voix-du-peuple-api.service # Service systemd Gunicorn +│ └── nginx.conf # Configuration Nginx +├── .env.example # Variables d'environnement (modèle) +└── DEPLOIEMENT.md # Ce fichier +``` + +--- + +## Variables d'environnement complètes + +| Variable | Obligatoire | Description | +|----------|-------------|-------------| +| `DATABASE_URL` | Oui | URL PostgreSQL complète | +| `OPENAI_API_KEY` | Oui | Clé API OpenAI (sk-...) | +| `OPENAI_BASE_URL` | Non | Proxy OpenAI compatible (Ollama, Azure, etc.) | +| `OPENAI_FILTER_MODEL` | Non | Modèle de filtrage (défaut : `gpt-4o-mini`) | +| `OPENAI_SYNTHESIS_MODEL` | Non | Modèle de synthèse (défaut : `gpt-4o`) | +| `SESSION_SECRET` | Oui | Clé secrète Flask (min 32 caractères aléatoires) | +| `PORT` | Non | Port Gunicorn (défaut : 8000) | +| `FLASK_ENV` | Non | `production` ou `development` | + +--- + +## Modèles IA utilisés + +| Fonction | Modèle par défaut | Configurable dans | +|----------|-------------------|-------------------| +| Filtrage des idées | `gpt-4o-mini` | `ai_agent.py` ligne 56 | +| Synthèse collective | `gpt-4o` | `ai_agent.py` ligne 104 | + +> **Note :** Les modèles `gpt-5-mini` et `gpt-5.2` sont les noms utilisés sur la plateforme Replit. En auto-hébergement avec l'API OpenAI standard, utilisez `gpt-4o-mini` et `gpt-4o`. + +Pour changer les modèles, éditez `artifacts/flask-api/ai_agent.py` : +```python +# Filtrage (ligne ~56) +model="gpt-4o-mini", # ou tout modèle OpenAI compatible + +# Synthèse (ligne ~104) +model="gpt-4o", # ou tout modèle OpenAI compatible +``` + +--- + +## Mise à jour + +```bash +sudo -u voixdupeuple bash -c " + cd /opt/voix-du-peuple + git pull origin main + + # Mise à jour des dépendances Python si nécessaire + .venv/bin/pip install -r artifacts/flask-api/requirements.txt + + # Mise à jour des dépendances Node et rebuild frontend + pnpm install --frozen-lockfile + cd artifacts/voix-du-peuple + pnpm exec vite build --config vite.config.selfhost.ts +" + +# Redémarrer le service +sudo systemctl restart voix-du-peuple-api +sudo systemctl reload nginx +``` + +--- + +## Logs et supervision + +```bash +# Logs API Flask +sudo journalctl -u voix-du-peuple-api -f +tail -f /var/log/voix-du-peuple/api-error.log +tail -f /var/log/voix-du-peuple/api-access.log + +# Logs Nginx +sudo tail -f /var/log/nginx/error.log +sudo tail -f /var/log/nginx/access.log + +# Statut des services +sudo systemctl status voix-du-peuple-api +sudo systemctl status nginx +sudo systemctl status postgresql-15 +``` + +--- + +## Dépannage fréquent + +### L'API répond 502 Bad Gateway +```bash +# Vérifier que Gunicorn tourne +sudo systemctl status voix-du-peuple-api +# Vérifier SELinux +sudo setsebool -P httpd_can_network_connect 1 +``` + +### Erreur "could not connect to server" (PostgreSQL) +```bash +# Vérifier que PostgreSQL tourne +sudo systemctl status postgresql-15 +# Vérifier l'URL de connexion dans .env +psql -U voixdupeuple -h 127.0.0.1 -d voixdupeuple +``` + +### Erreur OpenAI API +```bash +# Vérifier la clé dans .env +grep OPENAI_API_KEY /opt/voix-du-peuple/.env +# Tester directement +curl https://api.openai.com/v1/models \ + -H "Authorization: Bearer $OPENAI_API_KEY" +``` + +### Le frontend affiche une page blanche +```bash +# Vérifier que le build existe +ls /opt/voix-du-peuple/artifacts/voix-du-peuple/dist/public/ +# Vérifier les droits +sudo chown -R voixdupeuple:voixdupeuple /opt/voix-du-peuple/artifacts/voix-du-peuple/dist/ +``` diff --git a/artifacts/flask-api/ai_agent.py b/artifacts/flask-api/ai_agent.py index a2b56c2..4401ef2 100644 --- a/artifacts/flask-api/ai_agent.py +++ b/artifacts/flask-api/ai_agent.py @@ -14,16 +14,31 @@ _client: OpenAI | None = None def get_client() -> OpenAI: + """ + Supporte deux modes : + - Replit AI Integration : AI_INTEGRATIONS_OPENAI_BASE_URL + AI_INTEGRATIONS_OPENAI_API_KEY + - Auto-hébergement standard : OPENAI_API_KEY (+ OPENAI_BASE_URL optionnel pour proxy) + """ global _client if _client is None: - base_url = os.environ.get("AI_INTEGRATIONS_OPENAI_BASE_URL") - api_key = os.environ.get("AI_INTEGRATIONS_OPENAI_API_KEY") - if not base_url or not api_key: + replit_base = os.environ.get("AI_INTEGRATIONS_OPENAI_BASE_URL") + replit_key = os.environ.get("AI_INTEGRATIONS_OPENAI_API_KEY") + std_key = os.environ.get("OPENAI_API_KEY") + std_base = os.environ.get("OPENAI_BASE_URL") + + if replit_base and replit_key: + _client = OpenAI(base_url=replit_base, api_key=replit_key) + elif std_key: + kwargs = {"api_key": std_key} + if std_base: + kwargs["base_url"] = std_base + _client = OpenAI(**kwargs) + else: raise RuntimeError( - "AI_INTEGRATIONS_OPENAI_BASE_URL et AI_INTEGRATIONS_OPENAI_API_KEY " - "sont requis. Configurez les intégrations Replit AI." + "Aucune clé IA configurée. " + "Définissez OPENAI_API_KEY dans le fichier .env " + "ou configurez les intégrations Replit AI." ) - _client = OpenAI(base_url=base_url, api_key=api_key) return _client @@ -34,8 +49,9 @@ def filter_idea(content: str) -> dict: """ try: client = get_client() + filter_model = os.environ.get("OPENAI_FILTER_MODEL", "gpt-4o-mini") response = client.chat.completions.create( - model="gpt-5-mini", + model=filter_model, max_completion_tokens=300, response_format={"type": "json_object"}, messages=[ @@ -96,9 +112,10 @@ def synthesize_ideas(ideas: list[str]) -> str: ) try: client = get_client() + synthesis_model = os.environ.get("OPENAI_SYNTHESIS_MODEL", "gpt-4o") ideas_text = "\n".join(f"{i + 1}. {idea}" for i, idea in enumerate(ideas)) response = client.chat.completions.create( - model="gpt-5.2", + model=synthesis_model, max_completion_tokens=1200, messages=[ {"role": "system", "content": SYNTHESIS_PROMPT}, diff --git a/artifacts/flask-api/requirements.txt b/artifacts/flask-api/requirements.txt index 24b2b10..a078b90 100644 --- a/artifacts/flask-api/requirements.txt +++ b/artifacts/flask-api/requirements.txt @@ -1,8 +1,8 @@ -flask==3.1.1 -flask-cors==5.0.1 -flask-limiter==3.9.4 -openai==1.77.0 -psycopg2-binary==2.9.10 -python-dotenv==1.0.1 -bleach==6.2.0 -validators==0.34.0 +bleach>=6.2.0 +flask>=3.1.1 +flask-cors>=5.0.1 +flask-limiter>=3.9.4 +gunicorn>=23.0.0 +openai>=1.77.0 +psycopg2-binary>=2.9.10 +python-dotenv>=1.0.1 diff --git a/artifacts/voix-du-peuple/vite.config.selfhost.ts b/artifacts/voix-du-peuple/vite.config.selfhost.ts new file mode 100644 index 0000000..3ad06db --- /dev/null +++ b/artifacts/voix-du-peuple/vite.config.selfhost.ts @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import path from "path"; + +export default defineConfig({ + base: "/", + plugins: [ + react(), + tailwindcss(), + ], + resolve: { + alias: { + "@": path.resolve(import.meta.dirname, "src"), + }, + dedupe: ["react", "react-dom"], + }, + root: path.resolve(import.meta.dirname), + build: { + outDir: path.resolve(import.meta.dirname, "dist/public"), + emptyOutDir: true, + }, +}); diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..b2c6f2c --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,75 @@ +server { + listen 80; + server_name voix-du-peuple.example.com; + + # Redirection HTTPS (décommentez après avoir configuré Certbot) + # return 301 https://$host$request_uri; + + root /opt/voix-du-peuple/artifacts/voix-du-peuple/dist/public; + index index.html; + + # Sécurité + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # API Flask — proxy vers Gunicorn + location /api/ { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + } + + # Frontend React — Single Page Application + location / { + try_files $uri $uri/ /index.html; + } + + # Assets statiques — cache long + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} + +# HTTPS — décommentez et adaptez après avoir obtenu votre certificat SSL +# server { +# listen 443 ssl http2; +# server_name voix-du-peuple.example.com; +# +# ssl_certificate /etc/letsencrypt/live/voix-du-peuple.example.com/fullchain.pem; +# ssl_certificate_key /etc/letsencrypt/live/voix-du-peuple.example.com/privkey.pem; +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers HIGH:!aNULL:!MD5; +# +# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +# add_header X-Content-Type-Options "nosniff" always; +# add_header X-Frame-Options "DENY" always; +# add_header X-XSS-Protection "1; mode=block" always; +# add_header Referrer-Policy "strict-origin-when-cross-origin" always; +# +# root /opt/voix-du-peuple/artifacts/voix-du-peuple/dist/public; +# index index.html; +# +# location /api/ { +# proxy_pass http://127.0.0.1:8000; +# proxy_set_header Host $host; +# proxy_set_header X-Real-IP $remote_addr; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header X-Forwarded-Proto $scheme; +# proxy_read_timeout 60s; +# } +# +# location / { +# try_files $uri $uri/ /index.html; +# } +# +# location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { +# expires 1y; +# add_header Cache-Control "public, immutable"; +# } +# } diff --git a/deploy/voix-du-peuple-api.service b/deploy/voix-du-peuple-api.service new file mode 100644 index 0000000..b3f35a0 --- /dev/null +++ b/deploy/voix-du-peuple-api.service @@ -0,0 +1,29 @@ +[Unit] +Description=La Voix du Peuple — API Flask +After=network.target postgresql.service +Requires=postgresql.service + +[Service] +Type=exec +User=voixdupeuple +Group=voixdupeuple +WorkingDirectory=/opt/voix-du-peuple/artifacts/flask-api +EnvironmentFile=/opt/voix-du-peuple/.env +ExecStart=/opt/voix-du-peuple/.venv/bin/gunicorn \ + --workers 4 \ + --bind 127.0.0.1:8000 \ + --access-logfile /var/log/voix-du-peuple/api-access.log \ + --error-logfile /var/log/voix-du-peuple/api-error.log \ + --timeout 60 \ + app:app +Restart=always +RestartSec=5 + +# Sécurité +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ReadWritePaths=/var/log/voix-du-peuple + +[Install] +WantedBy=multi-user.target