feat(seed): add I4 forms publics + space etudiant + I5 healthcheck etendu
Some checks are pending
CI / Lint bridge (Biome) (push) Waiting to run
CI / Type-check bridge (push) Blocked by required conditions
CI / Tests unit bridge (push) Blocked by required conditions
CI / Tests integration bridge (push) Blocked by required conditions
CI / Security scan (push) Waiting to run
CI / Docker build + healthcheck (push) Blocked by required conditions
Some checks are pending
CI / Lint bridge (Biome) (push) Waiting to run
CI / Type-check bridge (push) Blocked by required conditions
CI / Tests unit bridge (push) Blocked by required conditions
CI / Tests integration bridge (push) Blocked by required conditions
CI / Security scan (push) Waiting to run
CI / Docker build + healthcheck (push) Blocked by required conditions
I4 — Forms publics + space etudiant : - baserow/seed/seed_forms.py : cree forms publics sur attribution + intervention (saisie heures via lien sans compte). Form cree OK, field-options endpoint retourne 404 sur Baserow 1.30 — a investiguer (URL different selon version). - docmost/setup/create-space-etudiant.py : cree space prive + invite etudiant + page Welcome template. Slug strict lettres+chiffres only (fix Docmost). I5 — Ops : - scripts/healthcheck.sh : check UI + API health (Docmost+Baserow), affiche status containers Docker. 4/4 OK en local. - scripts/cron-install.sh : installe cron quotidien backup + healthcheck */5min. Makefile : targets seed-baserow-forms, create-space-etudiant. Tous les targets utilisent maintenant .venv/ local pour eviter pip systeme. .gitignore : exclut .venv/ + __pycache__/.
This commit is contained in:
parent
d5558caf9a
commit
1d71364c6e
6 changed files with 504 additions and 14 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -12,6 +12,10 @@ backups/
|
|||
*.sql.gz
|
||||
*.tar.gz
|
||||
|
||||
# Python venv
|
||||
.venv/
|
||||
__pycache__/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
dist/
|
||||
|
|
|
|||
29
Makefile
29
Makefile
|
|
@ -75,12 +75,12 @@ backup-baserow:
|
|||
@echo " -> $(BACKUP_DIR)/baserow-$(DATE).tar.gz"
|
||||
|
||||
seed-baserow:
|
||||
@command -v python3 >/dev/null || (echo "ERREUR: python3 requis" && exit 1)
|
||||
@test -d .venv || python3 -m venv .venv
|
||||
@.venv/bin/pip install -q -r baserow/seed/requirements.txt
|
||||
@test -n "$$BASEROW_EMAIL" -a -n "$$BASEROW_PASSWORD" || \
|
||||
(echo "ERREUR: exporter BASEROW_EMAIL et BASEROW_PASSWORD" && exit 1)
|
||||
@cd baserow/seed && pip install -q -r requirements.txt
|
||||
BASEROW_URL=$${BASEROW_URL:-http://localhost:8080} \
|
||||
python3 baserow/seed/seed.py
|
||||
.venv/bin/python baserow/seed/seed.py
|
||||
|
||||
setup-docmost-guide:
|
||||
@echo "Docmost API publique = feature Enterprise payante."
|
||||
|
|
@ -89,12 +89,29 @@ setup-docmost-guide:
|
|||
@echo " - Seed automatise via endpoints internes — cf seed-docmost"
|
||||
|
||||
seed-docmost:
|
||||
@command -v python3 >/dev/null || (echo "ERREUR: python3 requis" && exit 1)
|
||||
@test -d .venv || python3 -m venv .venv
|
||||
@.venv/bin/pip install -q -r docmost/setup/requirements.txt
|
||||
@test -n "$$DOCMOST_ADMIN_EMAIL" -a -n "$$DOCMOST_ADMIN_PASSWORD" || \
|
||||
(echo "ERREUR: exporter DOCMOST_ADMIN_EMAIL et DOCMOST_ADMIN_PASSWORD" && exit 1)
|
||||
@cd docmost/setup && pip install -q -r requirements.txt
|
||||
DOCMOST_URL=$${DOCMOST_URL:-http://localhost:3000} \
|
||||
python3 docmost/setup/seed.py
|
||||
.venv/bin/python docmost/setup/seed.py
|
||||
|
||||
seed-baserow-forms:
|
||||
@test -d .venv || python3 -m venv .venv
|
||||
@.venv/bin/pip install -q -r baserow/seed/requirements.txt
|
||||
@test -n "$$BASEROW_EMAIL" -a -n "$$BASEROW_PASSWORD" || \
|
||||
(echo "ERREUR: exporter BASEROW_EMAIL et BASEROW_PASSWORD" && exit 1)
|
||||
BASEROW_URL=$${BASEROW_URL:-http://localhost:8080} \
|
||||
.venv/bin/python baserow/seed/seed_forms.py
|
||||
|
||||
create-space-etudiant:
|
||||
@test -d .venv || python3 -m venv .venv
|
||||
@.venv/bin/pip install -q -r docmost/setup/requirements.txt
|
||||
@test -n "$$NOM" -a -n "$$PRENOM" -a -n "$$EMAIL" || \
|
||||
(echo "ERREUR: exporter NOM, PRENOM, EMAIL (etudiant)" && exit 1)
|
||||
DOCMOST_URL=$${DOCMOST_URL:-http://localhost:3000} \
|
||||
.venv/bin/python docmost/setup/create-space-etudiant.py \
|
||||
--nom "$$NOM" --prenom "$$PRENOM" --email "$$EMAIL"
|
||||
|
||||
clean:
|
||||
@echo "ATTENTION: cette commande supprime TOUS les volumes (donnees perdues)."
|
||||
|
|
|
|||
220
baserow/seed/seed_forms.py
Normal file
220
baserow/seed/seed_forms.py
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Baserow forms publics — Iteration 4 BUILD.
|
||||
|
||||
Cree des form views publiques sur les tables attribution et intervention.
|
||||
Permet la saisie rapide via lien public sans compte Baserow (UX mobile).
|
||||
|
||||
Usage :
|
||||
BASEROW_URL=http://localhost:8080 \\
|
||||
BASEROW_EMAIL=admin@acadenice.fr \\
|
||||
BASEROW_PASSWORD=... \\
|
||||
python baserow/seed/seed_forms.py
|
||||
|
||||
Idempotent : skip si form existe deja.
|
||||
|
||||
ATTENTION Phase 1 : pas de validation d'identite cote form. N'importe qui
|
||||
avec le lien peut soumettre. Phase 2 bridge service ajoutera l'auth SSO
|
||||
+ filtre row-level (formateur saisit que ses propres attributions).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
FORMS_TO_CREATE: list[dict[str, Any]] = [
|
||||
{
|
||||
"table_name": "attribution",
|
||||
"form": {
|
||||
"name": "Saisir heures realisees (formateur)",
|
||||
"type": "form",
|
||||
"title": "Heures realisees - Acadenice formation",
|
||||
"description": "Formateur : enregistre tes heures realisees apres une session.\n\nMerci de selectionner le module concerne, ton attribution, et le nombre d'heures effectuees.",
|
||||
"submit_action": "MESSAGE",
|
||||
"submit_action_message": "Merci ! Tes heures sont enregistrees. La capacite restante est mise a jour automatiquement.",
|
||||
"public": True,
|
||||
},
|
||||
"fields_to_show": [
|
||||
"attribution_personne",
|
||||
"attribution_module",
|
||||
"attribution_heures_realisees",
|
||||
"attribution_date_fin",
|
||||
"attribution_statut",
|
||||
],
|
||||
},
|
||||
{
|
||||
"table_name": "intervention",
|
||||
"form": {
|
||||
"name": "Saisir intervention (developpeur)",
|
||||
"type": "form",
|
||||
"title": "Intervention - Acadenice agence",
|
||||
"description": "Developpeur : log ton intervention sur une tache projet client.\n\nIndique la tache, tes heures, la date, et notes optionnelles (commit, lien PR).",
|
||||
"submit_action": "MESSAGE",
|
||||
"submit_action_message": "Merci ! L'intervention est enregistree. Capacite agence mise a jour.",
|
||||
"public": True,
|
||||
},
|
||||
"fields_to_show": [
|
||||
"intervention_personne",
|
||||
"intervention_tache",
|
||||
"intervention_heures",
|
||||
"intervention_date",
|
||||
"intervention_notes",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class BaserowForms:
|
||||
def __init__(self, base_url: str, email: str, password: str) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.email = email
|
||||
self.password = password
|
||||
self.token: str | None = None
|
||||
self.session = requests.Session()
|
||||
|
||||
def _headers(self) -> dict[str, str]:
|
||||
return {"Authorization": f"JWT {self.token}", "Content-Type": "application/json"}
|
||||
|
||||
def login(self) -> None:
|
||||
r = self.session.post(
|
||||
f"{self.base_url}/api/user/token-auth/",
|
||||
json={"email": self.email, "password": self.password},
|
||||
)
|
||||
r.raise_for_status()
|
||||
self.token = r.json()["token"]
|
||||
print(f" [auth] Logged in as {self.email}")
|
||||
|
||||
def list_workspaces(self) -> list[dict[str, Any]]:
|
||||
r = self.session.get(f"{self.base_url}/api/workspaces/", headers=self._headers())
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def list_databases(self, workspace_id: int) -> list[dict[str, Any]]:
|
||||
r = self.session.get(
|
||||
f"{self.base_url}/api/applications/workspace/{workspace_id}/", headers=self._headers()
|
||||
)
|
||||
r.raise_for_status()
|
||||
return [a for a in r.json() if a["type"] == "database"]
|
||||
|
||||
def list_tables(self, database_id: int) -> dict[str, dict[str, Any]]:
|
||||
r = self.session.get(
|
||||
f"{self.base_url}/api/database/tables/database/{database_id}/", headers=self._headers()
|
||||
)
|
||||
r.raise_for_status()
|
||||
return {t["name"]: t for t in r.json()}
|
||||
|
||||
def list_views(self, table_id: int) -> list[dict[str, Any]]:
|
||||
r = self.session.get(
|
||||
f"{self.base_url}/api/database/views/table/{table_id}/", headers=self._headers()
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def list_fields(self, table_id: int) -> dict[str, dict[str, Any]]:
|
||||
r = self.session.get(
|
||||
f"{self.base_url}/api/database/fields/table/{table_id}/", headers=self._headers()
|
||||
)
|
||||
r.raise_for_status()
|
||||
return {f["name"]: f for f in r.json()}
|
||||
|
||||
def create_form(self, table_id: int, form_def: dict[str, Any]) -> dict[str, Any]:
|
||||
r = self.session.post(
|
||||
f"{self.base_url}/api/database/views/table/{table_id}/",
|
||||
headers=self._headers(),
|
||||
json=form_def,
|
||||
)
|
||||
if r.status_code >= 300:
|
||||
print(f" [ERROR] create form payload={form_def} resp={r.text[:300]}")
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def update_field_options(
|
||||
self, view_id: int, field_options: dict[int, dict[str, Any]]
|
||||
) -> None:
|
||||
r = self.session.patch(
|
||||
f"{self.base_url}/api/database/views/form/{view_id}/field-options/",
|
||||
headers=self._headers(),
|
||||
json={"field_options": field_options},
|
||||
)
|
||||
if r.status_code >= 300:
|
||||
print(f" [ERROR] update field options resp={r.text[:300]}")
|
||||
r.raise_for_status()
|
||||
|
||||
def run(self) -> None:
|
||||
print(f"\n[1/4] Login")
|
||||
self.login()
|
||||
|
||||
print(f"\n[2/4] Find workspace 'Acadenice' + database 'formation-hub'")
|
||||
ws = next((w for w in self.list_workspaces() if w["name"] == "Acadenice"), None)
|
||||
if not ws:
|
||||
raise RuntimeError("Workspace 'Acadenice' not found")
|
||||
db = next((d for d in self.list_databases(ws["id"]) if d["name"] == "formation-hub"), None)
|
||||
if not db:
|
||||
raise RuntimeError("Database 'formation-hub' not found")
|
||||
print(f" Workspace id={ws['id']}, Database id={db['id']}")
|
||||
|
||||
print(f"\n[3/4] Resolve tables")
|
||||
tables = self.list_tables(db["id"])
|
||||
|
||||
print(f"\n[4/4] Create forms")
|
||||
for spec in FORMS_TO_CREATE:
|
||||
tname = spec["table_name"]
|
||||
if tname not in tables:
|
||||
print(f" [skip] table '{tname}' absent")
|
||||
continue
|
||||
tid = tables[tname]["id"]
|
||||
existing_forms = [v for v in self.list_views(tid) if v["type"] == "form"]
|
||||
existing_names = {v["name"] for v in existing_forms}
|
||||
if spec["form"]["name"] in existing_names:
|
||||
form = next(v for v in existing_forms if v["name"] == spec["form"]["name"])
|
||||
print(f" [form] Reuse '{spec['form']['name']}' on {tname} id={form['id']}")
|
||||
else:
|
||||
form = self.create_form(tid, spec["form"])
|
||||
print(f" [form] Created '{form['name']}' on {tname} id={form['id']}")
|
||||
|
||||
# Configure visible fields
|
||||
fields = self.list_fields(tid)
|
||||
field_options: dict[int, dict[str, Any]] = {}
|
||||
for fname in spec["fields_to_show"]:
|
||||
if fname in fields:
|
||||
field_options[fields[fname]["id"]] = {
|
||||
"enabled": True,
|
||||
"required": fname in ("attribution_personne", "attribution_module", "attribution_heures_realisees", "intervention_personne", "intervention_tache", "intervention_heures"),
|
||||
}
|
||||
if field_options:
|
||||
self.update_field_options(form["id"], field_options)
|
||||
print(f" [form] {len(field_options)} fields visible/required configures")
|
||||
|
||||
slug = form.get("slug")
|
||||
if slug:
|
||||
print(f" [public URL] {self.base_url}/form/{slug}")
|
||||
else:
|
||||
print(f" [note] slug pas trouve dans la response (cf UI Baserow pour le lien public)")
|
||||
|
||||
print("\n=== Forms OK ===")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
base_url = os.environ.get("BASEROW_URL", "http://localhost:8080")
|
||||
email = os.environ.get("BASEROW_EMAIL")
|
||||
password = os.environ.get("BASEROW_PASSWORD")
|
||||
if not email or not password:
|
||||
print("ERROR: set BASEROW_EMAIL et BASEROW_PASSWORD env vars", file=sys.stderr)
|
||||
return 1
|
||||
try:
|
||||
BaserowForms(base_url, email, password).run()
|
||||
except requests.HTTPError as e:
|
||||
print(f"\nHTTP error: {e}", file=sys.stderr)
|
||||
if e.response is not None:
|
||||
print(f"Response: {e.response.text[:500]}", file=sys.stderr)
|
||||
return 2
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}", file=sys.stderr)
|
||||
return 3
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
182
docmost/setup/create-space-etudiant.py
Normal file
182
docmost/setup/create-space-etudiant.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Cree un space etudiant prive dans Docmost — pattern story S-08.
|
||||
|
||||
Usage :
|
||||
DOCMOST_URL=http://localhost:3000 \\
|
||||
DOCMOST_ADMIN_EMAIL=corentin@acadenice.fr \\
|
||||
DOCMOST_ADMIN_PASSWORD=... \\
|
||||
python docmost/setup/create-space-etudiant.py \\
|
||||
--nom "Dupont" --prenom "Marie" --email "marie.dupont@acadenice.fr"
|
||||
|
||||
Cree :
|
||||
- Un space "Etudiant - Marie Dupont" en visibility=private
|
||||
- Y invite l'etudiant en role 'writer'
|
||||
- Cree une page template "Bienvenue" avec instructions
|
||||
|
||||
Idempotent : skip si space existe deja.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class DocmostSpaceCreator:
|
||||
def __init__(self, base_url: str) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({"Content-Type": "application/json"})
|
||||
|
||||
def _post(self, path: str, body: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
r = self.session.post(f"{self.base_url}{path}", json=body or {})
|
||||
if r.status_code >= 300:
|
||||
print(f" [ERROR] POST {path} → {r.status_code} resp={r.text[:300]}")
|
||||
r.raise_for_status()
|
||||
if not r.text:
|
||||
return {}
|
||||
payload = r.json()
|
||||
if isinstance(payload, dict) and "data" in payload and isinstance(payload["data"], (dict, list)):
|
||||
return payload["data"]
|
||||
return payload
|
||||
|
||||
def login(self, email: str, password: str) -> None:
|
||||
self._post("/api/auth/login", {"email": email, "password": password})
|
||||
print(f" [auth] Logged in as {email}")
|
||||
|
||||
def list_spaces(self) -> list[dict[str, Any]]:
|
||||
try:
|
||||
data = self._post("/api/spaces/", {"page": 1, "limit": 100})
|
||||
if isinstance(data, dict):
|
||||
return data.get("items") or []
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
except requests.HTTPError:
|
||||
pass
|
||||
return []
|
||||
|
||||
def create_space(self, name: str, description: str, slug: str, visibility: str = "private") -> str:
|
||||
result = self._post(
|
||||
"/api/spaces/create",
|
||||
{"name": name, "description": description, "slug": slug, "visibility": visibility},
|
||||
)
|
||||
return result.get("id")
|
||||
|
||||
def add_member_by_email(self, space_id: str, email: str, role: str = "writer") -> dict[str, Any] | None:
|
||||
try:
|
||||
return self._post(
|
||||
"/api/spaces/members/add",
|
||||
{"spaceId": space_id, "userEmails": [email], "role": role},
|
||||
)
|
||||
except requests.HTTPError as e:
|
||||
print(f" [member] Echec ajout {email} : {e}")
|
||||
print(f" (l'etudiant doit deja exister comme user Docmost. Sinon, l'inviter d'abord via UI ou API user/create.)")
|
||||
return None
|
||||
|
||||
def create_page(self, space_id: str, title: str, content_md: str) -> str:
|
||||
result = self._post(
|
||||
"/api/pages/create",
|
||||
{"spaceId": space_id, "title": title, "format": "markdown", "content": content_md},
|
||||
)
|
||||
return result.get("id")
|
||||
|
||||
|
||||
def welcome_template(prenom: str, nom: str) -> str:
|
||||
return f"""# Bienvenue {prenom} !
|
||||
|
||||
Voici ton **espace personnel** sur le wiki Acadenice. Il est **prive** : seul toi et l'admin peuvent le voir.
|
||||
|
||||
## Tu peux faire ce que tu veux ici
|
||||
|
||||
- Prendre des notes pendant les cours
|
||||
- Garder tes ressources perso (liens, references, brouillons)
|
||||
- Tester les fonctionnalites du wiki :
|
||||
- `/mermaid` pour faire un diagramme
|
||||
- `/excalidraw` pour dessiner
|
||||
- `/drawio` pour des schemas techniques
|
||||
- Liste, tableaux, code blocks, embeds video, etc.
|
||||
|
||||
## Quelques idees pour commencer
|
||||
|
||||
- Cree une page "Mes objectifs formation"
|
||||
- Une page par module avec tes notes
|
||||
- Un journal d'apprentissage hebdomadaire
|
||||
|
||||
## Acces aux supports formation
|
||||
|
||||
Les supports officiels sont dans le space **CFA** (lecture seule pour toi).
|
||||
|
||||
Bonne formation !
|
||||
|
||||
— L'equipe Acadenice
|
||||
"""
|
||||
|
||||
|
||||
def slugify(s: str) -> str:
|
||||
"""Docmost exige slug = lettres + chiffres uniquement."""
|
||||
import re
|
||||
return re.sub(r"[^a-z0-9]", "", s.lower())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Cree un space etudiant Docmost")
|
||||
parser.add_argument("--nom", required=True)
|
||||
parser.add_argument("--prenom", required=True)
|
||||
parser.add_argument("--email", required=True, help="Email de l'etudiant (doit exister comme user Docmost)")
|
||||
parser.add_argument("--no-template", action="store_true", help="Ne pas creer la page Bienvenue")
|
||||
args = parser.parse_args()
|
||||
|
||||
base_url = os.environ.get("DOCMOST_URL", "http://localhost:3000")
|
||||
admin_email = os.environ.get("DOCMOST_ADMIN_EMAIL")
|
||||
admin_password = os.environ.get("DOCMOST_ADMIN_PASSWORD")
|
||||
if not admin_email or not admin_password:
|
||||
print("ERROR: set DOCMOST_ADMIN_EMAIL and DOCMOST_ADMIN_PASSWORD", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
creator = DocmostSpaceCreator(base_url)
|
||||
space_name = f"Etudiant - {args.prenom} {args.nom}"
|
||||
slug = slugify(f"etudiant-{args.prenom}-{args.nom}")
|
||||
|
||||
try:
|
||||
print(f"\n[1/4] Login admin")
|
||||
creator.login(admin_email, admin_password)
|
||||
|
||||
print(f"\n[2/4] Check existing space '{space_name}'")
|
||||
existing = next((s for s in creator.list_spaces() if s.get("slug") == slug or s.get("name") == space_name), None)
|
||||
if existing:
|
||||
sid = existing["id"]
|
||||
print(f" [space] Reuse id={sid}")
|
||||
else:
|
||||
sid = creator.create_space(
|
||||
name=space_name,
|
||||
description=f"Space personnel de {args.prenom} {args.nom}. Libre usage.",
|
||||
slug=slug,
|
||||
visibility="private",
|
||||
)
|
||||
print(f" [space] Created id={sid}")
|
||||
|
||||
print(f"\n[3/4] Add etudiant {args.email} as writer")
|
||||
creator.add_member_by_email(sid, args.email, role="writer")
|
||||
|
||||
if not args.no_template:
|
||||
print(f"\n[4/4] Welcome page template")
|
||||
try:
|
||||
creator.create_page(sid, "Bienvenue", welcome_template(args.prenom, args.nom))
|
||||
print(f" [page] Created Bienvenue")
|
||||
except requests.HTTPError as e:
|
||||
print(f" [page] Skip (peut-etre deja existante) : {e}")
|
||||
|
||||
print(f"\n=== Space etudiant OK ===")
|
||||
print(f" Name : {space_name}")
|
||||
print(f" Slug : {slug}")
|
||||
print(f" URL : {base_url} (login {args.email})")
|
||||
except requests.HTTPError as e:
|
||||
print(f"\nHTTP error: {e}", file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
37
scripts/cron-install.sh
Executable file
37
scripts/cron-install.sh
Executable file
|
|
@ -0,0 +1,37 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/cron-install.sh — installe le cron quotidien backup
|
||||
# Usage : sudo ./scripts/cron-install.sh
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
CRON_USER="${CRON_USER:-$(whoami)}"
|
||||
CRON_FILE="/etc/cron.d/formation-hub"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run as root (sudo) pour ecrire dans /etc/cron.d/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat > "$CRON_FILE" <<EOF
|
||||
# formation-hub — backup quotidien
|
||||
# Genere par scripts/cron-install.sh
|
||||
SHELL=/bin/bash
|
||||
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
MAILTO=""
|
||||
|
||||
# Backup quotidien Docmost + Baserow + uploads (03:00 local time)
|
||||
0 3 * * * $CRON_USER cd $REPO_DIR && ./scripts/backup.sh >> /var/log/formation-hub-backup.log 2>&1
|
||||
|
||||
# Healthcheck toutes les 5 minutes (logue uniquement les fails)
|
||||
*/5 * * * * $CRON_USER cd $REPO_DIR && ./scripts/healthcheck.sh >/dev/null 2>&1 || logger -t formation-hub "healthcheck failed at \$(date -Iseconds)"
|
||||
EOF
|
||||
|
||||
chmod 644 "$CRON_FILE"
|
||||
|
||||
echo "Cron installe : $CRON_FILE"
|
||||
echo ""
|
||||
echo "Verification :"
|
||||
cat "$CRON_FILE"
|
||||
echo ""
|
||||
echo "Pour test manuel : sudo -u $CRON_USER cd $REPO_DIR && ./scripts/backup.sh"
|
||||
echo "Logs backup : tail -f /var/log/formation-hub-backup.log"
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
# scripts/healthcheck.sh — verifie que la stack repond
|
||||
# scripts/healthcheck.sh — health check stack + APIs internes
|
||||
set -euo pipefail
|
||||
|
||||
DOCMOST_URL="${DOCMOST_URL:-http://localhost:3000}"
|
||||
|
|
@ -9,8 +9,9 @@ TIMEOUT="${HEALTHCHECK_TIMEOUT:-10}"
|
|||
|
||||
red() { printf '\033[31m%s\033[0m\n' "$1"; }
|
||||
green() { printf '\033[32m%s\033[0m\n' "$1"; }
|
||||
yellow(){ printf '\033[33m%s\033[0m\n' "$1"; }
|
||||
|
||||
check() {
|
||||
check_http() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
if curl -sf --max-time "$TIMEOUT" -o /dev/null "$url"; then
|
||||
|
|
@ -22,23 +23,52 @@ check() {
|
|||
fi
|
||||
}
|
||||
|
||||
echo "Healthcheck (timeout ${TIMEOUT}s)..."
|
||||
check_health_endpoint() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local expected="$3"
|
||||
local resp
|
||||
resp=$(curl -sf --max-time "$TIMEOUT" "$url" || echo "")
|
||||
if echo "$resp" | grep -q "$expected"; then
|
||||
green " OK $name (API health endpoint)"
|
||||
return 0
|
||||
else
|
||||
red " KO $name (API health endpoint) — got: ${resp:0:80}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Healthcheck stack (timeout ${TIMEOUT}s)"
|
||||
echo "----------------------------------------"
|
||||
|
||||
ok=0
|
||||
total=0
|
||||
|
||||
((++total)) || true
|
||||
check "Docmost " "$DOCMOST_URL" && ((++ok)) || true
|
||||
check_http "Docmost UI " "$DOCMOST_URL" && ((++ok)) || true
|
||||
|
||||
((++total)) || true
|
||||
check "Baserow " "$BASEROW_URL" && ((++ok)) || true
|
||||
check_health_endpoint "Docmost API" "$DOCMOST_URL/api/health" "ok" && ((++ok)) || true
|
||||
|
||||
((++total)) || true
|
||||
check_http "Baserow UI " "$BASEROW_URL" && ((++ok)) || true
|
||||
|
||||
((++total)) || true
|
||||
check_http "Baserow API" "$BASEROW_URL/api/_health/" && ((++ok)) || true
|
||||
|
||||
if [ -n "$BRIDGE_URL" ]; then
|
||||
((++total)) || true
|
||||
check "Bridge " "$BRIDGE_URL/api/health" && ((++ok)) || true
|
||||
check_health_endpoint "Bridge " "$BRIDGE_URL/api/health" "ok" && ((++ok)) || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
# Container status (si Docker dispo)
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "Containers :"
|
||||
docker compose ps --format " {{.Name}}: {{.Status}}" 2>&1 | head -10 || yellow " (compose not running here)"
|
||||
fi
|
||||
|
||||
echo "----------------------------------------"
|
||||
if [ "$ok" -eq "$total" ]; then
|
||||
green "Healthcheck : $ok/$total OK"
|
||||
exit 0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue