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 }} -

-
- - - Texte officiel - - - - Depot git - + +
+
+

+ {{ 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, + } + }) +}) @@ -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 {