diff --git a/backend/seed.py b/backend/seed.py
index c130bfd..67f4dc3 100644
--- a/backend/seed.py
+++ b/backend/seed.py
@@ -27,7 +27,7 @@ from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
-from app.database import async_session, engine, Base
+from app.database import async_session, engine, Base, init_db
from app.models.protocol import FormulaConfig, VotingProtocol
from app.models.document import Document, DocumentItem
from app.models.decision import Decision, DecisionStep
@@ -351,7 +351,7 @@ ENGAGEMENT_CERTIFICATION_ITEMS: list[dict] = [
{
"position": "E2",
"item_type": "clause",
- "title": "Certification responsable",
+ "title": "Reciprocite",
"sort_order": 5,
"section_tag": "fondamental",
"inertia_preset": "standard",
@@ -534,7 +534,10 @@ ENGAGEMENT_CERTIFICATION_ITEMS: list[dict] = [
"- Sentinelle : membre ayant recu et emis >= Y[N] certifs "
"(Y = ceil(N^(1/5)))\n"
"- Certifications actives valables **2 ans**\n"
- "- Renouvellement de l'accord tous les **12 mois**"
+ "- Renouvellement de l'accord tous les **12 mois**\n\n"
+ "*Note : le vote porte sur l'inclusion de ces regles dans le document, "
+ "pas sur les valeurs des variables protocolaires elles-memes, "
+ "qui sont fixees par le protocole Duniter.*"
),
},
{
@@ -549,7 +552,10 @@ ENGAGEMENT_CERTIFICATION_ITEMS: list[dict] = [
"- 1 Dividende Universel (DU) par personne par jour\n"
"- Reevaluation a chaque equinoxe : "
"`DU(n+1) = DU(n) + c² × (M/N) / 182.625` avec c = 4.88%\n"
- "- DU(0) = 10.00 G1"
+ "- DU(0) = 10.00 G1\n\n"
+ "*Note : le vote porte sur l'inclusion de ces parametres dans le document, "
+ "pas sur les valeurs monetaires elles-memes, "
+ "qui decoulent de la TRM et du bloc 0.*"
),
},
# ===================================================================
@@ -790,11 +796,11 @@ ENGAGEMENT_CERTIFICATION_ITEMS: list[dict] = [
"title": "Ordonnancement du document",
"sort_order": 33,
"section_tag": "ordonnancement",
- "inertia_preset": "high",
+ "inertia_preset": "standard",
"current_text": (
"L'ordre de presentation des items dans le document est "
"lui-meme soumis au vote. Toute proposition de reorganisation "
- "doit atteindre le seuil d'adoption avec l'inertie haute."
+ "doit atteindre le seuil d'adoption avec l'inertie standard."
),
},
]
@@ -1442,6 +1448,10 @@ async def run_seed():
print("Glibredecision - Seed Database")
print("=" * 60)
+ # Ensure tables exist
+ await init_db()
+ print("[0/7] Tables created.\n")
+
async with async_session() as session:
async with session.begin():
print("\n[1/7] Formula Configs...")
diff --git a/frontend/app/components/documents/EngagementCard.vue b/frontend/app/components/documents/EngagementCard.vue
index 0996363..4d5e565 100644
--- a/frontend/app/components/documents/EngagementCard.vue
+++ b/frontend/app/components/documents/EngagementCard.vue
@@ -46,6 +46,23 @@ const itemTypeLabel = computed(() => {
}
})
+// Mock vote data varies by item for demo — items in "bonnes pratiques" (E8-E11) get lower/mixed votes
+const mockVotes = computed(() => {
+ const order = props.item.sort_order
+ const pos = props.item.position
+
+ // Conseils et bonnes pratiques: varied votes, some non-adopted
+ if (pos === 'E8') return { votesFor: 4, votesAgainst: 3 } // contested
+ if (pos === 'E9') return { votesFor: 2, votesAgainst: 5 } // rejected
+ if (pos === 'E10') return { votesFor: 6, votesAgainst: 2 } // borderline
+ if (pos === 'E11') return { votesFor: 3, votesAgainst: 4 } // rejected
+
+ // Default: well-adopted items
+ const base = ((order * 7 + 13) % 5) + 8 // 8-12
+ const against = (order % 3) // 0-2
+ return { votesFor: base, votesAgainst: against }
+})
+
function navigateToItem() {
navigateTo(`/documents/${props.documentSlug}/items/${props.item.id}`)
}
@@ -102,8 +119,8 @@ function navigateToItem() {
/**
* Genesis block: displays source documents, repos, forum synthesis, and formula trigger
- * for a reference document. Collapsible by default.
+ * for a reference document. Main block collapsible, each sub-section independently collapsible.
*/
const props = defineProps<{
genesisJson: string
@@ -9,6 +9,19 @@ const props = defineProps<{
const expanded = ref(false)
+// Individual section toggles
+const sectionOpen = reactive>({
+ source: true,
+ tools: false,
+ forum: true,
+ process: false,
+ contributors: false,
+})
+
+function toggleSection(key: string) {
+ sectionOpen[key] = !sectionOpen[key]
+}
+
interface GenesisData {
source_document: {
title: string
@@ -88,122 +101,167 @@ const statusLabel = (status: string) => {
-
-
- Document source
-
-
-
- {{ genesis.source_document.title }}
-
-
-
-
- Outils de reference
-
-
-
-
- Synthese des discussions
-
-
-
-
- Processus de depot
-
-
-
- {{ genesis.formula_trigger }}
-
+
+
+
+
+ {{ genesis.formula_trigger }}
+
+
-
-
- Contributeurs
-
-
-
-
{{ c.name }}
-
{{ c.role }}
+
+
+
+
+ {{ c.name }}
+ {{ c.role }}
+
@@ -250,7 +308,29 @@ const statusLabel = (status: string) => {
padding: 0 1.25rem 1.25rem;
display: flex;
flex-direction: column;
- gap: 1.25rem;
+ gap: 0.5rem;
+}
+
+.genesis-section {
+ border-radius: 10px;
+ overflow: hidden;
+ background: color-mix(in srgb, var(--mood-accent) 2%, transparent);
+}
+
+.genesis-section__toggle {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ cursor: pointer;
+ background: none;
+ border: none;
+ transition: background 0.15s;
+}
+
+.genesis-section__toggle:hover {
+ background: color-mix(in srgb, var(--mood-accent) 5%, transparent);
}
.genesis-section__title {
@@ -262,7 +342,11 @@ const statusLabel = (status: string) => {
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--mood-accent);
- margin-bottom: 0.5rem;
+ margin: 0;
+}
+
+.genesis-section__content {
+ padding: 0 0.75rem 0.75rem;
}
.genesis-card {
diff --git a/frontend/app/components/documents/InertiaSlider.vue b/frontend/app/components/documents/InertiaSlider.vue
index b0ddaeb..078f0da 100644
--- a/frontend/app/components/documents/InertiaSlider.vue
+++ b/frontend/app/components/documents/InertiaSlider.vue
@@ -2,7 +2,7 @@
/**
* Inertia slider — displays the inertia preset level for a section.
* Read-only indicator (voting on the preset uses the standard vote flow).
- * Shows the formula parameters underneath.
+ * In full mode: shows formula diagram with simplified curve visualization.
*/
const props = withDefaults(defineProps<{
preset: string
@@ -22,40 +22,86 @@ interface InertiaLevel {
const LEVELS: Record
= {
low: {
- label: 'Basse',
+ label: 'Remplacement facile',
gradient: 0.1,
majority: 50,
color: '#22c55e',
position: 10,
- description: 'Facile a remplacer',
+ description: 'Majorite simple suffit, meme a faible participation',
},
standard: {
- label: 'Standard',
+ label: 'Inertie pour le remplacement',
gradient: 0.2,
majority: 50,
color: '#3b82f6',
position: 37,
- description: 'Equilibre participation/consensus',
+ description: 'Equilibre : consensus croissant avec la participation',
},
high: {
- label: 'Haute',
+ label: 'Remplacement difficile',
gradient: 0.4,
majority: 60,
color: '#f59e0b',
position: 63,
- description: 'Forte mobilisation requise',
+ description: 'Forte mobilisation et super-majorite requises',
},
very_high: {
- label: 'Tres haute',
+ label: 'Remplacement tres difficile',
gradient: 0.6,
majority: 66,
color: '#ef4444',
position: 90,
- description: 'Quasi-unanimite requise',
+ description: 'Quasi-unanimite requise a toute participation',
},
}
const level = computed((): InertiaLevel => LEVELS[props.preset] ?? LEVELS.standard!)
+
+// Generate SVG curve points for the inertia function
+// Formula simplified: Seuil% = M + (1-M) × (1 - (T/W)^G)
+// Where T/W = participation rate, so Seuil% goes from ~100% at low participation to M at full participation
+const curvePath = computed(() => {
+ const G = level.value.gradient
+ const M = level.value.majority / 100
+ const points: string[] = []
+ const steps = 40
+
+ for (let i = 0; i <= steps; i++) {
+ const participation = i / steps // T/W ratio 0..1
+ const threshold = M + (1 - M) * (1 - Math.pow(participation, G))
+ // SVG coordinates: x = participation (0..200), y = threshold inverted (0=100%, 80=20%)
+ const x = 30 + participation * 170
+ const y = 10 + (1 - threshold) * 70
+ points.push(`${x.toFixed(1)},${y.toFixed(1)}`)
+ }
+
+ return `M ${points.join(' L ')}`
+})
+
+// The 4 curve paths for the diagram overlay
+const allCurves = computed(() => {
+ return Object.entries(LEVELS).map(([key, lvl]) => {
+ const G = lvl.gradient
+ const M = lvl.majority / 100
+ const points: string[] = []
+ const steps = 40
+
+ for (let i = 0; i <= steps; i++) {
+ const participation = i / steps
+ const threshold = M + (1 - M) * (1 - Math.pow(participation, G))
+ const x = 30 + participation * 170
+ const y = 10 + (1 - threshold) * 70
+ points.push(`${x.toFixed(1)},${y.toFixed(1)}`)
+ }
+
+ return {
+ key,
+ color: lvl.color,
+ path: `M ${points.join(' L ')}`,
+ active: key === props.preset,
+ }
+ })
+})
@@ -91,6 +137,80 @@ const level = computed((): InertiaLevel => LEVELS[props.preset] ?? LEVELS.standa
{{ level.description }}
+
+
+
+
+
+
+
+ Formule :
+ Seuil = M + (1-M) × (1 - (T/W)G)
+
+
+ T = votes exprimes
+ W = taille WoT
+ M = majorite cible
+ G = gradient d'inertie
+
+
@@ -119,14 +239,11 @@ const level = computed((): InertiaLevel => LEVELS[props.preset] ?? LEVELS.standa
.inertia__fill {
position: absolute;
inset: 0;
+ right: auto;
border-radius: 3px;
transition: width 0.3s ease;
}
-.inertia__fill {
- right: auto;
-}
-
.inertia__thumb {
position: absolute;
top: 50%;
@@ -189,4 +306,83 @@ const level = computed((): InertiaLevel => LEVELS[props.preset] ?? LEVELS.standa
color: var(--mood-text-muted);
line-height: 1.3;
}
+
+/* Diagram */
+.inertia__diagram {
+ margin-top: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.inertia__svg {
+ width: 100%;
+ max-width: 320px;
+ height: auto;
+}
+
+.inertia__axis {
+ stroke: color-mix(in srgb, var(--mood-text) 25%, transparent);
+ stroke-width: 1;
+}
+
+.inertia__grid {
+ stroke: color-mix(in srgb, var(--mood-text) 8%, transparent);
+ stroke-width: 0.5;
+ stroke-dasharray: 2 4;
+}
+
+.inertia__majority-line {
+ stroke: var(--mood-accent);
+ stroke-width: 0.75;
+ stroke-dasharray: 4 3;
+ opacity: 0.5;
+}
+
+.inertia__axis-label {
+ font-size: 5px;
+ fill: var(--mood-text-muted);
+ font-family: monospace;
+}
+
+.inertia__axis-title {
+ font-size: 5px;
+ fill: var(--mood-text-muted);
+ font-weight: 600;
+}
+
+.inertia__formula {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+ flex-wrap: wrap;
+}
+
+.inertia__formula-label {
+ font-size: 0.625rem;
+ font-weight: 600;
+ color: var(--mood-text-muted);
+}
+
+.inertia__formula-code {
+ font-size: 0.6875rem;
+ font-family: monospace;
+ color: var(--mood-text);
+ padding: 0.125rem 0.375rem;
+ border-radius: 4px;
+ background: color-mix(in srgb, var(--mood-accent) 6%, var(--mood-bg));
+}
+
+.inertia__formula-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ font-size: 0.5625rem;
+ color: var(--mood-text-muted);
+}
+
+.inertia__formula-legend strong {
+ color: var(--mood-text);
+ font-weight: 700;
+}
diff --git a/frontend/app/components/documents/MiniVoteBoard.vue b/frontend/app/components/documents/MiniVoteBoard.vue
index 4f7ba52..0cdaf8e 100644
--- a/frontend/app/components/documents/MiniVoteBoard.vue
+++ b/frontend/app/components/documents/MiniVoteBoard.vue
@@ -81,36 +81,28 @@ function formatDate(d: string): string {
-
+