Sprint 1 : scaffolding complet de Glibredecision

Plateforme de decisions collectives pour Duniter/G1.
Backend FastAPI async + PostgreSQL (14 tables, 8 routers, 6 services,
moteur de vote avec formule d'inertie WoT/Smith/TechComm).
Frontend Nuxt 4 + Nuxt UI v3 + Pinia (9 pages, 5 stores).
Infrastructure Docker + Woodpecker CI + Traefik.
Documentation technique et utilisateur (15 fichiers).
Seed : Licence G1, Engagement Forgeron v2.0.0, 4 protocoles de vote.
30 tests unitaires (formules, mode params, vote nuance) -- tous verts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-02-28 12:46:11 +01:00
commit 25437f24e3
100 changed files with 10236 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
"""Sanctuary router: IPFS + on-chain anchoring entries."""
from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.sanctuary import SanctuaryEntry
from app.models.user import DuniterIdentity
from app.schemas.sanctuary import SanctuaryEntryCreate, SanctuaryEntryOut
from app.services.auth_service import get_current_identity
router = APIRouter()
@router.get("/", response_model=list[SanctuaryEntryOut])
async def list_entries(
db: AsyncSession = Depends(get_db),
entry_type: str | None = Query(default=None, description="Filtrer par type (document, decision, vote_result)"),
skip: int = Query(default=0, ge=0),
limit: int = Query(default=50, ge=1, le=200),
) -> list[SanctuaryEntryOut]:
"""List all sanctuary entries with optional type filter."""
stmt = select(SanctuaryEntry)
if entry_type is not None:
stmt = stmt.where(SanctuaryEntry.entry_type == entry_type)
stmt = stmt.order_by(SanctuaryEntry.created_at.desc()).offset(skip).limit(limit)
result = await db.execute(stmt)
entries = result.scalars().all()
return [SanctuaryEntryOut.model_validate(e) for e in entries]
@router.get("/{id}", response_model=SanctuaryEntryOut)
async def get_entry(
id: uuid.UUID,
db: AsyncSession = Depends(get_db),
) -> SanctuaryEntryOut:
"""Get a single sanctuary entry by ID."""
result = await db.execute(select(SanctuaryEntry).where(SanctuaryEntry.id == id))
entry = result.scalar_one_or_none()
if entry is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Entree sanctuaire introuvable")
return SanctuaryEntryOut.model_validate(entry)
@router.post("/", response_model=SanctuaryEntryOut, status_code=status.HTTP_201_CREATED)
async def create_entry(
payload: SanctuaryEntryCreate,
db: AsyncSession = Depends(get_db),
identity: DuniterIdentity = Depends(get_current_identity),
) -> SanctuaryEntryOut:
"""Create a new sanctuary entry.
This endpoint is typically called by internal services after:
1. Content is hashed (SHA-256)
2. Content is pinned to IPFS
3. Hash is anchored on-chain via system.remark
The IPFS CID and chain TX hash can be added later via updates.
"""
entry = SanctuaryEntry(**payload.model_dump())
db.add(entry)
await db.commit()
await db.refresh(entry)
return SanctuaryEntryOut.model_validate(entry)