Accents FR, architecture modulaire, protocoles opérationnels

- Fix accents manquants dans 7 pages UI (décisions, boîte à outils, etc.)
- Titres accueil enrichis : Décisions structurantes, Documents de référence,
  Mandats et nominations, Protocoles et fonctionnement
- Retrait Embarquement Forgeron du seed (n'est pas une Decision)
- 2 protocoles opérationnels dans Protocoles : Embarquement Forgeron
  (lié à l'Acte d'engagement) + Soumission Runtime Upgrade (lié à la
  Décision Runtime Upgrade) avec timeline et liens croisés signalétiques
- Décision Runtime Upgrade : badge on-chain + lien protocole + contexte
- Document [slug] : lien protocole dans la section Qualification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-03-03 07:05:55 +01:00
parent c19c1aa55e
commit 8201e73d7c
8 changed files with 328 additions and 221 deletions

View File

@@ -1994,9 +1994,16 @@ async def seed_decision_runtime_upgrade(session: AsyncSession) -> Decision:
"title", "title",
"Runtime Upgrade", "Runtime Upgrade",
description=( description=(
"Processus de mise à jour du runtime Duniter V2 on-chain. " "Décision on-chain — Mise à jour du runtime Duniter V2. "
"Chaque upgrade suit un protocole strict en 5 étapes. " "Requiert le seuil multi-critères : WoT + Smith + TechComm. "
"(Décision on-chain)" "Protocole lié : Soumission Runtime Upgrade."
),
context=(
"Chaque mise à jour du runtime Duniter V2 est une décision "
"on-chain qui nécessite l'approbation simultanée de trois "
"critères : le vote communautaire WoT, le quorum des "
"forgerons Smith, et la validation du Comité Technique. "
"Le seuil d'adoption est calculé selon la formule multi-critères."
), ),
decision_type="runtime_upgrade", decision_type="runtime_upgrade",
status="draft", status="draft",
@@ -2013,108 +2020,6 @@ async def seed_decision_runtime_upgrade(session: AsyncSession) -> Decision:
return decision return decision
# ---------------------------------------------------------------------------
# Seed: Decision - Embarquement Forgeron (protocole de qualification)
#
# Processus d'onboarding des forgerons en jalons progressifs.
# Lié explicitement à la section "Qualification" de l'Acte d'engagement.
# Cf. Yvv, ML#32603 post #3 : "compétence sécu et réactivité sont
# davantage les sujets de l'enrôlement et accompagnement (onboarding)"
# Cf. elois, Duniter#9047 : séquence miroir → sync → membership → certifs
# ---------------------------------------------------------------------------
EMBARQUEMENT_FORGERON_STEPS: list[dict] = [
{
"step_order": 1,
"step_type": "candidature",
"title": "Candidature",
"description": (
"Déclaration d'intention auprès de la communauté forgeron. "
"Premier contact avec un forgeron référent qui accepte "
"d'accompagner le parcours. Vérification du statut membre "
"de la toile de confiance principale."
),
},
{
"step_order": 2,
"step_type": "mirror",
"title": "Nœud miroir",
"description": (
"Déployer un nœud miroir Duniter V2 et le maintenir "
"synchronisé pendant une période probatoire. Démontrer "
"les compétences d'administration système Linux : "
"installation, configuration réseau, monitoring. "
"Le nœud doit être joignable et à jour."
),
},
{
"step_order": 3,
"step_type": "evaluation",
"title": "Évaluation technique",
"description": (
"Vérification des compétences techniques par un forgeron "
"certificateur. Parcours de la checklist d'engagement dans "
"un ordre aléatoire. Le candidat doit démontrer :\n"
"- Gestion de clés et sécurité\n"
"- Compréhension du consensus Duniter\n"
"- Capacité de diagnostic et réactivité\n"
"Toute réponse incorrecte interrompt la procédure."
),
},
{
"step_order": 4,
"step_type": "certification",
"title": "Certification Smith",
"description": (
"Invitation par un forgeron existant, acceptation de "
"l'invitation, puis réception de 3 certifications forgeron. "
"Chaque certificateur a vérifié les compétences du candidat "
"via la checklist. La certification engage la responsabilité "
"du certificateur (section Certificateur de l'engagement)."
),
},
{
"step_order": 5,
"step_type": "online",
"title": "Mise en ligne",
"description": (
"Passage du nœud validateur en mode online (goOnline). "
"Le forgeron commence à produire des blocs. "
"Surveillance active pendant les premières semaines : "
"uptime, synchronisation, réactivité aux alertes. "
"Le forgeron est pleinement soumis à l'ensemble des "
"engagements de l'acte."
),
},
]
async def seed_decision_embarquement_forgeron(session: AsyncSession) -> Decision:
decision, created = await get_or_create(
session,
Decision,
"title",
"Embarquement Forgeron",
description=(
"Protocole d'embarquement (onboarding) des forgerons Duniter V2. "
"Parcours en 5 jalons progressifs de la candidature à la mise en "
"ligne du nœud validateur. Lié à la section Qualification de "
"l'Acte d'engagement forgeron."
),
decision_type="onboarding",
status="active",
)
print(f" Decision 'Embarquement Forgeron': {'created' if created else 'exists'}")
if created:
for step_data in EMBARQUEMENT_FORGERON_STEPS:
step = DecisionStep(decision_id=decision.id, **step_data)
session.add(step)
await session.flush()
print(f" -> {len(EMBARQUEMENT_FORGERON_STEPS)} steps created")
return decision
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Seed: Simulated voters + votes on first 3 engagement items # Seed: Simulated voters + votes on first 3 engagement items
@@ -2265,16 +2170,13 @@ async def run_seed():
print("\n[4/8] Document: Acte d'engagement forgeron v2.0.0...") print("\n[4/8] 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/8] Decision: Runtime Upgrade...") print("\n[5/7] Decision: Runtime Upgrade...")
await seed_decision_runtime_upgrade(session) await seed_decision_runtime_upgrade(session)
print("\n[6/8] Decision: Embarquement Forgeron...") print("\n[6/7] Simulated voters...")
await seed_decision_embarquement_forgeron(session)
print("\n[7/8] Simulated voters...")
voters = await seed_voters(session) voters = await seed_voters(session)
print("\n[8/8] Votes on first 3 engagements forgeron...") print("\n[7/7] Votes on first 3 engagements forgeron...")
await seed_votes_on_items( await seed_votes_on_items(
session, session,
doc_forgeron, doc_forgeron,

View File

@@ -3,7 +3,7 @@
* Decisions — page index. * Decisions — page index.
* *
* Utilise SectionLayout avec status filters, recherche, tri, * Utilise SectionLayout avec status filters, recherche, tri,
* et sidebar "Boite a outils" affichant les protocoles de vote. * et sidebar "Boîte à outils" affichant les protocoles de vote.
*/ */
const decisions = useDecisionsStore() const decisions = useDecisionsStore()
const protocols = useProtocolsStore() const protocols = useProtocolsStore()
@@ -28,7 +28,7 @@ onMounted(async () => {
/** Status filter pills with counts. */ /** Status filter pills with counts. */
const statuses = computed(() => [ const statuses = computed(() => [
{ id: 'draft', label: 'En prepa', count: decisions.list.filter(d => d.status === 'draft').length }, { id: 'draft', label: 'En prépa', count: decisions.list.filter(d => d.status === 'draft').length },
{ id: 'voting', label: 'En vote', count: decisions.list.filter(d => d.status === 'voting' || d.status === 'qualification' || d.status === 'review').length }, { id: 'voting', label: 'En vote', count: decisions.list.filter(d => d.status === 'voting' || d.status === 'qualification' || d.status === 'review').length },
{ id: 'executed', label: 'En vigueur', count: decisions.list.filter(d => d.status === 'executed').length }, { id: 'executed', label: 'En vigueur', count: decisions.list.filter(d => d.status === 'executed').length },
{ id: 'closed', label: 'Clos', count: decisions.list.filter(d => d.status === 'closed').length }, { id: 'closed', label: 'Clos', count: decisions.list.filter(d => d.status === 'closed').length },
@@ -97,8 +97,8 @@ function formatDate(dateStr: string): string {
<template> <template>
<SectionLayout <SectionLayout
title="Decisions" title="Décisions"
subtitle="Processus de decision collectifs" subtitle="Processus de décision collectifs"
:statuses="statuses" :statuses="statuses"
:active-status="activeStatus" :active-status="activeStatus"
@update:active-status="activeStatus = $event" @update:active-status="activeStatus = $event"
@@ -111,7 +111,7 @@ function formatDate(dateStr: string): string {
v-model="searchQuery" v-model="searchQuery"
type="text" type="text"
class="search-field__input" class="search-field__input"
placeholder="Rechercher une decision..." placeholder="Rechercher une décision..."
/> />
</div> </div>
<select v-model="sortBy" class="sort-select"> <select v-model="sortBy" class="sort-select">
@@ -149,7 +149,7 @@ function formatDate(dateStr: string): string {
style="color: var(--mood-text-muted);" style="color: var(--mood-text-muted);"
> >
<UIcon name="i-lucide-scale" class="text-4xl mb-3 block mx-auto" /> <UIcon name="i-lucide-scale" class="text-4xl mb-3 block mx-auto" />
<p>Aucune decision trouvee</p> <p>Aucune décision trouvée</p>
<p v-if="searchQuery || activeStatus" class="text-sm mt-1"> <p v-if="searchQuery || activeStatus" class="text-sm mt-1">
Essayez de modifier vos filtres Essayez de modifier vos filtres
</p> </p>
@@ -179,15 +179,33 @@ function formatDate(dateStr: string): string {
<span class="decision-card__type-badge"> <span class="decision-card__type-badge">
{{ typeLabel(decision.decision_type) }} {{ typeLabel(decision.decision_type) }}
</span> </span>
<span
v-if="decision.decision_type === 'runtime_upgrade'"
class="decision-card__onchain-badge"
>
<UIcon name="i-lucide-link" class="text-xs" />
on-chain
</span>
<span class="decision-card__steps"> <span class="decision-card__steps">
<UIcon name="i-lucide-layers" class="text-xs" /> <UIcon name="i-lucide-layers" class="text-xs" />
{{ decision.steps.length }} etape{{ decision.steps.length !== 1 ? 's' : '' }} {{ decision.steps.length }} étape{{ decision.steps.length !== 1 ? 's' : '' }}
</span> </span>
<span class="decision-card__date"> <span class="decision-card__date">
<UIcon name="i-lucide-clock" class="text-xs" /> <UIcon name="i-lucide-clock" class="text-xs" />
{{ formatDate(decision.created_at) }} {{ formatDate(decision.created_at) }}
</span> </span>
</div> </div>
<!-- Protocol link for runtime_upgrade -->
<NuxtLink
v-if="decision.decision_type === 'runtime_upgrade'"
to="/protocols"
class="decision-card__protocol-link"
@click.stop
>
<UIcon name="i-lucide-git-branch" class="text-xs" />
<span>Protocole : Soumission Runtime Upgrade</span>
<UIcon name="i-lucide-arrow-right" class="text-xs" />
</NuxtLink>
</div> </div>
</div> </div>
</template> </template>
@@ -196,21 +214,21 @@ function formatDate(dateStr: string): string {
<template #toolbox> <template #toolbox>
<ToolboxVignette <ToolboxVignette
title="Vote majoritaire WoT" title="Vote majoritaire WoT"
:bullets="['Seuil adaptatif a la participation', 'Formule g1vote inertielle']" :bullets="['Seuil adaptatif à la participation', 'Formule g1vote inertielle']"
:actions="[ :actions="[
{ label: 'Simuler', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true }, { label: 'Simuler', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true },
]" ]"
/> />
<ToolboxVignette <ToolboxVignette
title="Vote nuance" title="Vote nuancé"
:bullets="['6 niveaux de preference', 'Seuil de satisfaction 80%']" :bullets="['6 niveaux de préférence', 'Seuil de satisfaction 80%']"
:actions="[ :actions="[
{ label: 'Voir', icon: 'i-lucide-bar-chart-3', emit: 'nuance' }, { label: 'Voir', icon: 'i-lucide-bar-chart-3', emit: 'nuance' },
]" ]"
/> />
<ToolboxVignette <ToolboxVignette
title="Mandature" title="Mandature"
:bullets="['Election en binome', 'Transparence et revocation']" :bullets="['Élection en binôme', 'Transparence et révocation']"
:actions="[ :actions="[
{ label: 'Mandats', icon: 'i-lucide-user-check', to: '/mandates', primary: true }, { label: 'Mandats', icon: 'i-lucide-user-check', to: '/mandates', primary: true },
]" ]"
@@ -337,6 +355,40 @@ function formatDate(dateStr: string): string {
color: var(--mood-accent); color: var(--mood-accent);
} }
.decision-card__onchain-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 3px 8px;
border-radius: 20px;
background: color-mix(in srgb, var(--mood-success) 15%, transparent);
color: var(--mood-success);
}
.decision-card__protocol-link {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: 20px;
text-decoration: none;
background: color-mix(in srgb, var(--mood-tertiary, var(--mood-accent)) 10%, transparent);
color: var(--mood-tertiary, var(--mood-accent));
transition: transform 0.12s ease, box-shadow 0.12s ease;
width: fit-content;
}
.decision-card__protocol-link:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px var(--mood-shadow);
}
/* --- Modern search / sort / action --- */ /* --- Modern search / sort / action --- */
.search-field { .search-field {
flex: 1; flex: 1;

View File

@@ -332,6 +332,20 @@ function toggleSection(tag: string) {
</div> </div>
</button> </button>
<!-- Protocol link for qualification section -->
<NuxtLink
v-if="section.tag === 'qualification' && !collapsedSections[section.tag]"
to="/protocols"
class="doc-page__protocol-link"
>
<UIcon name="i-lucide-git-branch" class="text-sm" />
<div>
<span class="doc-page__protocol-link-label">Protocole lié</span>
<span class="doc-page__protocol-link-name">Embarquement Forgeron</span>
</div>
<UIcon name="i-lucide-arrow-right" class="text-sm doc-page__protocol-link-arrow" />
</NuxtLink>
<!-- Items (collapsible) --> <!-- Items (collapsible) -->
<Transition name="section-collapse"> <Transition name="section-collapse">
<div v-show="!collapsedSections[section.tag]" class="doc-page__section-items"> <div v-show="!collapsedSections[section.tag]" class="doc-page__section-items">
@@ -531,6 +545,51 @@ function toggleSection(tag: string) {
gap: 0.75rem; gap: 0.75rem;
} }
/* Protocol link */
.doc-page__protocol-link {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: color-mix(in srgb, var(--mood-tertiary, var(--mood-accent)) 8%, var(--mood-surface));
border: 1px solid color-mix(in srgb, var(--mood-tertiary, var(--mood-accent)) 15%, transparent);
border-radius: 14px;
text-decoration: none;
transition: transform 0.12s ease, box-shadow 0.12s ease;
color: var(--mood-tertiary, var(--mood-accent));
}
.doc-page__protocol-link:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--mood-shadow);
}
.doc-page__protocol-link-label {
display: block;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--mood-text-muted);
}
.doc-page__protocol-link-name {
display: block;
font-size: 0.875rem;
font-weight: 700;
color: var(--mood-text);
}
.doc-page__protocol-link-arrow {
margin-left: auto;
opacity: 0.3;
transition: opacity 0.12s;
}
.doc-page__protocol-link:hover .doc-page__protocol-link-arrow {
opacity: 1;
}
/* Section collapse transition */ /* Section collapse transition */
.section-collapse-enter-active, .section-collapse-enter-active,
.section-collapse-leave-active { .section-collapse-leave-active {

View File

@@ -3,7 +3,7 @@
* Documents de reference — page index. * Documents de reference — page index.
* *
* Utilise SectionLayout avec status filters, recherche, tri, * Utilise SectionLayout avec status filters, recherche, tri,
* et sidebar "Boite a outils" affichant les protocoles de vote. * et sidebar "Boîte à outils" affichant les protocoles de vote.
*/ */
import type { DocumentCreate } from '~/stores/documents' import type { DocumentCreate } from '~/stores/documents'
@@ -29,7 +29,7 @@ const creating = ref(false)
const newDocTypeOptions = [ const newDocTypeOptions = [
{ label: 'Licence', value: 'licence' }, { label: 'Licence', value: 'licence' },
{ label: 'Engagement', value: 'engagement' }, { label: 'Engagement', value: 'engagement' },
{ label: 'Reglement', value: 'reglement' }, { label: 'Règlement', value: 'reglement' },
{ label: 'Constitution', value: 'constitution' }, { label: 'Constitution', value: 'constitution' },
] ]
@@ -48,7 +48,7 @@ onMounted(async () => {
/** Status filter pills with counts. */ /** Status filter pills with counts. */
const statuses = computed(() => [ const statuses = computed(() => [
{ id: 'draft', label: 'En prepa', count: documents.list.filter(d => d.status === 'draft').length }, { id: 'draft', label: 'En prépa', count: documents.list.filter(d => d.status === 'draft').length },
{ id: 'voting', label: 'En vote', count: documents.list.filter(d => d.status === 'voting').length }, { id: 'voting', label: 'En vote', count: documents.list.filter(d => d.status === 'voting').length },
{ id: 'active', label: 'En vigueur', count: documents.list.filter(d => d.status === 'active').length }, { id: 'active', label: 'En vigueur', count: documents.list.filter(d => d.status === 'active').length },
{ id: 'archived', label: 'Clos', count: documents.list.filter(d => d.status === 'archived').length }, { id: 'archived', label: 'Clos', count: documents.list.filter(d => d.status === 'archived').length },
@@ -92,7 +92,7 @@ const typeLabel = (docType: string): string => {
switch (docType) { switch (docType) {
case 'licence': return 'Licence' case 'licence': return 'Licence'
case 'engagement': return 'Engagement' case 'engagement': return 'Engagement'
case 'reglement': return 'Reglement' case 'reglement': return 'Règlement'
case 'constitution': return 'Constitution' case 'constitution': return 'Constitution'
default: return docType default: return docType
} }
@@ -154,8 +154,8 @@ async function createDocument() {
<template> <template>
<SectionLayout <SectionLayout
title="Documents de reference" title="Documents de référence"
subtitle="Textes fondateurs sous vote permanent de la communaute" subtitle="Textes fondateurs sous vote permanent de la communauté"
:statuses="statuses" :statuses="statuses"
:active-status="activeStatus" :active-status="activeStatus"
@update:active-status="activeStatus = $event" @update:active-status="activeStatus = $event"
@@ -206,7 +206,7 @@ async function createDocument() {
style="color: var(--mood-text-muted);" style="color: var(--mood-text-muted);"
> >
<UIcon name="i-lucide-book-open" class="text-4xl mb-3 block mx-auto" /> <UIcon name="i-lucide-book-open" class="text-4xl mb-3 block mx-auto" />
<p>Aucun document trouve</p> <p>Aucun document trouvé</p>
<p v-if="searchQuery || activeStatus" class="text-sm mt-1"> <p v-if="searchQuery || activeStatus" class="text-sm mt-1">
Essayez de modifier vos filtres Essayez de modifier vos filtres
</p> </p>
@@ -253,7 +253,7 @@ async function createDocument() {
<template #toolbox> <template #toolbox>
<ToolboxVignette <ToolboxVignette
title="Modules" title="Modules"
:bullets="['Structurer en sections et clauses', 'Vote independant par clause']" :bullets="['Structurer en sections et clauses', 'Vote indépendant par clause']"
:actions="[ :actions="[
{ label: 'Voir', icon: 'i-lucide-puzzle', emit: 'modules' }, { label: 'Voir', icon: 'i-lucide-puzzle', emit: 'modules' },
]" ]"
@@ -267,7 +267,7 @@ async function createDocument() {
/> />
<ToolboxVignette <ToolboxVignette
title="Inertie de remplacement" title="Inertie de remplacement"
:bullets="['4 niveaux de difficulte', 'Protege les textes fondamentaux']" :bullets="['4 niveaux de difficulté', 'Protège les textes fondamentaux']"
:actions="[ :actions="[
{ label: 'Simuler', icon: 'i-lucide-sliders-horizontal', to: '/protocols/formulas', primary: true }, { label: 'Simuler', icon: 'i-lucide-sliders-horizontal', to: '/protocols/formulas', primary: true },
]" ]"
@@ -280,7 +280,7 @@ async function createDocument() {
<template #content> <template #content>
<div class="p-4 sm:p-6 space-y-4"> <div class="p-4 sm:p-6 space-y-4">
<h3 class="text-base sm:text-lg font-semibold" style="color: var(--mood-text);"> <h3 class="text-base sm:text-lg font-semibold" style="color: var(--mood-text);">
Nouveau document de reference Nouveau document de référence
</h3> </h3>
<div class="space-y-4"> <div class="space-y-4">
@@ -335,7 +335,7 @@ async function createDocument() {
<UTextarea <UTextarea
v-model="newDoc.description" v-model="newDoc.description"
:rows="3" :rows="3"
placeholder="Decrivez brievement ce document..." placeholder="Décrivez brièvement ce document..."
class="w-full" class="w-full"
/> />
</div> </div>
@@ -349,7 +349,7 @@ async function createDocument() {
@click="showNewDocModal = false" @click="showNewDocModal = false"
/> />
<UButton <UButton
label="Creer le document" label="Créer le document"
icon="i-lucide-plus" icon="i-lucide-plus"
color="primary" color="primary"
:loading="creating" :loading="creating"

View File

@@ -23,18 +23,18 @@ onMounted(async () => {
const entryCards = computed(() => [ const entryCards = computed(() => [
{ {
key: 'decisions', key: 'decisions',
title: 'Decisions', title: 'Décisions structurantes',
icon: 'i-lucide-scale', icon: 'i-lucide-scale',
to: '/decisions', to: '/decisions',
count: decisions.activeDecisions.length, count: decisions.activeDecisions.length,
countLabel: `${decisions.activeDecisions.length} en cours`, countLabel: `${decisions.activeDecisions.length} en cours`,
totalLabel: `${decisions.list.length} au total`, totalLabel: `${decisions.list.length} au total`,
description: 'Processus de decision collectifs', description: 'Processus de décision collectifs',
color: 'var(--mood-secondary, var(--mood-accent))', color: 'var(--mood-secondary, var(--mood-accent))',
}, },
{ {
key: 'documents', key: 'documents',
title: 'Documents', title: 'Documents de référence',
icon: 'i-lucide-book-open', icon: 'i-lucide-book-open',
to: '/documents', to: '/documents',
count: documents.activeDocuments.length, count: documents.activeDocuments.length,
@@ -45,24 +45,24 @@ const entryCards = computed(() => [
}, },
{ {
key: 'mandats', key: 'mandats',
title: 'Mandats', title: 'Mandats et nominations',
icon: 'i-lucide-user-check', icon: 'i-lucide-user-check',
to: '/mandates', to: '/mandates',
count: null, count: null,
countLabel: null, countLabel: null,
totalLabel: null, totalLabel: null,
description: 'Missions deleguees avec nomination en binome', description: 'Missions déléguées avec nomination en binôme',
color: 'var(--mood-success)', color: 'var(--mood-success)',
}, },
{ {
key: 'protocoles', key: 'protocoles',
title: 'Protocoles', title: 'Protocoles et fonctionnement',
icon: 'i-lucide-settings', icon: 'i-lucide-settings',
to: '/protocols', to: '/protocols',
count: protocols.protocols.length, count: protocols.protocols.length,
countLabel: `${protocols.protocols.length} modalite${protocols.protocols.length > 1 ? 's' : ''}`, countLabel: `${protocols.protocols.length} modalité${protocols.protocols.length > 1 ? 's' : ''}`,
totalLabel: 'Boite a outils de vote + workflows', totalLabel: 'Boîte à outils de vote + workflows',
description: 'Modalites de vote, formules, workflows', description: 'Modalités de vote, formules, workflows',
color: 'var(--mood-tertiary, var(--mood-accent))', color: 'var(--mood-tertiary, var(--mood-accent))',
}, },
]) ])
@@ -81,7 +81,7 @@ function formatDate(dateStr: string): string {
if (diffHours < 1) { if (diffHours < 1) {
const diffMinutes = Math.floor(diffMs / (1000 * 60)) const diffMinutes = Math.floor(diffMs / (1000 * 60))
return diffMinutes <= 1 ? 'A l\'instant' : `Il y a ${diffMinutes} min` return diffMinutes <= 1 ? 'À l\'instant' : `Il y a ${diffMinutes} min`
} }
if (diffHours < 24) { if (diffHours < 24) {
return `Il y a ${Math.floor(diffHours)}h` return `Il y a ${Math.floor(diffHours)}h`
@@ -101,7 +101,7 @@ function formatDate(dateStr: string): string {
<span class="dash__title-g">ğ</span><span class="dash__title-paren">(</span>Decision<span class="dash__title-paren">)</span> <span class="dash__title-g">ğ</span><span class="dash__title-paren">(</span>Decision<span class="dash__title-paren">)</span>
</h1> </h1>
<p class="dash__subtitle"> <p class="dash__subtitle">
Decisions collectives pour la communaute Duniter / G1 Décisions collectives pour la communauté Duniter / G1
</p> </p>
</div> </div>
@@ -141,7 +141,7 @@ function formatDate(dateStr: string): string {
<div class="dash__connect-left"> <div class="dash__connect-left">
<UIcon name="i-lucide-key-round" class="text-lg" /> <UIcon name="i-lucide-key-round" class="text-lg" />
<div> <div>
<p class="dash__connect-text">Connectez-vous avec votre identite Duniter pour participer.</p> <p class="dash__connect-text">Connectez-vous avec votre identité Duniter pour participer.</p>
<p class="dash__connect-hint">Signature Ed25519 · aucun mot de passe</p> <p class="dash__connect-hint">Signature Ed25519 · aucun mot de passe</p>
</div> </div>
</div> </div>
@@ -158,7 +158,7 @@ function formatDate(dateStr: string): string {
<UIcon name="i-lucide-wrench" class="text-xl" /> <UIcon name="i-lucide-wrench" class="text-xl" />
</div> </div>
<div class="dash__toolbox-card-body"> <div class="dash__toolbox-card-body">
<h3 class="dash__toolbox-card-title">Boite a outils</h3> <h3 class="dash__toolbox-card-title">Boîte à outils</h3>
<p class="dash__toolbox-card-desc"> <p class="dash__toolbox-card-desc">
Simulateur de formules, modules de vote, workflows Simulateur de formules, modules de vote, workflows
</p> </p>
@@ -177,7 +177,7 @@ function formatDate(dateStr: string): string {
<div v-if="recentDecisions.length > 0" class="dash__activity"> <div v-if="recentDecisions.length > 0" class="dash__activity">
<div class="dash__activity-head"> <div class="dash__activity-head">
<UIcon name="i-lucide-activity" class="text-lg" /> <UIcon name="i-lucide-activity" class="text-lg" />
<h3>Activite recente</h3> <h3>Activité récente</h3>
</div> </div>
<div class="dash__activity-list"> <div class="dash__activity-list">
<NuxtLink <NuxtLink
@@ -207,7 +207,7 @@ function formatDate(dateStr: string): string {
<template #content> <template #content>
<div class="dash__formula-body"> <div class="dash__formula-body">
<p class="dash__formula-desc"> <p class="dash__formula-desc">
Le seuil s'adapte a la participation : faible = quasi-unanimite ; forte = majorite simple. Le seuil s'adapte à la participation : faible = quasi-unanimité ; forte = majorité simple.
</p> </p>
<code class="dash__formula-code"> <code class="dash__formula-code">
Seuil = C + B^W + (M + (1-M) * (1 - (T/W)^G)) * max(0, T-C) Seuil = C + B^W + (M + (1-M) * (1 - (T/W)^G)) * max(0, T-C)
@@ -217,7 +217,7 @@ function formatDate(dateStr: string): string {
<span>B = base</span> <span>B = base</span>
<span>W = taille WoT</span> <span>W = taille WoT</span>
<span>T = votes</span> <span>T = votes</span>
<span>M = majorite</span> <span>M = majorité</span>
<span>G = gradient</span> <span>G = gradient</span>
</div> </div>
<NuxtLink to="/protocols/formulas" class="dash__formula-link"> <NuxtLink to="/protocols/formulas" class="dash__formula-link">

View File

@@ -3,8 +3,8 @@
* Mandats — page index. * Mandats — page index.
* *
* Utilise SectionLayout avec status filters, recherche, * Utilise SectionLayout avec status filters, recherche,
* et sidebar "Boite a outils" affichant les protocoles de vote. * et sidebar "Boîte à outils" affichant les protocoles de vote.
* Etat vide enrichi avec onboarding expliquant le concept de mandat. * État vide enrichi avec onboarding expliquant le concept de mandat.
*/ */
import type { MandateCreate } from '~/stores/mandates' import type { MandateCreate } from '~/stores/mandates'
@@ -25,9 +25,9 @@ const sortOptions = [
// Create mandate modal state // Create mandate modal state
const showCreateModal = ref(false) const showCreateModal = ref(false)
const mandateTypeOptions = [ const mandateTypeOptions = [
{ label: 'Comite technique', value: 'techcomm' }, { label: 'Comité technique', value: 'techcomm' },
{ label: 'Forgeron', value: 'smith' }, { label: 'Forgeron', value: 'smith' },
{ label: 'Personnalise', value: 'custom' }, { label: 'Personnalisé', value: 'custom' },
] ]
const newMandate = ref<MandateCreate>({ const newMandate = ref<MandateCreate>({
@@ -46,7 +46,7 @@ onMounted(async () => {
/** Status filter pills with counts. */ /** Status filter pills with counts. */
const statuses = computed(() => [ const statuses = computed(() => [
{ id: 'draft', label: 'En prepa', count: mandates.list.filter(m => m.status === 'draft' || m.status === 'candidacy').length }, { id: 'draft', label: 'En prépa', count: mandates.list.filter(m => m.status === 'draft' || m.status === 'candidacy').length },
{ id: 'voting', label: 'En vote', count: mandates.list.filter(m => m.status === 'voting').length }, { id: 'voting', label: 'En vote', count: mandates.list.filter(m => m.status === 'voting').length },
{ id: 'active', label: 'En vigueur', count: mandates.list.filter(m => m.status === 'active' || m.status === 'reporting').length }, { id: 'active', label: 'En vigueur', count: mandates.list.filter(m => m.status === 'active' || m.status === 'reporting').length },
{ id: 'closed', label: 'Clos', count: mandates.list.filter(m => m.status === 'completed' || m.status === 'revoked').length }, { id: 'closed', label: 'Clos', count: mandates.list.filter(m => m.status === 'completed' || m.status === 'revoked').length },
@@ -95,9 +95,9 @@ const filteredMandates = computed(() => {
const typeLabel = (mandateType: string) => { const typeLabel = (mandateType: string) => {
switch (mandateType) { switch (mandateType) {
case 'techcomm': return 'Comite technique' case 'techcomm': return 'Comité technique'
case 'smith': return 'Forgeron' case 'smith': return 'Forgeron'
case 'custom': return 'Personnalise' case 'custom': return 'Personnalisé'
default: return mandateType default: return mandateType
} }
} }
@@ -133,7 +133,7 @@ async function handleCreate() {
<template> <template>
<SectionLayout <SectionLayout
title="Mandats" title="Mandats"
subtitle="Un contexte, un objectif, une duree, une ou plusieurs nominations ; par defaut : nomination d'un binome." subtitle="Un contexte, un objectif, une durée, une ou plusieurs nominations ; par défaut : nomination d'un binôme."
:statuses="statuses" :statuses="statuses"
:active-status="activeStatus" :active-status="activeStatus"
@update:active-status="activeStatus = $event" @update:active-status="activeStatus = $event"
@@ -189,17 +189,17 @@ async function handleCreate() {
Qu'est-ce qu'un mandat ? Qu'est-ce qu'un mandat ?
</h3> </h3>
<p class="mandate-onboarding__text"> <p class="mandate-onboarding__text">
Un mandat definit un contexte, un objectif et une duree pour une mission de gouvernance. Un mandat définit un contexte, un objectif et une durée pour une mission de gouvernance.
Il peut porter sur le comite technique, les forgerons, ou tout role specifique de la communaute. Il peut porter sur le comité technique, les forgerons, ou tout rôle spécifique de la communauté.
</p> </p>
<p class="mandate-onboarding__text"> <p class="mandate-onboarding__text">
Par defaut, un mandat nomme un binome pour assurer la continuite. Par défaut, un mandat nomme un binôme pour assurer la continuité.
Le processus comprend : candidature, vote communautaire, periode active et rapport final. Le processus comprend : candidature, vote communautaire, periode active et rapport final.
</p> </p>
<div class="mandate-onboarding__actions"> <div class="mandate-onboarding__actions">
<UButton <UButton
v-if="auth.isAuthenticated" v-if="auth.isAuthenticated"
label="Creer un premier mandat" label="Créer un premier mandat"
icon="i-lucide-plus" icon="i-lucide-plus"
color="primary" color="primary"
size="sm" size="sm"
@@ -207,7 +207,7 @@ async function handleCreate() {
/> />
<UButton <UButton
to="/protocols" to="/protocols"
label="Decouvrir les protocoles" label="Découvrir les protocoles"
variant="outline" variant="outline"
size="sm" size="sm"
icon="i-lucide-wrench" icon="i-lucide-wrench"
@@ -222,7 +222,7 @@ async function handleCreate() {
style="color: var(--mood-text-muted);" style="color: var(--mood-text-muted);"
> >
<UIcon name="i-lucide-user-check" class="text-4xl mb-3 block mx-auto" /> <UIcon name="i-lucide-user-check" class="text-4xl mb-3 block mx-auto" />
<p>Aucun mandat trouve</p> <p>Aucun mandat trouvé</p>
<p v-if="searchQuery || activeStatus" class="text-sm mt-1"> <p v-if="searchQuery || activeStatus" class="text-sm mt-1">
Essayez de modifier vos filtres Essayez de modifier vos filtres
</p> </p>
@@ -254,7 +254,7 @@ async function handleCreate() {
</span> </span>
<span class="mandate-card__steps"> <span class="mandate-card__steps">
<UIcon name="i-lucide-layers" class="text-xs" /> <UIcon name="i-lucide-layers" class="text-xs" />
{{ mandate.steps.length }} etape{{ mandate.steps.length !== 1 ? 's' : '' }} {{ mandate.steps.length }} étape{{ mandate.steps.length !== 1 ? 's' : '' }}
</span> </span>
<span v-if="mandate.mandatee_id" class="mandate-card__mandatee"> <span v-if="mandate.mandatee_id" class="mandate-card__mandatee">
<UIcon name="i-lucide-user" class="text-xs" /> <UIcon name="i-lucide-user" class="text-xs" />
@@ -263,7 +263,7 @@ async function handleCreate() {
</div> </div>
<div class="mandate-card__dates"> <div class="mandate-card__dates">
<span>Debut : {{ formatDate(mandate.starts_at) }}</span> <span>Début : {{ formatDate(mandate.starts_at) }}</span>
<span>Fin : {{ formatDate(mandate.ends_at) }}</span> <span>Fin : {{ formatDate(mandate.ends_at) }}</span>
</div> </div>
</div> </div>
@@ -274,28 +274,28 @@ async function handleCreate() {
<template #toolbox> <template #toolbox>
<ToolboxVignette <ToolboxVignette
title="Ouverture" title="Ouverture"
:bullets="['Definir mission et perimetre', 'Duree et objectifs clairs']" :bullets="['Définir mission et périmètre', 'Durée et objectifs clairs']"
:actions="[ :actions="[
{ label: 'Creer', icon: 'i-lucide-door-open', emit: 'create', primary: true }, { label: 'Créer', icon: 'i-lucide-door-open', emit: 'create', primary: true },
]" ]"
/> />
<ToolboxVignette <ToolboxVignette
title="Nomination" title="Nomination"
:bullets="['Election en binome', 'Titulaire + suppleant']" :bullets="['Élection en binôme', 'Titulaire + suppléant']"
:actions="[ :actions="[
{ label: 'Voir', icon: 'i-lucide-users', emit: 'nomination' }, { label: 'Voir', icon: 'i-lucide-users', emit: 'nomination' },
]" ]"
/> />
<ToolboxVignette <ToolboxVignette
title="Transparence" title="Transparence"
:bullets="['Rapports d\'activite', 'Soumis au vote communautaire']" :bullets="['Rapports d\'activité', 'Soumis au vote communautaire']"
:actions="[ :actions="[
{ label: 'Voir', icon: 'i-lucide-eye', emit: 'transparence' }, { label: 'Voir', icon: 'i-lucide-eye', emit: 'transparence' },
]" ]"
/> />
<ToolboxVignette <ToolboxVignette
title="Cloture" title="Cloture"
:bullets="['Fin de mandat ou revocation', 'Bilan et transmission']" :bullets="['Fin de mandat ou révocation', 'Bilan et transmission']"
:actions="[ :actions="[
{ label: 'Voir', icon: 'i-lucide-lock', emit: 'cloture' }, { label: 'Voir', icon: 'i-lucide-lock', emit: 'cloture' },
]" ]"
@@ -352,7 +352,7 @@ async function handleCreate() {
/> />
<UButton <UButton
type="submit" type="submit"
label="Creer" label="Créer"
icon="i-lucide-plus" icon="i-lucide-plus"
color="primary" color="primary"
:loading="creating" :loading="creating"

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
/** /**
* Protocoles & Fonctionnement — Boite a outils de vote. * Protocoles & Fonctionnement — Boîte à outils de vote.
* *
* Liste les protocoles de vote avec SectionLayout, * Liste les protocoles de vote avec SectionLayout,
* sidebar n8n workflow + simulateur de formules. * sidebar n8n workflow + simulateur de formules.
@@ -30,14 +30,14 @@ onMounted(async () => {
const voteTypeLabel = (voteType: string) => { const voteTypeLabel = (voteType: string) => {
switch (voteType) { switch (voteType) {
case 'binary': return 'Binaire' case 'binary': return 'Binaire'
case 'nuanced': return 'Nuance' case 'nuanced': return 'Nuancé'
default: return voteType default: return voteType
} }
} }
const voteTypeOptions = [ const voteTypeOptions = [
{ label: 'Binaire (Pour/Contre)', value: 'binary' }, { label: 'Binaire (Pour/Contre)', value: 'binary' },
{ label: 'Nuance (6 niveaux)', value: 'nuanced' }, { label: 'Nuancé (6 niveaux)', value: 'nuanced' },
] ]
const formulaOptions = computed(() => { const formulaOptions = computed(() => {
@@ -57,7 +57,7 @@ const statuses = computed(() => [
}, },
{ {
id: 'nuanced', id: 'nuanced',
label: 'Nuance', label: 'Nuancé',
count: protocols.protocols.filter(p => p.vote_type === 'nuanced').length, count: protocols.protocols.filter(p => p.vote_type === 'nuanced').length,
cssClass: 'status-prepa', cssClass: 'status-prepa',
}, },
@@ -117,24 +117,60 @@ interface WorkflowStep {
type: string type: string
} }
const operationalProtocols = [ interface LinkedRef {
label: string
icon: string
to: string
kind: 'document' | 'decision'
}
interface OperationalProtocol {
slug: string
name: string
description: string
category: string
icon: string
instancesLabel: string
linkedRefs: LinkedRef[]
steps: WorkflowStep[]
}
const operationalProtocols: OperationalProtocol[] = [
{ {
slug: 'embarquement-forgeron', slug: 'embarquement-forgeron',
name: 'Embarquement Forgeron', name: 'Embarquement Forgeron',
description: 'Processus complet d\'intégration d\'un nouveau forgeron dans le réseau Duniter.', description: 'Processus complet d\'intégration d\'un nouveau forgeron dans le réseau Duniter. Parcours en 5 jalons de la candidature à la mise en ligne du nœud validateur.',
category: 'onboarding', category: 'onboarding',
icon: 'i-lucide-hammer', icon: 'i-lucide-hammer',
instancesLabel: '~10-50 / an', instancesLabel: '~10-50 / an',
linkedRefs: [
{ label: 'Acte d\'engagement forgeron', icon: 'i-lucide-book-open', to: '/documents/engagement-forgeron', kind: 'document' },
],
steps: [ steps: [
{ label: 'Invitation on-chain', actor: 'Smith existant', icon: 'i-lucide-send', type: 'on_chain' }, { label: 'Candidature', actor: 'Aspirant forgeron', icon: 'i-lucide-user-plus', type: 'checklist' },
{ label: 'Acceptation', actor: 'Candidat', icon: 'i-lucide-check', type: 'on_chain' }, { label: 'Nœud miroir', actor: 'Candidat', icon: 'i-lucide-server', type: 'on_chain' },
{ label: 'Session keys', actor: 'Candidat', icon: 'i-lucide-key', type: 'on_chain' }, { label: 'Évaluation technique', actor: 'Certificateur', icon: 'i-lucide-clipboard-check', type: 'checklist' },
{ label: 'Checklist aspirant', actor: 'Candidat', icon: 'i-lucide-clipboard-check', type: 'checklist' }, { label: 'Certification Smith (×3)', actor: 'Certificateurs', icon: 'i-lucide-stamp', type: 'certification' },
{ label: 'Certification 1', actor: 'Certificateur', icon: 'i-lucide-stamp', type: 'certification' },
{ label: 'Certification 2', actor: 'Certificateur', icon: 'i-lucide-stamp', type: 'certification' },
{ label: 'Certification 3', actor: 'Certificateur', icon: 'i-lucide-stamp', type: 'certification' },
{ label: 'Go online', actor: 'Candidat', icon: 'i-lucide-wifi', type: 'on_chain' }, { label: 'Go online', actor: 'Candidat', icon: 'i-lucide-wifi', type: 'on_chain' },
] as WorkflowStep[], ],
},
{
slug: 'soumission-runtime-upgrade',
name: 'Soumission Runtime Upgrade',
description: 'Protocole de soumission d\'une mise à jour du runtime Duniter V2 on-chain. Chaque upgrade suit un parcours strict en 5 étapes, de la qualification technique au suivi post-déploiement.',
category: 'on-chain',
icon: 'i-lucide-cpu',
instancesLabel: '~2-6 / an',
linkedRefs: [
{ label: 'Décision Runtime Upgrade', icon: 'i-lucide-scale', to: '/decisions', kind: 'decision' },
],
steps: [
{ label: 'Qualification', actor: 'Proposant', icon: 'i-lucide-file-check', type: 'checklist' },
{ label: 'Revue technique', actor: 'Comité technique', icon: 'i-lucide-search', type: 'checklist' },
{ label: 'Vote communautaire', actor: 'Communauté WoT', icon: 'i-lucide-vote', type: 'on_chain' },
{ label: 'Exécution on-chain', actor: 'Proposant', icon: 'i-lucide-zap', type: 'on_chain' },
{ label: 'Suivi post-upgrade', actor: 'Forgerons', icon: 'i-lucide-activity', type: 'checklist' },
],
}, },
] ]
@@ -142,7 +178,7 @@ const operationalProtocols = [
const n8nWorkflows = [ const n8nWorkflows = [
{ {
name: 'Vote -> Notification', name: 'Vote -> Notification',
description: 'Notifie les membres lorsqu\'un nouveau vote demarre ou se termine.', description: 'Notifie les membres lorsqu\'un nouveau vote démarre ou se termine.',
icon: 'i-lucide-bell', icon: 'i-lucide-bell',
status: 'actif', status: 'actif',
}, },
@@ -153,13 +189,13 @@ const n8nWorkflows = [
status: 'actif', status: 'actif',
}, },
{ {
name: 'Decision -> Etape suivante', name: 'Décision → Étape suivante',
description: 'Avance automatiquement une decision a l\'etape suivante apres validation.', description: 'Avance automatiquement une décision à l\'étape suivante après validation.',
icon: 'i-lucide-git-branch', icon: 'i-lucide-git-branch',
status: 'demo', status: 'demo',
}, },
{ {
name: 'Mandat expire -> Alerte', name: 'Mandat expiré → Alerte',
description: 'Envoie une alerte 7 jours avant l\'expiration d\'un mandat.', description: 'Envoie une alerte 7 jours avant l\'expiration d\'un mandat.',
icon: 'i-lucide-alarm-clock', icon: 'i-lucide-alarm-clock',
status: 'demo', status: 'demo',
@@ -170,7 +206,7 @@ const n8nWorkflows = [
<template> <template>
<SectionLayout <SectionLayout
title="Protocoles & Fonctionnement" title="Protocoles & Fonctionnement"
subtitle="Boite a outils de vote, formules de seuil, workflows automatises" subtitle="Boîte à outils de vote, formules de seuil, workflows automatisés"
:statuses="statuses" :statuses="statuses"
:active-status="activeStatus" :active-status="activeStatus"
@update:active-status="activeStatus = $event" @update:active-status="activeStatus = $event"
@@ -212,7 +248,7 @@ const n8nWorkflows = [
<template v-else> <template v-else>
<div v-if="filteredProtocols.length === 0" class="proto-empty"> <div v-if="filteredProtocols.length === 0" class="proto-empty">
<UIcon name="i-lucide-settings" class="text-2xl" /> <UIcon name="i-lucide-settings" class="text-2xl" />
<p>Aucun protocole trouve</p> <p>Aucun protocole trouvé</p>
</div> </div>
<div v-else class="proto-list"> <div v-else class="proto-list">
@@ -262,7 +298,7 @@ const n8nWorkflows = [
<div class="proto-ops"> <div class="proto-ops">
<h3 class="proto-ops__title"> <h3 class="proto-ops__title">
<UIcon name="i-lucide-git-branch" class="text-sm" /> <UIcon name="i-lucide-git-branch" class="text-sm" />
Protocoles operationnels Protocoles opérationnels
<span class="proto-ops__count">{{ operationalProtocols.length }}</span> <span class="proto-ops__count">{{ operationalProtocols.length }}</span>
</h3> </h3>
@@ -282,6 +318,21 @@ const n8nWorkflows = [
</div> </div>
</div> </div>
<!-- Linked references -->
<div v-if="op.linkedRefs.length > 0" class="proto-ops__refs">
<NuxtLink
v-for="ref in op.linkedRefs"
:key="ref.to"
:to="ref.to"
class="proto-ops__ref"
:class="`proto-ops__ref--${ref.kind}`"
>
<UIcon :name="ref.icon" class="text-xs" />
<span>{{ ref.label }}</span>
<UIcon name="i-lucide-arrow-right" class="text-xs proto-ops__ref-arrow" />
</NuxtLink>
</div>
<!-- Step timeline --> <!-- Step timeline -->
<div class="proto-ops__timeline"> <div class="proto-ops__timeline">
<div <div
@@ -314,8 +365,8 @@ const n8nWorkflows = [
<thead> <thead>
<tr> <tr>
<th>Nom</th> <th>Nom</th>
<th>Duree</th> <th>Durée</th>
<th>Majorite</th> <th>Majorité</th>
<th>B</th> <th>B</th>
<th>G</th> <th>G</th>
<th>Smith</th> <th>Smith</th>
@@ -343,7 +394,7 @@ const n8nWorkflows = [
<!-- Simulateur --> <!-- Simulateur -->
<ToolboxVignette <ToolboxVignette
title="Simulateur de formules" title="Simulateur de formules"
:bullets="['Testez WoT, Smith, TechComm', 'Ajustez les parametres en temps reel', 'Visualisez les seuils']" :bullets="['Testez WoT, Smith, TechComm', 'Ajustez les paramètres en temps réel', 'Visualisez les seuils']"
:actions="[ :actions="[
{ label: 'Tutos', icon: 'i-lucide-graduation-cap', emit: 'tutos' }, { label: 'Tutos', icon: 'i-lucide-graduation-cap', emit: 'tutos' },
{ label: 'Ouvrir', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true }, { label: 'Ouvrir', icon: 'i-lucide-calculator', to: '/protocols/formulas', primary: true },
@@ -357,7 +408,7 @@ const n8nWorkflows = [
<span>Workflows n8n</span> <span>Workflows n8n</span>
</div> </div>
<p class="n8n-section__desc"> <p class="n8n-section__desc">
Automatisations reliees via MCP Automatisations reliées via MCP
</p> </p>
<div class="n8n-workflows"> <div class="n8n-workflows">
@@ -387,12 +438,12 @@ const n8nWorkflows = [
<!-- Meta-gouvernance --> <!-- Meta-gouvernance -->
<ToolboxVignette <ToolboxVignette
title="Meta-gouvernance" title="Méta-gouvernance"
:bullets="['Les formules sont soumises au vote', 'Modifier les seuils collectivement', 'Transparence totale']" :bullets="['Les formules sont soumises au vote', 'Modifier les seuils collectivement', 'Transparence totale']"
:actions="[ :actions="[
{ label: 'Tutos', icon: 'i-lucide-graduation-cap', emit: 'tutos' }, { label: 'Tutos', icon: 'i-lucide-graduation-cap', emit: 'tutos' },
{ label: 'Formules', icon: 'i-lucide-calculator', emit: 'formules' }, { label: 'Formules', icon: 'i-lucide-calculator', emit: 'formules' },
{ label: 'Demarrer', icon: 'i-lucide-play', emit: 'meta', primary: true }, { label: 'Démarrer', icon: 'i-lucide-play', emit: 'meta', primary: true },
]" ]"
/> />
</template> </template>
@@ -439,7 +490,7 @@ const n8nWorkflows = [
<USelect <USelect
v-model="newProtocol.formula_config_id" v-model="newProtocol.formula_config_id"
:items="formulaOptions" :items="formulaOptions"
placeholder="Selectionnez une formule..." placeholder="Sélectionnez une formule..."
value-key="value" value-key="value"
/> />
</div> </div>
@@ -455,7 +506,7 @@ const n8nWorkflows = [
@click="createProtocol" @click="createProtocol"
> >
<UIcon v-if="creating" name="i-lucide-loader-2" class="animate-spin text-xs" /> <UIcon v-if="creating" name="i-lucide-loader-2" class="animate-spin text-xs" />
<span>Creer</span> <span>Créer</span>
</button> </button>
</div> </div>
</div> </div>
@@ -952,6 +1003,49 @@ const n8nWorkflows = [
opacity: 0.7; opacity: 0.7;
} }
/* Linked references */
.proto-ops__refs {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.proto-ops__ref {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: 20px;
text-decoration: none;
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.proto-ops__ref:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px var(--mood-shadow);
}
.proto-ops__ref--document {
background: color-mix(in srgb, var(--mood-accent) 12%, transparent);
color: var(--mood-accent);
}
.proto-ops__ref--decision {
background: color-mix(in srgb, var(--mood-secondary, var(--mood-accent)) 12%, transparent);
color: var(--mood-secondary, var(--mood-accent));
}
.proto-ops__ref-arrow {
opacity: 0.4;
transition: opacity 0.12s;
}
.proto-ops__ref:hover .proto-ops__ref-arrow {
opacity: 1;
}
/* Timeline */ /* Timeline */
.proto-ops__timeline { .proto-ops__timeline {
display: flex; display: flex;

View File

@@ -28,23 +28,23 @@ const sections: ToolSection[] = [
color: 'var(--mood-accent)', color: 'var(--mood-accent)',
tools: [ tools: [
{ label: 'Modules', icon: 'i-lucide-puzzle', description: 'Structurer un document en sections et clauses modulaires', to: '/documents', status: 'ready' }, { label: 'Modules', icon: 'i-lucide-puzzle', description: 'Structurer un document en sections et clauses modulaires', to: '/documents', status: 'ready' },
{ label: 'Votes permanents', icon: 'i-lucide-infinity', description: 'Chaque clause est sous vote permanent, modifiable a tout moment', status: 'ready' }, { label: 'Votes permanents', icon: 'i-lucide-infinity', description: 'Chaque clause est sous vote permanent, modifiable à tout moment', status: 'ready' },
{ label: 'Inertie de remplacement', icon: 'i-lucide-sliders-horizontal', description: 'Regler la difficulte de modification par section (standard, haute, tres haute)', to: '/protocols/formulas', status: 'ready' }, { label: 'Inertie de remplacement', icon: 'i-lucide-sliders-horizontal', description: 'Régler la difficulté de modification par section (standard, haute, très haute)', to: '/protocols/formulas', status: 'ready' },
{ label: 'Contre-propositions', icon: 'i-lucide-pen-line', description: 'Soumettre un texte alternatif soumis au vote de la communaute', status: 'ready' }, { label: 'Contre-propositions', icon: 'i-lucide-pen-line', description: 'Soumettre un texte alternatif soumis au vote de la communauté', status: 'ready' },
{ label: 'Ancrage IPFS', icon: 'i-lucide-hard-drive', description: 'Archiver les documents valides sur IPFS avec preuve on-chain', status: 'soon' }, { label: 'Ancrage IPFS', icon: 'i-lucide-hard-drive', description: 'Archiver les documents validés sur IPFS avec preuve on-chain', status: 'soon' },
], ],
}, },
{ {
key: 'decisions', key: 'decisions',
title: 'Decisions', title: 'Décisions',
icon: 'i-lucide-scale', icon: 'i-lucide-scale',
color: 'var(--mood-secondary, var(--mood-accent))', color: 'var(--mood-secondary, var(--mood-accent))',
tools: [ tools: [
{ label: 'Vote majoritaire WoT', icon: 'i-lucide-check-circle', description: 'Seuil adaptatif par la toile de confiance, formule g1vote', to: '/protocols/formulas', status: 'ready' }, { label: 'Vote majoritaire WoT', icon: 'i-lucide-check-circle', description: 'Seuil adaptatif par la toile de confiance, formule g1vote', to: '/protocols/formulas', status: 'ready' },
{ label: 'Vote quadratique', icon: 'i-lucide-square-stack', description: 'Ponderation degresssive pour eviter la concentration de pouvoir', status: 'soon' }, { label: 'Vote quadratique', icon: 'i-lucide-square-stack', description: 'Pondération dégressive pour éviter la concentration de pouvoir', status: 'soon' },
{ label: 'Vote nuance 6 niveaux', icon: 'i-lucide-bar-chart-3', description: 'De Tout a fait contre a Tout a fait pour, avec seuil de satisfaction', status: 'ready' }, { label: 'Vote nuancé 6 niveaux', icon: 'i-lucide-bar-chart-3', description: 'De Tout à fait contre à Tout à fait pour, avec seuil de satisfaction', status: 'ready' },
{ label: 'Mandature', icon: 'i-lucide-user-check', description: 'Election et nomination en binome avec transparence', status: 'ready' }, { label: 'Mandature', icon: 'i-lucide-user-check', description: 'Élection et nomination en binôme avec transparence', status: 'ready' },
{ label: 'Multi-criteres', icon: 'i-lucide-layers', description: 'Combinaison WoT + Smith + TechComm, tous doivent passer', to: '/protocols/formulas', status: 'ready' }, { label: 'Multi-critères', icon: 'i-lucide-layers', description: 'Combinaison WoT + Smith + TechComm, tous doivent passer', to: '/protocols/formulas', status: 'ready' },
], ],
}, },
{ {
@@ -53,10 +53,10 @@ const sections: ToolSection[] = [
icon: 'i-lucide-user-check', icon: 'i-lucide-user-check',
color: 'var(--mood-success)', color: 'var(--mood-success)',
tools: [ tools: [
{ label: 'Ouverture', icon: 'i-lucide-door-open', description: 'Definir une mission, son perimetre, sa duree et ses objectifs', status: 'ready' }, { label: 'Ouverture', icon: 'i-lucide-door-open', description: 'Définir une mission, son périmètre, sa durée et ses objectifs', status: 'ready' },
{ label: 'Nomination', icon: 'i-lucide-users', description: 'Election en binome : un titulaire + un suppleant', status: 'ready' }, { label: 'Nomination', icon: 'i-lucide-users', description: 'Élection en binôme : un titulaire + un suppléant', status: 'ready' },
{ label: 'Transparence', icon: 'i-lucide-eye', description: 'Rapports d\'activite periodiques soumis au vote', status: 'ready' }, { label: 'Transparence', icon: 'i-lucide-eye', description: 'Rapports d\'activité périodiques soumis au vote', status: 'ready' },
{ label: 'Cloture', icon: 'i-lucide-lock', description: 'Fin de mandat avec bilan ou revocation anticipee par vote', status: 'ready' }, { label: 'Clôture', icon: 'i-lucide-lock', description: 'Fin de mandat avec bilan ou révocation anticipée par vote', status: 'ready' },
], ],
}, },
{ {
@@ -65,10 +65,10 @@ const sections: ToolSection[] = [
icon: 'i-lucide-settings', icon: 'i-lucide-settings',
color: 'var(--mood-tertiary, var(--mood-accent))', color: 'var(--mood-tertiary, var(--mood-accent))',
tools: [ tools: [
{ label: 'Simulateur de formules', icon: 'i-lucide-calculator', description: 'Tester les parametres de seuil WoT en temps reel', to: '/protocols/formulas', status: 'ready' }, { label: 'Simulateur de formules', icon: 'i-lucide-calculator', description: 'Tester les paramètres de seuil WoT en temps réel', to: '/protocols/formulas', status: 'ready' },
{ label: 'Meta-gouvernance', icon: 'i-lucide-shield', description: 'Les formules elles-memes sont soumises au vote', status: 'ready' }, { label: 'Méta-gouvernance', icon: 'i-lucide-shield', description: 'Les formules elles-mêmes sont soumises au vote', status: 'ready' },
{ label: 'Workflows n8n', icon: 'i-lucide-workflow', description: 'Automatisations optionnelles (notifications, alertes, relances)', status: 'soon' }, { label: 'Workflows n8n', icon: 'i-lucide-workflow', description: 'Automatisations optionnelles (notifications, alertes, relances)', status: 'soon' },
{ label: 'Protocoles operationnels', icon: 'i-lucide-git-branch', description: 'Processus multi-etapes reutilisables (embarquement, upgrade)', to: '/protocols', status: 'ready' }, { label: 'Protocoles opérationnels', icon: 'i-lucide-git-branch', description: 'Processus multi-étapes réutilisables (embarquement, upgrade)', to: '/protocols', status: 'ready' },
], ],
}, },
] ]
@@ -83,7 +83,7 @@ const sections: ToolSection[] = [
variant="ghost" variant="ghost"
color="neutral" color="neutral"
icon="i-lucide-arrow-left" icon="i-lucide-arrow-left"
label="Retour a l'accueil" label="Retour à l'accueil"
size="sm" size="sm"
/> />
</div> </div>
@@ -92,10 +92,10 @@ const sections: ToolSection[] = [
<div class="tools-page__header"> <div class="tools-page__header">
<h1 class="tools-page__title"> <h1 class="tools-page__title">
<UIcon name="i-lucide-wrench" class="tools-page__title-icon" /> <UIcon name="i-lucide-wrench" class="tools-page__title-icon" />
Boite a outils Boîte à outils
</h1> </h1>
<p class="tools-page__subtitle"> <p class="tools-page__subtitle">
Tous les outils de decision collective, organises par section Tous les outils de décision collective, organisés par section
</p> </p>
</div> </div>
@@ -143,7 +143,7 @@ const sections: ToolSection[] = [
<div class="tool-card__body"> <div class="tool-card__body">
<div class="tool-card__head"> <div class="tool-card__head">
<span class="tool-card__label">{{ tool.label }}</span> <span class="tool-card__label">{{ tool.label }}</span>
<span v-if="tool.status === 'soon'" class="tool-card__badge">bientot</span> <span v-if="tool.status === 'soon'" class="tool-card__badge">bientôt</span>
</div> </div>
<p class="tool-card__desc">{{ tool.description }}</p> <p class="tool-card__desc">{{ tool.description }}</p>
</div> </div>