Renommer l'application en app

This commit is contained in:
Daniel Allaire 2026-06-01 09:16:37 -04:00
parent 7fc75eacde
commit ddb391c673
33 changed files with 166 additions and 83 deletions

View file

@ -6,7 +6,7 @@
tasks:
- name: Lancer le script de sauvegarde
ansible.builtin.command: /etc/cron.daily/groupe-meditation-backup
ansible.builtin.command: /etc/cron.daily/app-backup
changed_when: true
- name: Lister les dernieres sauvegardes

View file

@ -3,16 +3,16 @@
groupe_meditation_domain: app.87-16.org
# Chemins de déploiement.
groupe_meditation_app_dir: /opt/groupe-meditation
groupe_meditation_app_dir: /opt/app
groupe_meditation_backend_dir: "{{ groupe_meditation_app_dir }}/backend"
groupe_meditation_deploy_dir: "{{ groupe_meditation_app_dir }}/deploy"
groupe_meditation_data_dir: "{{ groupe_meditation_app_dir }}/data"
groupe_meditation_frontend_dir: "{{ groupe_meditation_app_dir }}/frontend"
groupe_meditation_web_root: /var/www/groupe-meditation
groupe_meditation_web_root: /var/www/app
groupe_meditation_upload_dir: "{{ groupe_meditation_app_dir }}/uploads/preuves"
# Service applicatif.
groupe_meditation_service_name: groupe-meditation
groupe_meditation_service_name: app
groupe_meditation_service_user: www-data
groupe_meditation_service_group: www-data
groupe_meditation_api_host: 127.0.0.1
@ -54,5 +54,5 @@ groupe_meditation_enable_tls: false
groupe_meditation_certbot_email: admin@example.com
# Sauvegardes.
groupe_meditation_backup_dir: /var/backups/groupe-meditation
groupe_meditation_backup_dir: /var/backups/app
groupe_meditation_backup_retention_days: 30

View file

@ -3,7 +3,7 @@ all:
children:
groupe_meditation:
hosts:
groupe-meditation-prod:
app-prod:
ansible_host: 192.168.12.46
ansible_user: ansible
ansible_ssh_private_key_file: ~/.ssh/id_ed25519_ansible_chezlepro

View file

@ -3,7 +3,7 @@
ansible.builtin.systemd:
daemon_reload: true
- name: Redemarrer groupe-meditation
- name: Redemarrer app
ansible.builtin.service:
name: "{{ groupe_meditation_service_name }}"
state: restarted

View file

@ -39,21 +39,21 @@
- "--exclude=.env"
- "--exclude=__pycache__/"
- "--exclude=*.pyc"
notify: Redemarrer groupe-meditation
notify: Redemarrer app
- name: Synchroniser les fichiers de deploiement
ansible.posix.synchronize:
src: "{{ playbook_dir }}/../deploy/"
dest: "{{ groupe_meditation_deploy_dir }}/"
delete: true
notify: Redemarrer groupe-meditation
notify: Redemarrer app
- name: Synchroniser les donnees d'import
ansible.posix.synchronize:
src: "{{ playbook_dir }}/../data/"
dest: "{{ groupe_meditation_data_dir }}/"
delete: true
notify: Redemarrer groupe-meditation
notify: Redemarrer app
- name: Synchroniser le frontend
ansible.posix.synchronize:
@ -73,7 +73,7 @@
ansible.builtin.pip:
requirements: "{{ groupe_meditation_backend_dir }}/requirements.txt"
virtualenv: "{{ groupe_meditation_backend_dir }}/venv"
notify: Redemarrer groupe-meditation
notify: Redemarrer app
- name: Deployer le fichier d'environnement backend
ansible.builtin.template:
@ -83,7 +83,7 @@
group: "{{ groupe_meditation_service_group }}"
mode: "0640"
no_log: true
notify: Redemarrer groupe-meditation
notify: Redemarrer app
- name: Installer les dependances frontend sur le poste de controle avec npm ci
ansible.builtin.command:
@ -130,16 +130,31 @@
- "{{ groupe_meditation_app_dir }}"
- "{{ groupe_meditation_web_root }}"
- name: Arreter l'ancien service applicatif si present
ansible.builtin.service:
name: groupe-meditation
state: stopped
enabled: false
failed_when: false
when: groupe_meditation_service_name != "groupe-meditation"
- name: Retirer l'ancienne unite systemd
ansible.builtin.file:
path: /etc/systemd/system/groupe-meditation.service
state: absent
when: groupe_meditation_service_name != "groupe-meditation"
notify: Recharger systemd
- name: Installer l'unite systemd
ansible.builtin.template:
src: groupe-meditation.service.j2
src: app.service.j2
dest: "/etc/systemd/system/{{ groupe_meditation_service_name }}.service"
owner: root
group: root
mode: "0644"
notify:
- Recharger systemd
- Redemarrer groupe-meditation
- Redemarrer app
- name: Compter les groupes existants
community.postgresql.postgresql_query:
@ -185,7 +200,7 @@
- groupe_meditation_run_seed | bool
- groupes_table.query_result[0].count | int == 0 or (groupes_count.query_result[0].count | default(0) | int == 0)
no_log: true
notify: Redemarrer groupe-meditation
notify: Redemarrer app
- name: Appliquer les handlers apres le seed
ansible.builtin.meta: flush_handlers

View file

@ -1,5 +1,5 @@
[Unit]
Description=Groupe Meditation API
Description=App API
After=network.target postgresql.service
Requires=postgresql.service
@ -15,4 +15,3 @@ RestartSec=5
[Install]
WantedBy=multi-user.target

View file

@ -9,9 +9,14 @@
- name: Installer la sauvegarde quotidienne
ansible.builtin.template:
src: groupe-meditation-backup.j2
dest: /etc/cron.daily/groupe-meditation-backup
src: app-backup.j2
dest: /etc/cron.daily/app-backup
owner: root
group: root
mode: "0750"
no_log: true
- name: Retirer l'ancienne sauvegarde quotidienne
ansible.builtin.file:
path: /etc/cron.daily/groupe-meditation-backup
state: absent

View file

@ -16,12 +16,13 @@ cd "{{ groupe_meditation_backend_dir }}"
"{{ groupe_meditation_backend_dir }}/venv/bin/python" "{{ groupe_meditation_deploy_dir }}/tenant_schemas.py"
"{{ groupe_meditation_backend_dir }}/venv/bin/python" "{{ groupe_meditation_deploy_dir }}/group_backup.py" all
pg_dump -U "$DB_USER" -h "$DB_HOST" -Fc "$DB_NAME" > "$BACKUP_DIR/grpmed_${DATE}.dump"
pg_dump -U "$DB_USER" -h "$DB_HOST" -Fc "$DB_NAME" > "$BACKUP_DIR/app_${DATE}.dump"
for schema in $(PGPASSWORD="$PGPASSWORD" psql -U "$DB_USER" -h "$DB_HOST" -d "$DB_NAME" -Atc "SELECT tenant_schema FROM groupes WHERE tenant_schema IS NOT NULL ORDER BY tenant_schema"); do
pg_dump -U "$DB_USER" -h "$DB_HOST" -n "$schema" -Fc "$DB_NAME" > "$BACKUP_DIR/${schema}_${DATE}.dump"
done
find "$BACKUP_DIR" -name "grpmed_*.dump" -mtime +"$RETENTION_DAYS" -delete
find "$BACKUP_DIR" -name "app_*.dump" -mtime +"$RETENTION_DAYS" -delete
find "$BACKUP_DIR" -name "group_*.json" -mtime +"$RETENTION_DAYS" -delete
find "$BACKUP_DIR" -name "grp_*.dump" -mtime +"$RETENTION_DAYS" -delete
echo "[$(date)] Sauvegarde terminee: grpmed_${DATE}.dump"
echo "[$(date)] Sauvegarde terminee: app_${DATE}.dump"

View file

@ -6,8 +6,8 @@
- name: Installer le site NGINX
ansible.builtin.template:
src: groupe-meditation.conf.j2
dest: /etc/nginx/sites-available/groupe-meditation
src: app.conf.j2
dest: /etc/nginx/sites-available/app
owner: root
group: root
mode: "0644"
@ -15,12 +15,21 @@
- name: Activer le site NGINX
ansible.builtin.file:
src: /etc/nginx/sites-available/groupe-meditation
dest: /etc/nginx/sites-enabled/groupe-meditation
src: /etc/nginx/sites-available/app
dest: /etc/nginx/sites-enabled/app
state: link
force: true
notify: Recharger nginx
- name: Desactiver l'ancien site NGINX
ansible.builtin.file:
path: "{{ item }}"
state: absent
loop:
- /etc/nginx/sites-enabled/groupe-meditation
- /etc/nginx/sites-available/groupe-meditation
notify: Recharger nginx
- name: Desactiver le site NGINX par defaut
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
@ -36,4 +45,3 @@
name: nginx
state: started
enabled: true

View file

@ -1,5 +1,5 @@
---
- name: Installer Groupe Meditation
- name: Installer App
hosts: groupe_meditation
become: true
gather_facts: true

View file

@ -1,5 +1,5 @@
---
- name: Verifier Groupe Meditation
- name: Verifier App
hosts: groupe_meditation
become: true
gather_facts: false
@ -39,4 +39,3 @@
that:
- frontend_index.stat.exists
fail_msg: "Le fichier {{ groupe_meditation_web_root }}/index.html est absent."

View file

@ -20,7 +20,7 @@ class Settings(BaseSettings):
VAPID_PUBLIC_KEY: str | None = None
VAPID_PRIVATE_KEY_B64: str | None = None
VAPID_SUBJECT: str = "mailto:admin@app.87-16.org"
GROUP_BACKUP_DIR: str = "/var/backups/groupe-meditation"
GROUP_BACKUP_DIR: str = "/var/backups/app"
class Config:
env_file = str(_ENV_FILE)

View file

@ -1,5 +1,5 @@
"""
Groupe Méditation — API Backend
App — API Backend
District 87-16, Région 87
100% logiciel libre · Hébergé au Québec
"""
@ -130,8 +130,8 @@ async def lifespan(app: FastAPI):
app = FastAPI(
title="Groupe Méditation API",
description="API du Groupe Méditation — District 87-16, Région 87",
title="App API",
description="API du App — District 87-16, Région 87",
version="0.1.0",
lifespan=lifespan,
)
@ -178,4 +178,4 @@ app.include_router(journal.router, prefix="/api")
@app.get("/api/health")
async def root_health():
return {"status": "ok", "app": "Groupe Méditation", "version": "0.1.0"}
return {"status": "ok", "app": "App", "version": "0.1.0"}

View file

@ -5,7 +5,7 @@ from sqlalchemy import func, select
from pydantic import BaseModel
from app.core.database import get_db
from app.core.security import create_token, get_current_membre, is_sysadmin, verify_pin
from app.core.security import create_token, get_current_membre, hash_pin, is_sysadmin, verify_pin
from app.services.admin_database import (
assurer_sysadmin_instance,
creer_groupe,
@ -48,6 +48,11 @@ class RestoreGroupeRequest(BaseModel):
fichier: str
class ChangePinRequest(BaseModel):
pin_actuel: str
nouveau_pin: str
async def require_sysadmin(membre=Depends(get_current_membre)):
if not is_sysadmin(membre):
raise HTTPException(status_code=403, detail="Réservé au sysadmin")
@ -75,6 +80,21 @@ async def sysadmin_login(req: SysadminLoginRequest, db: AsyncSession = Depends(g
raise HTTPException(status_code=401, detail="PIN Sysadmin invalide")
@router.post("/sysadmin-pin")
async def changer_pin_sysadmin(
req: ChangePinRequest,
sysadmin=Depends(require_sysadmin),
):
if getattr(sysadmin, "groupe_id", None) is not None:
raise HTTPException(status_code=403, detail="Réservé au sysadmin d'instance")
if not req.nouveau_pin.isdigit() or len(req.nouveau_pin) != 4:
raise HTTPException(status_code=400, detail="Le nouveau PIN doit contenir 4 chiffres")
if not verify_pin(req.pin_actuel, sysadmin.pin_hash):
raise HTTPException(status_code=401, detail="PIN actuel invalide")
sysadmin.pin_hash = hash_pin(req.nouveau_pin)
return {"message": "PIN sysadmin mis à jour"}
@router.post("/demo")
async def demo_database(
_sysadmin=Depends(require_sysadmin),

View file

@ -17,7 +17,7 @@ from app.services.historique import enregistrer_historique, snapshot, verifier_v
router = APIRouter(prefix="/depenses", tags=["S5 — Dépenses"])
METHODES_VALIDES = {"VIREMENT", "CHEQUE", "COLLECTE"}
UPLOAD_DIR = "/opt/groupe-meditation/uploads/preuves"
UPLOAD_DIR = "/opt/app/uploads/preuves"
async def _to_out(d: Depense, db: AsyncSession) -> DepenseOut:

17
deploy/app.service Normal file
View file

@ -0,0 +1,17 @@
[Unit]
Description=App API
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/app/backend
Environment=PATH=/opt/app/backend/venv/bin
ExecStart=/opt/app/backend/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View file

@ -1,8 +1,8 @@
#!/bin/bash
# Sauvegarde quotidienne PostgreSQL — Groupe Méditation
# Sauvegarde quotidienne PostgreSQL — App
# À placer dans /etc/cron.daily/ ou via crontab
BACKUP_DIR="/var/backups/groupe-meditation"
BACKUP_DIR="/var/backups/app"
RETENTION_DAYS=30
DB_NAME="groupe_meditation"
DB_USER="grpmed"
@ -11,9 +11,10 @@ DATE=$(date +%Y-%m-%d_%H%M)
mkdir -p "$BACKUP_DIR"
# Dump
pg_dump -U "$DB_USER" -h 127.0.0.1 -Fc "$DB_NAME" > "$BACKUP_DIR/grpmed_${DATE}.dump"
pg_dump -U "$DB_USER" -h 127.0.0.1 -Fc "$DB_NAME" > "$BACKUP_DIR/app_${DATE}.dump"
# Nettoyage des anciennes sauvegardes
find "$BACKUP_DIR" -name "grpmed_*.dump" -mtime +$RETENTION_DAYS -delete
find "$BACKUP_DIR" -name "app_*.dump" -mtime +$RETENTION_DAYS -delete
echo "[$(date)] Sauvegarde terminée: grpmed_${DATE}.dump"
echo "[$(date)] Sauvegarde terminée: app_${DATE}.dump"

View file

@ -2,7 +2,7 @@
"""Sauvegardes applicatives par groupe.
Usage:
cd /opt/groupe-meditation/backend
cd /opt/app/backend
venv/bin/python ../deploy/group_backup.py all
"""
import asyncio

View file

@ -1,17 +0,0 @@
[Unit]
Description=Groupe Méditation API
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/groupe-meditation/backend
Environment=PATH=/opt/groupe-meditation/backend/venv/bin
ExecStart=/opt/groupe-meditation/backend/venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View file

@ -1,9 +1,9 @@
# Groupe Méditation — NGINX (seul, sans Apache)
# App — NGINX (seul, sans Apache)
# Sert les fichiers statiques + reverse proxy vers FastAPI
#
# Installation :
# sudo cp deploy/nginx-groupe-meditation.conf /etc/nginx/sites-available/groupe-meditation
# sudo ln -s /etc/nginx/sites-available/groupe-meditation /etc/nginx/sites-enabled/
# sudo cp deploy/nginx-app.conf /etc/nginx/sites-available/app
# sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/
# sudo nginx -t && sudo systemctl reload nginx
# sudo certbot --nginx -d app.87-16.org
@ -32,7 +32,7 @@ server {
}
# ── Frontend — fichiers statiques (React build) ──
root /var/www/groupe-meditation;
root /var/www/app;
index index.html;
# Cache longue durée sur les assets (JS, CSS, images)

View file

@ -1,7 +1,7 @@
"""
Script d'initialisation.
Crée le schéma et le groupe de démonstration initial.
Usage: cd /opt/groupe-meditation/backend && venv/bin/python ../deploy/seed.py
Usage: cd /opt/app/backend && venv/bin/python ../deploy/seed.py
"""
import asyncio
import sys

View file

@ -9,7 +9,7 @@ Dernière révision: 2026-06-01
Sur la VM:
```bash
systemctl status groupe-meditation
systemctl status app
systemctl status nginx
```
@ -28,20 +28,20 @@ curl http://127.0.0.1:8000/api/health
Réponse attendue:
```json
{"status":"ok","app":"Groupe Méditation","version":"0.1.0"}
{"status":"ok","app":"App","version":"0.1.0"}
```
## Redémarrer
```bash
sudo systemctl restart groupe-meditation
sudo systemctl restart app
sudo systemctl reload nginx
```
## Lire les journaux système
```bash
journalctl -u groupe-meditation -n 200 --no-pager
journalctl -u app -n 200 --no-pager
journalctl -u nginx -n 100 --no-pager
```

View file

@ -105,13 +105,13 @@ ansible-playbook ansible/verify.yml --ask-vault-pass
Vérification directe de l'API:
```bash
ansible groupe-meditation-prod -m uri -a 'url=http://127.0.0.1:8000/api/health return_content=true status_code=200' -o
ansible app-prod -m uri -a 'url=http://127.0.0.1:8000/api/health return_content=true status_code=200' -o
```
Vérification des groupes publics:
```bash
ansible groupe-meditation-prod -m uri -a 'url=http://127.0.0.1:8000/api/auth/groupes return_content=true status_code=200' -o
ansible app-prod -m uri -a 'url=http://127.0.0.1:8000/api/auth/groupes return_content=true status_code=200' -o
```
## Initialisation de la base

View file

@ -15,7 +15,7 @@ Le groupe courant est déterminé par le membre authentifié et le `groupe_id` d
Chaque groupe reçoit aussi un schéma PostgreSQL dédié, nommé à partir de son identifiant (`grp_<uuid>`). Les tables du groupe peuvent être rafraîchies dans ce schéma avec le script:
```bash
cd /opt/groupe-meditation/backend
cd /opt/app/backend
venv/bin/python ../deploy/tenant_schemas.py
```

View file

@ -8,7 +8,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Groupe Méditation</title>
<title>App</title>
</head>
<body class="bg-gray-50">
<div id="root"></div>

View file

@ -1,11 +1,11 @@
{
"name": "groupe-meditation",
"name": "app",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "groupe-meditation",
"name": "app",
"version": "0.1.0",
"dependencies": {
"react": "^18.3.1",

View file

@ -1,8 +1,8 @@
{
"name": "groupe-meditation",
"name": "app",
"private": true,
"version": "0.1.0",
"description": "Application du Groupe Méditation — District 87-16",
"description": "App — District 87-16",
"type": "module",
"scripts": {
"dev": "vite",

View file

@ -4,10 +4,10 @@ self.addEventListener('push', (event) => {
try {
data = event.data.json();
} catch {
data = { title: 'Groupe Méditation', body: event.data.text() };
data = { title: 'App', body: event.data.text() };
}
}
const title = data.title || 'Groupe Méditation';
const title = data.title || 'App';
const options = {
body: data.body || 'Nouvelle notification',
icon: '/icon-192.png',

View file

@ -25,7 +25,6 @@ export default function GestionGroupes() {
try {
const data = await api.get('/admin/groupes');
setGroupes(data);
if (!ouvertId && data.length) setOuvertId(data[0].id);
} catch (e) {
setError(e.message);
} finally {
@ -168,6 +167,36 @@ export default function GestionGroupes() {
}
};
const changerPinSysadmin = async () => {
const pinActuel = window.prompt('PIN actuel');
if (!pinActuel) return;
const nouveauPin = window.prompt('Nouveau PIN sysadmin (4 chiffres)');
if (!nouveauPin) return;
if (!/^[0-9]{4}$/.test(nouveauPin)) {
setError('Le nouveau PIN doit contenir 4 chiffres');
return;
}
const confirmation = window.prompt('Confirmer le nouveau PIN');
if (confirmation !== nouveauPin) {
setError('La confirmation ne correspond pas');
return;
}
setBusy(true);
setError('');
setMsg('');
try {
const res = await api.post('/admin/sysadmin-pin', {
pin_actuel: pinActuel,
nouveau_pin: nouveauPin,
});
setMsg(res.message || 'PIN sysadmin mis à jour');
} catch (e) {
setError(e.message);
} finally {
setBusy(false);
}
};
return (
<div className="min-h-screen bg-gray-100">
<PageHeader titre="Gestion des groupes" sousTitre="Groupes, démo et opérations sysadmin" />
@ -184,6 +213,13 @@ export default function GestionGroupes() {
>
Créer un groupe
</button>
<button
onClick={changerPinSysadmin}
disabled={busy}
className="w-full min-h-[48px] p-3 bg-gray-800 text-white rounded-xl text-sm font-bold disabled:opacity-40"
>
Changer le PIN sysadmin
</button>
</div>
{form && (

View file

@ -12,7 +12,7 @@ export default function Login() {
const [courriel, setCourriel] = useState(localStorage.getItem('gm_courriel') || '');
const [pin, setPin] = useState('');
const [groupeId, setGroupeId] = useState(localStorage.getItem('gm_groupe_id') || '');
const [groupeOuvert, setGroupeOuvert] = useState(localStorage.getItem('gm_groupe_id') || '');
const [groupeOuvert, setGroupeOuvert] = useState('');
const [seSouvenir, setSeSouvenir] = useState(!!localStorage.getItem('gm_courriel'));
useEffect(() => {
@ -25,7 +25,6 @@ export default function Login() {
if (groupeId && groupeExiste) return;
const prochainGroupeId = groupes.length === 1 ? groupes[0].id : '';
setGroupeId(prochainGroupeId);
setGroupeOuvert(prochainGroupeId);
if (prochainGroupeId) {
localStorage.setItem('gm_groupe_id', prochainGroupeId);
} else {

View file

@ -1,5 +1,5 @@
/**
* Client API — Groupe Méditation
* Client API — App
*/
const BASE = '/api';

View file

@ -9,9 +9,9 @@ export default defineConfig({
registerType: 'autoUpdate',
includeAssets: ['favicon.svg'],
manifest: {
name: 'Groupe Méditation',
name: 'App',
short_name: 'Méditation',
description: 'Application du Groupe Méditation — District 87-16',
description: 'App — District 87-16',
theme_color: '#1a5276',
background_color: '#f8f9fa',
display: 'standalone',