Typo Plus Jakarta Sans + renommage libreDecision + mode démo prod + seed mandats

- Fonte : Nunito → Plus Jakarta Sans (moderne, ronde sans être toy)
- Logo : ğ(Decision) → libreDecision (libre italic/muted + Decision bold)
- Footer et watermark DocumentPreview mis à jour
- Mode démo : DEMO_MODE flag dans config.py + auth.py (profils rapides en prod)
- docker-compose : ENVIRONMENT=production explicite + DEMO_MODE=true par défaut
- Seed : +décision Licence G1 v0.4.0, +3 mandats (ComTech, Admin Forgerons, Modération)
  runner 7→10 étapes, import Mandate/MandateStep ajouté

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-03-24 01:22:36 +01:00
parent 6fe0b41e7f
commit f87cbc0f2f
9 changed files with 384 additions and 52 deletions

101
CLAUDE.md
View File

@@ -1,25 +1,88 @@
# libreDecision # libreDecision
Plateforme de decisions collectives pour la communaute Duniter/G1. Boîte à outils de gouvernance collective pour la communauté Duniter/G1.
Documents modulaires sous vote permanent + protocoles de vote + mandats.
Architecture marque blanche — vocation à être intégré dans sweethomeCloud et librodrome.
## Protocole de début de session
1. `git pull --rebase origin main`
2. Si des migrations DB sont attendues : `cd backend && alembic upgrade head`
3. Si l'objectif de la session n'est pas précisé, le demander
## Stack ## Stack
- **Frontend**: Nuxt 4 + Nuxt UI v3 + Pinia + UnoCSS (port 3002)
- **Backend**: Python FastAPI + SQLAlchemy async + PostgreSQL asyncpg (port 8002)
- **Auth**: Duniter V2 Ed25519 challenge-response
- **Sanctuaire**: IPFS (kubo) + hash on-chain (system.remark)
## Commands - **Frontend** : Nuxt 4 (Vue 3, TypeScript) + Nuxt UI v3 + Pinia + UnoCSS ; package manager : npm
- Backend: `cd backend && uvicorn app.main:app --port 8002 --reload` - **Backend** : Python FastAPI + SQLAlchemy 2.0 async + PostgreSQL asyncpg ; migrations Alembic
- Backend tests: `cd backend && pytest tests/ -v` - **Auth** : Duniter V2 Ed25519 challenge-response (substrate-interface — stub en dev)
- Frontend: `cd frontend && npm run dev` - **Sanctuaire** : IPFS kubo + hash on-chain (system.remark) — TODO sprint 2
- Frontend build: `cd frontend && npm run build` - Déploiement : Docker multi-stage + Traefik (postgres + backend + frontend + ipfs) ; CI Woodpecker
- Migrations: `cd backend && alembic upgrade head`
- Docker: `docker compose -f docker/docker-compose.yml up`
## Conventions ## Structure
- French for UI text and documentation
- English for code (variable names, comments, docstrings) ```
- API versioned under `/api/v1/` frontend/
- Pydantic v2 for all schemas app/
- Async everywhere (SQLAlchemy, FastAPI) components/ # composants Vue
- Ed25519 signatures for vote integrity layouts/ # layouts Nuxt
pages/ # routing file-based (9 pages sprint 1)
composables/ # (1 composable sprint 1)
stores/ # 5 Pinia stores (auth, ...)
assets/css/
moods.css # système de palettes (.mood-* sur <html>)
utils/ # (2 utils sprint 1)
nuxt.config.ts # port 3002, host 0.0.0.0, apiBase via NUXT_PUBLIC_API_BASE
backend/
app/
routers/ # 8 routers : auth, communes, documents, protocols, votes, ...
services/ # 6 services
engine/ # 5 modules : formule inertie, critères Smith/TechComm, médiane
models/ # 14 tables SQLAlchemy
alembic/versions/ # migrations
tests/ # 186 tests (63 intégration TDD sprint 1)
seed.py # Engagement Certification (33 items) + Forgeron (51 items) + Runtime Upgrade
docker/
docker-compose.yml # postgres + backend + frontend + ipfs
backend.Dockerfile
frontend.Dockerfile
docs/content/ # 7 docs dev + 8 docs user
public/
hexagram-tsing.svg # sceau 井 (embossed)
hexagram-tsing-flat.svg
```
## Données runtime
- **postgres-data** : volume Docker PostgreSQL — jamais écrasé par les builds
- **ipfs-data** : volume Docker IPFS kubo
- `.env` à la racine : `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `SECRET_KEY`, `DOMAIN`, `DUNITER_RPC_URL`
## Commandes
```bash
# Backend
cd backend && . venv/bin/activate
uvicorn app.main:app --port 8002 --host 0.0.0.0 --reload
pytest tests/ -v
alembic upgrade head
python seed.py # reseed Engagement Certification + Forgeron + Runtime Upgrade
# Frontend
cd frontend && npm run dev # :3002
npm run build
# Docker
docker compose -f docker/docker-compose.yml up
```
## Conventions / pièges
- **UI français, code anglais** (variables, commentaires, docstrings)
- **API** : préfixe `/api/v1/`, Pydantic v2 pour tous les schémas, async partout (SQLAlchemy + FastAPI)
- **Auth** : `get_current_admin` (24h), `get_current_citizen` (4h), `require_super_admin`
- **Formule inertie** : `Result = C + B^W + (M + (1-M) × (1 - (T/W)^G)) × max(0, T-C)` — voir `backend/app/engine/`
- **Mood system** : `useMood.ts` synchronise `colorMode.preference` avec la palette — **jamais** de `:global()` dans `<style scoped>` pour les styles mood-dépendants (causa le bug dark mode veil)
- **Sceau** `井` (#48 Tsing) : `.app-seal` dans `app.vue`, right-aligned ; SVGs dans `public/`
- **CSS drop-shadow()** safe pour effets emboss ; `<filter>` SVG inline cause des artefacts de rendu
- **Domaine** : libredecision.org (Woodpecker CI ; ancien dossier : Glibredecision)
- **Ed25519 verification** : stub en dev (substrate-interface), autoritaire en prod — ne pas bypasser sans test

View File

@@ -8,6 +8,7 @@ class Settings(BaseSettings):
# Environment # Environment
ENVIRONMENT: str = "development" # development, staging, production ENVIRONMENT: str = "development" # development, staging, production
DEMO_MODE: bool = False # Enable demo profiles (quick login) regardless of ENVIRONMENT
LOG_LEVEL: str = "INFO" LOG_LEVEL: str = "INFO"
# Database — SQLite by default for local dev, PostgreSQL for Docker/prod # Database — SQLite by default for local dev, PostgreSQL for Docker/prod

View File

@@ -147,7 +147,7 @@ async def verify_challenge(
# 6. Get or create identity (apply dev profile if available) # 6. Get or create identity (apply dev profile if available)
dev_profile = None dev_profile = None
if settings.ENVIRONMENT == "development": if settings.ENVIRONMENT == "development" or settings.DEMO_MODE:
dev_profile = next((p for p in DEV_PROFILES if p["address"] == payload.address), None) dev_profile = next((p for p in DEV_PROFILES if p["address"] == payload.address), None)
identity = await get_or_create_identity(db, payload.address, dev_profile=dev_profile) identity = await get_or_create_identity(db, payload.address, dev_profile=dev_profile)
@@ -162,8 +162,8 @@ async def verify_challenge(
@router.get("/dev/profiles") @router.get("/dev/profiles")
async def list_dev_profiles(): async def list_dev_profiles():
"""List available dev profiles for quick login. Only available in development.""" """List available demo profiles for quick login. Available in development or demo mode."""
if settings.ENVIRONMENT != "development": if settings.ENVIRONMENT != "development" and not settings.DEMO_MODE:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not available") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not available")
return DEV_PROFILES return DEV_PROFILES

View File

@@ -31,6 +31,7 @@ from app.database import async_session, engine, Base, init_db
from app.models.protocol import FormulaConfig, VotingProtocol from app.models.protocol import FormulaConfig, VotingProtocol
from app.models.document import Document, DocumentItem from app.models.document import Document, DocumentItem
from app.models.decision import Decision, DecisionStep from app.models.decision import Decision, DecisionStep
from app.models.mandate import Mandate, MandateStep
from app.models.user import DuniterIdentity from app.models.user import DuniterIdentity
from app.models.vote import VoteSession, Vote from app.models.vote import VoteSession, Vote
@@ -2143,6 +2144,270 @@ async def seed_votes_on_items(
print(f" VoteSession for '{item.title}': created (10 pour, 1 contre)") print(f" VoteSession for '{item.title}': created (10 pour, 1 contre)")
# ---------------------------------------------------------------------------
# Seed: Additional decisions (demo content)
# ---------------------------------------------------------------------------
async def seed_decision_licence_evolution(session: AsyncSession) -> Decision:
"""Seed a community decision: evolution of the G1 monetary license."""
decision, created = await get_or_create(
session,
Decision,
"title",
"Évolution Licence G1 v0.4.0",
description=(
"Proposition d'évolution de la Licence G1 vers la version 0.4.0. "
"Intègre les retours du forum, clarifie les engagements de certification "
"et précise le processus de vote nuancé."
),
context=(
"La Licence G1 v0.3.0 est en vigueur depuis l'origine de la monnaie libre. "
"Des discussions communautaires approfondies (topics 31066, 32375, 32409, 32412) "
"ont permis d'identifier des clarifications nécessaires. "
"Cette décision lance le processus de vote communautaire pour l'adoption "
"de la v0.4.0 selon le protocole Vote WoT standard."
),
decision_type="community",
status="draft",
)
print(f" Decision 'Évolution Licence G1 v0.4.0': {'created' if created else 'exists'}")
if created:
steps = [
{
"step_order": 1,
"step_type": "qualification",
"title": "Rédaction de la proposition",
"description": (
"Co-rédaction de la v0.4.0 en intégrant les retours des discussions "
"forum. Coordination entre 1000i100, Natha, Pini et la communauté."
),
},
{
"step_order": 2,
"step_type": "review",
"title": "Période de commentaires",
"description": (
"Publication de la proposition sur le forum pendant 30 jours. "
"Recueil des amendements et objections. Intégration des retours "
"dans la version finale soumise au vote."
),
},
{
"step_order": 3,
"step_type": "vote",
"title": "Vote WoT (nuancé)",
"description": (
"Vote nuancé à 6 niveaux ouvert à tous les membres de la WoT. "
"Durée : 30 jours. Protocole : Vote WoT standard avec formule inertie."
),
},
{
"step_order": 4,
"step_type": "execution",
"title": "Mise à jour du dépôt officiel",
"description": (
"Si le seuil est atteint : mise à jour du dépôt git officiel, "
"calcul du hash IPFS, ancrage on-chain via system.remark."
),
},
]
for step_data in steps:
step = DecisionStep(decision_id=decision.id, **step_data)
session.add(step)
await session.flush()
print(f" -> {len(steps)} steps created")
return decision
# ---------------------------------------------------------------------------
# Seed: Mandates (Comité Technique + Admin Forgerons)
# ---------------------------------------------------------------------------
async def seed_mandates(session: AsyncSession, voters: list[DuniterIdentity]) -> None:
"""Seed example mandates: TechComm and Smith Admin."""
now = datetime.now(timezone.utc)
# Find Charlie (techcomm voter) as mandatee, or use first voter
mandatee_techcomm = next(
(v for v in voters if "Cgeek" in v.display_name or "Elois" in v.display_name),
voters[0] if voters else None,
)
mandatee_smith = next(
(v for v in voters if "Moul" in v.display_name or "Tuxmain" in v.display_name),
voters[1] if len(voters) > 1 else None,
)
mandates_data = [
{
"title": "Mandat Comité Technique — Session 2025-2026",
"description": (
"Le Comité Technique (ComTech) est mandaté par la communauté pour "
"assurer la validation technique des propositions on-chain : "
"runtime upgrades, modifications de paramètres réseau, audits de code. "
"Composition : 5 membres élus pour 12 mois."
),
"mandate_type": "techcomm",
"status": "active",
"mandatee_id": mandatee_techcomm.id if mandatee_techcomm else None,
"starts_at": now - timedelta(days=90),
"ends_at": now + timedelta(days=275),
"steps": [
{
"step_order": 1,
"step_type": "candidacy",
"title": "Appel à candidatures",
"status": "completed",
"description": "Période de candidatures ouvertes sur le forum Duniter. Durée : 14 jours.",
"outcome": "5 candidats retenus : Elois, Cgeek, Maaltir, Hugo, Tuxmain",
},
{
"step_order": 2,
"step_type": "vote",
"title": "Élection par vote nuancé",
"status": "completed",
"description": "Vote WoT ouvert à tous les membres. Protocole Vote Nuancé (6 niveaux).",
"outcome": "Quorum atteint. 5 membres élus à plus de 70% de soutien.",
},
{
"step_order": 3,
"step_type": "assignment",
"title": "Prise de fonction",
"status": "completed",
"description": "Mise en place du ComTech, définition des processus internes de décision.",
"outcome": "ComTech opérationnel depuis le 2025-09-15.",
},
{
"step_order": 4,
"step_type": "reporting",
"title": "Rapport de mi-mandat",
"status": "active",
"description": "Rapport public d'activité à mi-parcours du mandat.",
},
{
"step_order": 5,
"step_type": "completion",
"title": "Fin de mandat et bilan",
"status": "pending",
"description": "Rapport final, transmission aux successeurs, renouvellement ou dissolution.",
},
],
},
{
"title": "Mandat Administrateur des Forgerons — Rotation 2026-Q1",
"description": (
"L'Administrateur des Forgerons coordonne l'onboarding des nouveaux "
"forgerons, maintient la liste des nœuds actifs et anime les "
"discussions techniques de la sous-WoT Smith. "
"Mandat tournant de 6 mois, renouvelable une fois."
),
"mandate_type": "smith",
"status": "voting",
"mandatee_id": mandatee_smith.id if mandatee_smith else None,
"starts_at": None,
"ends_at": None,
"steps": [
{
"step_order": 1,
"step_type": "formulation",
"title": "Définition du rôle",
"status": "completed",
"description": "Rédaction de la fiche de rôle et des responsabilités de l'Administrateur.",
"outcome": "Fiche de rôle validée par consensus sur le forum Duniter (topic 34201).",
},
{
"step_order": 2,
"step_type": "candidacy",
"title": "Appel à candidatures forgerons",
"status": "completed",
"description": "Candidatures ouvertes aux forgerons actifs depuis plus de 6 mois.",
"outcome": "2 candidats : Moul (Forgeron senior), Tuxmain (Forgeron actif).",
},
{
"step_order": 3,
"step_type": "vote",
"title": "Vote forgeron (Smith)",
"status": "active",
"description": (
"Vote Smith à double critère : quorum WoT + quorum forgerons. "
"Durée : 30 jours. En cours."
),
},
{
"step_order": 4,
"step_type": "assignment",
"title": "Prise de fonction",
"status": "pending",
"description": "Passation avec l'administrateur sortant. Accès aux outils de coordination.",
},
],
},
{
"title": "Mandat Modération Forum — Duniter V2",
"description": (
"Équipe de modération élue pour maintenir la qualité des discussions "
"sur forum.duniter.org et forum.monnaie-libre.fr. "
"3 modérateurs, mandat de 12 mois."
),
"mandate_type": "custom",
"status": "draft",
"mandatee_id": None,
"starts_at": None,
"ends_at": None,
"steps": [
{
"step_order": 1,
"step_type": "formulation",
"title": "Élaboration de la charte de modération",
"status": "active",
"description": "Définition des règles, outils et périmètre de la modération communautaire.",
},
{
"step_order": 2,
"step_type": "candidacy",
"title": "Appel à candidatures",
"status": "pending",
"description": "Ouvert à tous les membres de la WoT G1.",
},
{
"step_order": 3,
"step_type": "vote",
"title": "Élection",
"status": "pending",
"description": "Vote WoT standard.",
},
{
"step_order": 4,
"step_type": "assignment",
"title": "Prise de fonction",
"status": "pending",
"description": "Formation aux outils Discourse et mise en place de la rotation.",
},
],
},
]
for m_data in mandates_data:
steps_data = m_data.pop("steps")
mandate, created = await get_or_create(
session,
Mandate,
"title",
m_data["title"],
**{k: v for k, v in m_data.items() if k != "title"},
)
status_str = "created" if created else "exists"
print(f" Mandate '{mandate.title[:50]}': {status_str}")
if created:
for step_data in steps_data:
step = MandateStep(mandate_id=mandate.id, **step_data)
session.add(step)
await session.flush()
print(f" -> {len(steps_data)} steps created")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Main seed runner # Main seed runner
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -2154,29 +2419,32 @@ async def run_seed():
# Ensure tables exist # Ensure tables exist
await init_db() await init_db()
print("[0/8] Tables created.\n") print("[0/10] Tables created.\n")
async with async_session() as session: async with async_session() as session:
async with session.begin(): async with session.begin():
print("\n[1/8] Formula Configs...") print("\n[1/10] Formula Configs...")
formulas = await seed_formula_configs(session) formulas = await seed_formula_configs(session)
print("\n[2/8] Voting Protocols...") print("\n[2/10] Voting Protocols...")
protocols = await seed_voting_protocols(session, formulas) protocols = await seed_voting_protocols(session, formulas)
print("\n[3/8] Document: Acte d'engagement Certification...") print("\n[3/10] Document: Acte d'engagement Certification...")
await seed_document_engagement_certification(session, protocols) await seed_document_engagement_certification(session, protocols)
print("\n[4/8] Document: Acte d'engagement forgeron v2.0.0...") print("\n[4/10] Document: Acte d'engagement forgeron v2.0.0...")
doc_forgeron = await seed_document_engagement_forgeron(session, protocols) doc_forgeron = await seed_document_engagement_forgeron(session, protocols)
print("\n[5/7] Decision: Runtime Upgrade...") print("\n[5/10] Decision: Runtime Upgrade...")
await seed_decision_runtime_upgrade(session) await seed_decision_runtime_upgrade(session)
print("\n[6/7] Simulated voters...") print("\n[6/10] Decision: Évolution Licence G1 v0.4.0...")
await seed_decision_licence_evolution(session)
print("\n[7/10] Simulated voters...")
voters = await seed_voters(session) voters = await seed_voters(session)
print("\n[7/7] Votes on first 3 engagements forgeron...") print("\n[8/10] Votes on first 3 engagements forgeron...")
await seed_votes_on_items( await seed_votes_on_items(
session, session,
doc_forgeron, doc_forgeron,
@@ -2184,6 +2452,11 @@ async def run_seed():
voters, voters,
) )
print("\n[9/10] Mandates...")
await seed_mandates(session, voters)
print("\n[10/10] Done.")
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("Seed complete.") print("Seed complete.")
print("=" * 60) print("=" * 60)

View File

@@ -31,7 +31,9 @@ services:
environment: environment:
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-libredecision}:${POSTGRES_PASSWORD:-change-me-in-production}@postgres:5432/${POSTGRES_DB:-libredecision} DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-libredecision}:${POSTGRES_PASSWORD:-change-me-in-production}@postgres:5432/${POSTGRES_DB:-libredecision}
SECRET_KEY: ${SECRET_KEY:-change-me-in-production-with-a-real-secret-key} SECRET_KEY: ${SECRET_KEY:-change-me-in-production-with-a-real-secret-key}
ENVIRONMENT: production
DEBUG: "false" DEBUG: "false"
DEMO_MODE: ${DEMO_MODE:-true}
CORS_ORIGINS: '["https://${DOMAIN:-libredecision.org}"]' CORS_ORIGINS: '["https://${DOMAIN:-libredecision.org}"]'
DUNITER_RPC_URL: ${DUNITER_RPC_URL:-wss://gdev.p2p.legal/ws} DUNITER_RPC_URL: ${DUNITER_RPC_URL:-wss://gdev.p2p.legal/ws}
IPFS_API_URL: http://ipfs:5001 IPFS_API_URL: http://ipfs:5001

View File

@@ -108,7 +108,7 @@ function isActive(to: string) {
<UIcon name="i-lucide-gavel" class="app-header__logo-icon" /> <UIcon name="i-lucide-gavel" class="app-header__logo-icon" />
</span> </span>
<span class="app-header__logo-text"> <span class="app-header__logo-text">
<span class="app-header__logo-g">ğ</span><span class="app-header__logo-paren">(</span><span class="app-header__logo-word">Decision</span><span class="app-header__logo-paren">)</span> <span class="app-header__logo-libre">libre</span><span class="app-header__logo-decision">Decision</span>
</span> </span>
</NuxtLink> </NuxtLink>
</div> </div>
@@ -262,7 +262,7 @@ function isActive(to: string) {
<!-- Footer --> <!-- Footer -->
<footer class="app-footer"> <footer class="app-footer">
<span>ğ(Decision) v0.1.0</span> <span>libreDecision v0.1.0</span>
<span class="app-footer__sep">·</span> <span class="app-footer__sep">·</span>
<span>Licence libre</span> <span>Licence libre</span>
</footer> </footer>
@@ -361,24 +361,17 @@ function isActive(to: string) {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 0; gap: 0;
letter-spacing: -0.01em;
} }
.app-header__logo-g { .app-header__logo-libre {
font-size: 1.5rem; font-size: 1.0625rem;
font-weight: 800; font-weight: 400;
color: var(--mood-accent);
line-height: 1;
font-style: italic; font-style: italic;
}
.app-header__logo-paren {
font-size: 1.125rem;
font-weight: 300;
color: var(--mood-text-muted); color: var(--mood-text-muted);
opacity: 0.5;
} }
.app-header__logo-word { .app-header__logo-decision {
font-size: 1.125rem; font-size: 1.125rem;
font-weight: 700; font-weight: 700;
color: var(--mood-text); color: var(--mood-text);

View File

@@ -1,5 +1,5 @@
/* ========================================================================== /* ==========================================================================
ğ(Decision) — Mood / Ambiance System libreDecision — Mood / Ambiance System
Palettes harmoniques variees, colores en lite, lumineux en dark. Palettes harmoniques variees, colores en lite, lumineux en dark.
========================================================================== */ ========================================================================== */
@@ -144,7 +144,7 @@
} }
/* ========================================================================== /* ==========================================================================
Global design tokens — Nunito, rounded, borderless Global design tokens — Plus Jakarta Sans, rounded, borderless
========================================================================== */ ========================================================================== */
*, *,
@@ -154,12 +154,12 @@
} }
html { html {
font-family: 'Nunito', system-ui, -apple-system, sans-serif; font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;
font-size: 16px; font-size: 16px;
} }
body { body {
font-family: 'Nunito', system-ui, -apple-system, sans-serif; font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif;
transition: background-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, color 0.3s ease;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@@ -234,7 +234,7 @@ input:focus, select:focus, textarea:focus {
:root [class*="u-button"], :root [class*="u-button"],
:root [data-variant] { :root [data-variant] {
border: none !important; border: none !important;
font-family: 'Nunito', system-ui, sans-serif !important; font-family: 'Plus Jakarta Sans', system-ui, sans-serif !important;
} }
:root input, :root input,
@@ -244,5 +244,5 @@ input:focus, select:focus, textarea:focus {
:root [class*="USelect"], :root [class*="USelect"],
:root [class*="UTextarea"] { :root [class*="UTextarea"] {
border: none !important; border: none !important;
font-family: 'Nunito', system-ui, sans-serif !important; font-family: 'Plus Jakarta Sans', system-ui, sans-serif !important;
} }

View File

@@ -121,7 +121,7 @@ const today = new Date().toLocaleDateString('fr-FR', {
<!-- Footer --> <!-- Footer -->
<div class="doc-preview__footer"> <div class="doc-preview__footer">
<div class="doc-preview__footer-main"> <div class="doc-preview__footer-main">
<span>ğ(Decision) · {{ document.title }} · v{{ document.version }}</span> <span>libreDecision · {{ document.title }} · v{{ document.version }}</span>
</div> </div>
<div v-if="mode === 'projected'" class="doc-preview__footer-note"> <div v-if="mode === 'projected'" class="doc-preview__footer-note">
Projection non officielle texte simulé selon {{ changedCount }} vote{{ changedCount > 1 ? 's' : '' }} en cours au {{ today }} Projection non officielle texte simulé selon {{ changedCount }} vote{{ changedCount > 1 ? 's' : '' }} en cours au {{ today }}

View File

@@ -29,7 +29,7 @@ export default defineNuxtConfig({
link: [ link: [
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' }, { rel: 'preconnect', href: 'https://fonts.googleapis.com' },
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' }, { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,700;1,800&display=swap' }, { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400;1,500;1,600;1,700&display=swap' },
{ rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css' }, { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css' },
], ],
script: [ script: [