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,149 @@
/**
* Documents store: reference documents, their items, and item versions.
*
* Maps to the backend /api/v1/documents endpoints.
*/
export interface DocumentItem {
id: string
document_id: string
position: string
item_type: string
title: string | null
current_text: string
voting_protocol_id: string | null
sort_order: number
created_at: string
updated_at: string
}
export interface Document {
id: string
slug: string
title: string
doc_type: string
version: string
status: string
description: string | null
ipfs_cid: string | null
chain_anchor: string | null
created_at: string
updated_at: string
items_count: number
}
export interface DocumentCreate {
slug: string
title: string
doc_type: string
description?: string | null
version?: string
}
interface DocumentsState {
list: Document[]
current: Document | null
items: DocumentItem[]
loading: boolean
error: string | null
}
export const useDocumentsStore = defineStore('documents', {
state: (): DocumentsState => ({
list: [],
current: null,
items: [],
loading: false,
error: null,
}),
getters: {
byType: (state) => {
return (docType: string) => state.list.filter(d => d.doc_type === docType)
},
activeDocuments: (state): Document[] => {
return state.list.filter(d => d.status === 'active')
},
draftDocuments: (state): Document[] => {
return state.list.filter(d => d.status === 'draft')
},
},
actions: {
/**
* Fetch all documents with optional filters.
*/
async fetchAll(params?: { doc_type?: string; status?: string }) {
this.loading = true
this.error = null
try {
const { $api } = useApi()
const query: Record<string, string> = {}
if (params?.doc_type) query.doc_type = params.doc_type
if (params?.status) query.status = params.status
this.list = await $api<Document[]>('/documents/', { query })
} catch (err: any) {
this.error = err?.data?.detail || err?.message || 'Erreur lors du chargement des documents'
} finally {
this.loading = false
}
},
/**
* Fetch a single document by slug and its items.
*/
async fetchBySlug(slug: string) {
this.loading = true
this.error = null
try {
const { $api } = useApi()
const [doc, items] = await Promise.all([
$api<Document>(`/documents/${slug}`),
$api<DocumentItem[]>(`/documents/${slug}/items`),
])
this.current = doc
this.items = items
} catch (err: any) {
this.error = err?.data?.detail || err?.message || 'Document introuvable'
} finally {
this.loading = false
}
},
/**
* Create a new reference document.
*/
async createDocument(payload: DocumentCreate) {
this.loading = true
this.error = null
try {
const { $api } = useApi()
const doc = await $api<Document>('/documents/', {
method: 'POST',
body: payload,
})
this.list.unshift(doc)
return doc
} catch (err: any) {
this.error = err?.data?.detail || err?.message || 'Erreur lors de la creation du document'
throw err
} finally {
this.loading = false
}
},
/**
* Clear the current document and items.
*/
clearCurrent() {
this.current = null
this.items = []
},
},
})