diff --git a/backend/seed.py b/backend/seed.py index cfe7712..856bf02 100644 --- a/backend/seed.py +++ b/backend/seed.py @@ -1,7 +1,9 @@ -"""Seed the database with initial FormulaConfigs, VotingProtocols, Documents, and Decisions. +"""Seed the database with real data from Duniter community documents. -Usage: - python seed.py +Sources: + - Engagement Forgeron v2.0.0: https://forum.monnaie-libre.fr/t/33165 + - Engagement Certification (Licence G1): monnaie-libre.fr/licence-g1/ + - Runtime Upgrade process template Idempotent: checks if data already exists before inserting. """ @@ -9,7 +11,9 @@ Idempotent: checks if data already exists before inserting. from __future__ import annotations import asyncio +import hashlib import uuid +from datetime import datetime, timedelta, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -18,6 +22,8 @@ from app.database import async_session, engine, Base from app.models.protocol import FormulaConfig, VotingProtocol from app.models.document import Document, DocumentItem from app.models.decision import Decision, DecisionStep +from app.models.user import DuniterIdentity +from app.models.vote import VoteSession, Vote # --------------------------------------------------------------------------- @@ -43,12 +49,16 @@ async def get_or_create( return instance, True +def fake_signature(payload: str) -> str: + """Generate a deterministic fake Ed25519-like signature for seed data.""" + return hashlib.sha256(payload.encode()).hexdigest() + + # --------------------------------------------------------------------------- # Seed: FormulaConfigs # --------------------------------------------------------------------------- async def seed_formula_configs(session: AsyncSession) -> dict[str, FormulaConfig]: - """Create the 4 base formula configurations.""" configs: dict[str, dict] = { "Standard Licence G1": { "description": "Formule standard pour la Licence G1 : vote binaire WoT.", @@ -101,14 +111,13 @@ async def seed_formula_configs(session: AsyncSession) -> dict[str, FormulaConfig # --------------------------------------------------------------------------- -# Seed: VotingProtocols (premier pack de modalites) +# Seed: VotingProtocols # --------------------------------------------------------------------------- async def seed_voting_protocols( session: AsyncSession, formulas: dict[str, FormulaConfig], ) -> dict[str, VotingProtocol]: - """Create the first pack of voting modalities (3 protocols).""" protocols: dict[str, dict] = { "Vote majoritaire": { "description": ( @@ -155,102 +164,112 @@ async def seed_voting_protocols( # --------------------------------------------------------------------------- -# Seed: Document - Acte d'engagement certification +# Seed: Engagement Certification (Licence G1 - obligations certificateur) # --------------------------------------------------------------------------- ENGAGEMENT_CERTIFICATION_ITEMS: list[dict] = [ { - "position": "1", - "item_type": "preamble", - "title": "Objet", + "position": "C1", + "item_type": "clause", + "title": "Transmission de la licence", "sort_order": 1, "current_text": ( - "Le present acte definit les engagements de tout membre de la " - "toile de confiance qui certifie l'identite d'une autre personne " - "dans le reseau Duniter." + "Toute operation de certification d'un nouveau membre de la monnaie libre G1 " + "doit prealablement s'accompagner de la transmission de cette licence " + "de la monnaie libre G1 au certifie." ), }, { - "position": "2", + "position": "C2", "item_type": "clause", - "title": "Connaissance personnelle", + "title": "Connaissance suffisante du certifie", "sort_order": 2, "current_text": ( - "Je certifie connaitre personnellement la personne que je " - "certifie, l'avoir rencontree physiquement a plusieurs reprises, " - "et pouvoir la contacter par au moins deux moyens de communication " - "differents." + "Certifier n'est pas uniquement s'assurer que vous avez rencontre la personne, " + "c'est assurer a la communaute G1 que vous connaissez suffisamment bien la " + "personne que vous vous appretez a certifier." ), }, { - "position": "3", + "position": "C3", "item_type": "clause", - "title": "Verification d'identite", + "title": "Contact par plusieurs moyens", "sort_order": 3, "current_text": ( - "Je certifie avoir verifie que la personne n'a qu'un seul compte " - "membre dans la toile de confiance, et que l'identite declaree " - "correspond a une personne humaine vivante." + "Connaitre la personne par plusieurs moyens de contact differents " + "(physique, electronique, etc.) permettant de verifier son identite " + "et de maintenir un lien de confiance dans la duree." ), }, { - "position": "4", + "position": "C4", "item_type": "clause", - "title": "Engagement de suivi", + "title": "Ne jamais certifier seul", "sort_order": 4, "current_text": ( - "Je m'engage a surveiller l'activite de mes certifies et a " - "signaler tout comportement suspect (comptes multiples, " - "usurpation d'identite, comptes abandonnes)." + "Ne certifiez jamais seul, mais accompagne d'au moins un autre membre " + "de la TdC G1, pour garantir un double controle et eviter les " + "certifications abusives." ), }, { - "position": "5", + "position": "C5", "item_type": "verification", - "title": "Delai entre certifications", + "title": "Verification des certifications existantes", "sort_order": 5, "current_text": ( - "Je respecte un delai minimum de reflexion de 5 jours entre " - "chaque nouvelle certification emise." + "Avant toute certification, assurez-vous de verifier si le compte du " + "certifie a deja recu une ou plusieurs certifications, et de qui " + "elles proviennent." ), }, { - "position": "6", - "item_type": "rule", - "title": "Renouvellement", + "position": "C6", + "item_type": "verification", + "title": "Verification de maitrise du compte", "sort_order": 6, "current_text": ( - "Je renouvelle mes certifications avant leur expiration pour " - "maintenir la cohesion de la toile de confiance." + "Verifier la maitrise du compte par un transfert test : envoyer " + "quelques G1 et demander un renvoi, afin de s'assurer que la personne " + "controle bien sa cle privee." ), }, { - "position": "7", - "item_type": "clause", - "title": "Responsabilite", + "position": "C7", + "item_type": "verification", + "title": "Verification de la licence", "sort_order": 7, "current_text": ( - "Je suis conscient que la certification engage ma responsabilite " - "vis-a-vis de la communaute. Une certification abusive peut " - "entrainer la perte de confiance des autres membres." + "Verifiez que vos contacts ont bien etudie et compris la licence G1 " + "a jour avant de proceder a la certification." ), }, { - "position": "8", - "item_type": "rule", - "title": "Revocation", + "position": "C8", + "item_type": "verification", + "title": "Document de revocation", "sort_order": 8, "current_text": ( - "Une certification peut etre revoquee si les conditions de " - "l'engagement ne sont plus remplies. La revocation est soumise " - "au protocole de vote en vigueur." + "D'avoir bien verifie avec la personne concernee qu'elle a bien genere " + "son document Duniter de revocation de compte, et qu'elle le conserve " + "en lieu sur." + ), + }, + { + "position": "C9", + "item_type": "clause", + "title": "Rencontre physique ou multi-canal", + "sort_order": 9, + "current_text": ( + "De rencontrer la personne physiquement, OU de verifier a distance " + "le lien personne / cle publique par plusieurs moyens de communication " + "differents et independants." ), }, ] async def seed_document_engagement_certification(session: AsyncSession) -> Document: - """Create the Acte d'engagement certification document with its items.""" doc, created = await get_or_create( session, Document, @@ -261,9 +280,8 @@ async def seed_document_engagement_certification(session: AsyncSession) -> Docum version="1.0.0", status="active", description=( - "Acte d'engagement pour les certificateurs de la toile de confiance " - "Duniter. Definit les obligations et responsabilites liees a la " - "certification de nouveaux membres." + "Obligations des certificateurs de la toile de confiance Duniter. " + "Chaque clause est soumise au vote permanent de la communaute." ), ) print(f" Document 'Acte d'engagement certification': {'created' if created else 'exists'}") @@ -279,113 +297,360 @@ async def seed_document_engagement_certification(session: AsyncSession) -> Docum # --------------------------------------------------------------------------- -# Seed: Document - Acte d'engagement forgeron v2.0.0 +# Seed: Engagement Forgeron v2.0.0 (real content from forum topic 33165) # --------------------------------------------------------------------------- ENGAGEMENT_FORGERON_ITEMS: list[dict] = [ + # --- Aspirant Forgeron : Securite et conformite --- { - "position": "1", - "item_type": "preamble", - "title": "Intention", + "position": "A1", + "item_type": "clause", + "title": "Intention et motivation", "sort_order": 1, "current_text": ( - "Avec la V2, une sous-toile de confiance pour les forgerons est " - "introduite. Les forgerons (validateurs de blocs) doivent demontrer " - "leurs competences techniques et leur engagement envers le reseau." + "J'ai clarifie ce qui me motive a devenir forgeron, " + "j'en assume les raisons." ), }, { - "position": "2", + "position": "A2", "item_type": "clause", - "title": "Savoirs-faire", + "title": "Veille securite", "sort_order": 2, "current_text": ( - "Administration systeme Linux, securite informatique, " - "cryptographie, blockchain Substrate. Le forgeron doit maitriser " - "l'ensemble de la chaine technique necessaire a la validation." + "Je fais de la veille pour maintenir mes pratiques de securite " + "systeme et reseau a jour." ), }, { - "position": "3", + "position": "A3", "item_type": "clause", - "title": "Rigueur", + "title": "Notifications forum", "sort_order": 3, "current_text": ( - "Comprendre en profondeur les configurations du runtime, " - "les parametres de consensus et les mecanismes de mise a jour " - "du reseau Duniter V2." + "J'ai active les notifications sur forum.duniter.org pour etre " + "alerte des discussions importantes concernant le reseau." ), }, { - "position": "4", - "item_type": "clause", - "title": "Reactivite", + "position": "A4", + "item_type": "verification", + "title": "Phrase de recuperation aleatoire", "sort_order": 4, "current_text": ( - "Reponse sous 24h aux alertes reseau. Disponibilite pour les " - "mises a jour critiques. Monitoring continu du noeud validateur." + "Je confirme que ma phrase de recuperation a ete generee " + "aleatoirement et n'est pas une phrase choisie par moi." ), }, { - "position": "5", + "position": "A5", "item_type": "verification", - "title": "Securite aspirant", + "title": "Compte separe", "sort_order": 5, "current_text": ( - "Phrases aleatoires de 12+ mots, comptes separes pour identite " - "et validation, sauvegardes chiffrees des cles, infrastructure " - "securisee et a jour." + "J'utilise un autre compte pour mes transactions courantes ; " + "le compte forgeron est strictement reserve a la validation." ), }, { - "position": "6", + "position": "A6", "item_type": "verification", - "title": "Contact aspirant", + "title": "Sauvegarde phrase de recuperation", "sort_order": 6, "current_text": ( - "Le candidat forgeron doit contacter au minimum 3 forgerons " - "existants par au moins 2 canaux de communication differents " - "avant de demander ses certifications." + "J'ai stocke ma phrase de recuperation sur plusieurs supports " + "physiques distincts et securises." ), }, { - "position": "7", - "item_type": "clause", - "title": "Clauses pieges", + "position": "A7", + "item_type": "verification", + "title": "Noeud a jour et synchronise", "sort_order": 7, "current_text": ( - "Exclusions : harcelement, abus de pouvoir, tentative " - "d'infiltration malveillante du reseau. Tout manquement " - "entraine le retrait des certifications forgeron." + "Je gere deja un noeud a jour, correctement synchronise et " + "joignable par les autres noeuds du reseau." ), }, { - "position": "8", + "position": "A8", "item_type": "verification", - "title": "Securite certificateur", + "title": "API unsafe non exposee", "sort_order": 8, "current_text": ( - "Verification de l'intention du candidat, de ses pratiques " - "de securite, et du bon fonctionnement de son noeud validateur " - "avant de delivrer une certification forgeron." + "J'ai veille a ne pas exposer publiquement l'api unsafe " + "de mon noeud validateur." ), }, { - "position": "9", - "item_type": "rule", - "title": "Regles TdC forgerons", + "position": "A9", + "item_type": "clause", + "title": "Transparence technique", "sort_order": 9, "current_text": ( - "Etre membre de la TdC principale. Recevoir une invitation " - "d'un forgeron existant. Obtenir au minimum 3 certifications " - "de forgerons actifs. Renouvellement annuel obligatoire." + "Je fournis a la demande d'un autre forgeron, mes choix " + "techniques (materiel, OS, configuration reseau)." + ), + }, + { + "position": "A10", + "item_type": "clause", + "title": "Declaration offline en cas de doute", + "sort_order": 10, + "current_text": ( + "Je me declare offline en cas de doute sur la securite " + "de mon noeud ou de mon infrastructure." + ), + }, + { + "position": "A11", + "item_type": "clause", + "title": "Reactivite 24h", + "sort_order": 11, + "current_text": ( + "Je m'engage a repondre en moins de 24h aux forgerons " + "quand je suis declare online." + ), + }, + # --- Aspirant Forgeron : Contact --- + { + "position": "A12", + "item_type": "clause", + "title": "Contact multi-canal", + "sort_order": 12, + "current_text": ( + "Je sais joindre efficacement et rapidement 3 des forgerons " + "par au moins 2 canaux de communication differents." + ), + }, + # --- Aspirant Forgeron : Connaissances --- + { + "position": "A13", + "item_type": "clause", + "title": "Acceptation des engagements", + "sort_order": 13, + "current_text": ( + "J'ai lu et j'accepte de respecter l'ensemble des " + "engagements forgerons en vigueur." + ), + }, + { + "position": "A14", + "item_type": "clause", + "title": "Regles de la TdC forgeron", + "sort_order": 14, + "current_text": ( + "J'ai pris connaissance des regles et delais associes au " + "fonctionnement de la TdC forgeron." + ), + }, + { + "position": "A15", + "item_type": "clause", + "title": "Fonctionnement blockchain", + "sort_order": 15, + "current_text": ( + "J'ai bien compris le fonctionnement d'un reseau blockchain " + "Duniter et le role du validateur." + ), + }, + # --- Aspirant Forgeron : Pieges (expected: NON) --- + { + "position": "A16", + "item_type": "rule", + "title": "Piege : harcelement", + "sort_order": 16, + "current_text": ( + "[Piege - reponse attendue : NON] " + "J'insiste, harcele ou fais pression pour etre certifie forgeron." + ), + }, + { + "position": "A17", + "item_type": "rule", + "title": "Piege : gloire et pouvoir", + "sort_order": 17, + "current_text": ( + "[Piege - reponse attendue : NON] " + "Je veux etre forgeron pour la gloire et le pouvoir." + ), + }, + { + "position": "A18", + "item_type": "rule", + "title": "Piege : nuisance", + "sort_order": 18, + "current_text": ( + "[Piege - reponse attendue : NON] " + "Je cherche a nuire a l'ecosysteme G1." + ), + }, + # --- Certificateur Forgeron : Securite et conformite --- + { + "position": "C1", + "item_type": "clause", + "title": "Intention du certifie questionnee", + "sort_order": 19, + "current_text": ( + "J'ai questionne l'intention du certifie a rejoindre " + "les forgerons et verifie sa motivation." + ), + }, + { + "position": "C2", + "item_type": "verification", + "title": "Pratiques de securite du certifie", + "sort_order": 20, + "current_text": ( + "J'ai demande au certifie quelles etaient ses pratiques de " + "securite systeme et reseau." + ), + }, + { + "position": "C3", + "item_type": "verification", + "title": "Phrase aleatoire du certifie", + "sort_order": 21, + "current_text": ( + "Le certifie m'assure que son compte forgeron est issu d'une " + "phrase generee aleatoirement." + ), + }, + { + "position": "C4", + "item_type": "verification", + "title": "Sauvegarde du certifie", + "sort_order": 22, + "current_text": ( + "Le certifie m'assure avoir stocke sa phrase de recuperation " + "sur plusieurs supports physiques." + ), + }, + { + "position": "C5", + "item_type": "verification", + "title": "Noeud du certifie verifie", + "sort_order": 23, + "current_text": ( + "J'ai verifie que le certifie gere deja un noeud a jour, " + "correctement synchronise et joignable." + ), + }, + { + "position": "C6", + "item_type": "clause", + "title": "Configuration du certifie notee", + "sort_order": 24, + "current_text": ( + "J'ai note le style de configuration du noeud du certifie " + "(materiel, OS, hebergement)." + ), + }, + { + "position": "C7", + "item_type": "clause", + "title": "Engagement d'information du certifie", + "sort_order": 25, + "current_text": ( + "Le certifie s'est engage a m'informer de tout changement " + "significatif de sa configuration." + ), + }, + { + "position": "C8", + "item_type": "verification", + "title": "Risques offline connus du certifie", + "sort_order": 26, + "current_text": ( + "J'ai verifie avec le certifie qu'il connait les risques " + "d'etre declare offline et les consequences." + ), + }, + # --- Certificateur Forgeron : Contact --- + { + "position": "C9", + "item_type": "clause", + "title": "Joindre les certifies", + "sort_order": 27, + "current_text": ( + "Je sais joindre efficacement les forgerons que j'ai certifies." + ), + }, + { + "position": "C10", + "item_type": "clause", + "title": "Deux canaux de contact", + "sort_order": 28, + "current_text": ( + "Je peux les joindre par au moins 2 canaux differents." + ), + }, + { + "position": "C11", + "item_type": "clause", + "title": "Contact sous 24h en cas de defaut", + "sort_order": 29, + "current_text": ( + "Je m'engage a contacter sous 24h ce forgeron si un defaut " + "concerne son noeud." + ), + }, + # --- Certificateur Forgeron : Connaissances --- + { + "position": "C12", + "item_type": "verification", + "title": "Engagements acceptes par le certifie", + "sort_order": 30, + "current_text": ( + "J'ai verifie que le certifie a accepte les engagements " + "forgerons integralement." + ), + }, + { + "position": "C13", + "item_type": "verification", + "title": "Regles consultables par le certifie", + "sort_order": 31, + "current_text": ( + "J'ai verifie que le certifie sait ou consulter les regles " + "detaillees de la TdC forgeron." + ), + }, + { + "position": "C14", + "item_type": "verification", + "title": "Delais connus du certifie", + "sort_order": 32, + "current_text": ( + "J'ai verifie que le certifie connait les delais de passage " + "en ligne et hors ligne." + ), + }, + # --- Certificateur Forgeron : Pieges (expected: NON) --- + { + "position": "C15", + "item_type": "rule", + "title": "Piege : certification sous pression", + "sort_order": 33, + "current_text": ( + "[Piege - reponse attendue : NON] " + "Je certifie sous la menace ou autre forme de pression." + ), + }, + { + "position": "C16", + "item_type": "rule", + "title": "Piege : avantage personnel", + "sort_order": 34, + "current_text": ( + "[Piege - reponse attendue : NON] " + "Je tire un avantage personnel en echange de ma certification." ), }, ] async def seed_document_engagement_forgeron(session: AsyncSession) -> Document: - """Create the Acte d'engagement forgeron v2.0.0 document with its items.""" doc, created = await get_or_create( session, Document, @@ -396,8 +661,9 @@ async def seed_document_engagement_forgeron(session: AsyncSession) -> Document: version="2.0.0", status="active", description=( - "Acte d'engagement des forgerons (validateurs de blocs) pour " - "Duniter V2. Adopte en fevrier 2026 (97 pour / 23 contre)." + "Acte d'engagement des forgerons (validateurs) Duniter V2. " + "Adopte en fevrier 2026 (97 pour / 23 contre). " + "34 clauses : aspirant (18) + certificateur (16)." ), ) print(f" Document 'Acte d'engagement forgeron': {'created' if created else 'exists'}") @@ -413,7 +679,7 @@ async def seed_document_engagement_forgeron(session: AsyncSession) -> Document: # --------------------------------------------------------------------------- -# Seed: Decision template - Runtime Upgrade +# Seed: Decision - Runtime Upgrade # --------------------------------------------------------------------------- RUNTIME_UPGRADE_STEPS: list[dict] = [ @@ -466,7 +732,6 @@ RUNTIME_UPGRADE_STEPS: list[dict] = [ async def seed_decision_runtime_upgrade(session: AsyncSession) -> Decision: - """Create the Runtime Upgrade decision template.""" decision, created = await get_or_create( session, Decision, @@ -492,33 +757,165 @@ async def seed_decision_runtime_upgrade(session: AsyncSession) -> Decision: return decision +# --------------------------------------------------------------------------- +# Seed: Simulated voters + votes on first 3 engagement items +# --------------------------------------------------------------------------- + +VOTER_NAMES = [ + "Moul", "Poka", "Hugo", "Elois", "Cgeek", + "Galuel", "Tortue", "Inso", "Tuxmain", "Matograine", + "Maaltir", +] + + +async def seed_voters(session: AsyncSession) -> list[DuniterIdentity]: + """Create 11 simulated Duniter identities for voting demo.""" + voters: list[DuniterIdentity] = [] + for i, name in enumerate(VOTER_NAMES): + # Deterministic address from name + addr_hash = hashlib.sha256(name.encode()).hexdigest()[:32] + address = f"5{addr_hash[:47]}" + + voter, created = await get_or_create( + session, + DuniterIdentity, + "address", + address, + display_name=name, + wot_status="member", + is_smith=(i < 5), # First 5 are smiths + ) + if created: + print(f" Voter '{name}': created") + voters.append(voter) + + return voters + + +async def seed_votes_on_items( + session: AsyncSession, + doc: Document, + protocol: VotingProtocol, + voters: list[DuniterIdentity], +): + """Create vote sessions on first 3 items with 10 for + 1 against.""" + # Fetch items for this document + stmt = select(DocumentItem).where( + DocumentItem.document_id == doc.id + ).order_by(DocumentItem.sort_order).limit(3) + result = await session.execute(stmt) + items = result.scalars().all() + + if not items: + print(" No items found to vote on") + return + + now = datetime.now(timezone.utc) + + for item in items: + # Check if a session already exists for this item + existing_stmt = select(VoteSession).where( + VoteSession.item_version_id == None, + VoteSession.voting_protocol_id == protocol.id, + ) + # We'll use a simpler idempotency check + session_id = uuid.uuid5(uuid.NAMESPACE_URL, f"seed-vote-{item.id}") + check_stmt = select(VoteSession).where(VoteSession.id == session_id) + check_result = await session.execute(check_stmt) + if check_result.scalar_one_or_none() is not None: + print(f" VoteSession for '{item.title}': exists") + continue + + vote_session = VoteSession( + id=session_id, + decision_id=None, + item_version_id=None, + voting_protocol_id=protocol.id, + wot_size=7224, + smith_size=23, + techcomm_size=5, + starts_at=now - timedelta(days=15), + ends_at=now + timedelta(days=15), + status="open", + votes_for=10, + votes_against=1, + votes_total=11, + threshold_required=97.0, + ) + session.add(vote_session) + await session.flush() + + # 10 votes "for" + for voter in voters[:10]: + payload = f"vote:{vote_session.id}:{voter.id}:for" + vote = Vote( + session_id=vote_session.id, + voter_id=voter.id, + vote_value="for", + comment="Oui c'est mieux que l'existant", + signature=fake_signature(payload), + signed_payload=payload, + voter_wot_status="member", + voter_is_smith=voter.is_smith, + ) + session.add(vote) + + # 1 vote "against" + against_voter = voters[10] + payload = f"vote:{vote_session.id}:{against_voter.id}:against" + vote = Vote( + session_id=vote_session.id, + voter_id=against_voter.id, + vote_value="against", + comment="Non, on ne remplace pas tel quel", + signature=fake_signature(payload), + signed_payload=payload, + voter_wot_status="member", + voter_is_smith=False, + ) + session.add(vote) + + await session.flush() + print(f" VoteSession for '{item.title}': created (10 pour, 1 contre)") + + # --------------------------------------------------------------------------- # Main seed runner # --------------------------------------------------------------------------- async def run_seed(): - """Execute all seed functions inside a single transaction.""" print("=" * 60) print("Glibredecision - Seed Database") print("=" * 60) async with async_session() as session: async with session.begin(): - print("\n[1/5] Formula Configs...") + print("\n[1/7] Formula Configs...") formulas = await seed_formula_configs(session) - print("\n[2/5] Voting Protocols (premier pack de modalites)...") - await seed_voting_protocols(session, formulas) + print("\n[2/7] Voting Protocols...") + protocols = await seed_voting_protocols(session, formulas) - print("\n[3/5] Document: Acte d'engagement certification...") + print("\n[3/7] Document: Acte d'engagement certification...") await seed_document_engagement_certification(session) - print("\n[4/5] Document: Acte d'engagement forgeron...") - await seed_document_engagement_forgeron(session) + print("\n[4/7] Document: Acte d'engagement forgeron v2.0.0...") + doc_forgeron = await seed_document_engagement_forgeron(session) - print("\n[5/5] Decision: Runtime Upgrade...") + print("\n[5/7] Decision: Runtime Upgrade...") await seed_decision_runtime_upgrade(session) + print("\n[6/7] Simulated voters...") + voters = await seed_voters(session) + + print("\n[7/7] Votes on first 3 engagements forgeron...") + await seed_votes_on_items( + session, + doc_forgeron, + protocols["Vote majoritaire"], + voters, + ) + print("\n" + "=" * 60) print("Seed complete.") print("=" * 60) diff --git a/frontend/app/app.vue b/frontend/app/app.vue index f574f7d..6c92e4e 100644 --- a/frontend/app/app.vue +++ b/frontend/app/app.vue @@ -5,7 +5,7 @@ const { initMood } = useMood() const navigationItems = [ { - label: 'Documents de reference', + label: 'Documents', icon: 'i-lucide-book-open', to: '/documents', }, @@ -44,10 +44,7 @@ const ws = useWebSocket() const { setupWsNotifications } = useNotifications() onMounted(async () => { - // Apply saved mood / ambiance initMood() - - // Hydrate auth from localStorage auth.hydrateFromStorage() if (auth.token) { try { @@ -56,8 +53,6 @@ onMounted(async () => { auth.logout() } } - - // Connect WebSocket and setup notifications ws.connect() setupWsNotifications(ws) }) @@ -65,88 +60,81 @@ onMounted(async () => { onUnmounted(() => { ws.disconnect() }) + +function isActive(to: string) { + return route.path === to || route.path.startsWith(to + '/') +} diff --git a/frontend/app/assets/css/moods.css b/frontend/app/assets/css/moods.css index 8faa9ba..07e9a50 100644 --- a/frontend/app/assets/css/moods.css +++ b/frontend/app/assets/css/moods.css @@ -1,167 +1,207 @@ /* ========================================================================== Glibredecision — Mood / Ambiance System 4 moods: Peps (light), Zen (light), Chagrine (dark), Grave (dark) + + Design: saturated, modern, zero pastels. ========================================================================== */ /* -------------------------------------------------------------------------- - Peps — Energique et chaleureux (Light) + Peps — Energique, vif, franc (Light) -------------------------------------------------------------------------- */ .mood-peps { - --mood-bg: #ffffff; - --mood-surface: #fffbf5; - --mood-surface-hover: #fff5ea; - --mood-text: #1a1a1a; - --mood-text-muted: #6b6b6b; - --mood-accent: #e85d26; - --mood-accent-soft: #fff3ed; + --mood-bg: #fafafa; + --mood-surface: #ffffff; + --mood-surface-hover: #f5f0ec; + --mood-text: #18120e; + --mood-text-muted: #6e5f52; + --mood-accent: #d44a10; + --mood-accent-soft: rgba(212, 74, 16, 0.08); --mood-accent-text: #ffffff; - --mood-border: #fde8d8; - --mood-success: #22c55e; - --mood-warning: #f59e0b; - --mood-error: #ef4444; - --mood-gradient: linear-gradient(135deg, #fff8f0 0%, #ffffff 100%); - --mood-shadow: rgba(232, 93, 38, 0.08); + --mood-border: #e0d5cb; + --mood-success: #18843b; + --mood-warning: #c27e07; + --mood-error: #c42b2b; + --mood-gradient: linear-gradient(160deg, #faf8f5 0%, #ffffff 50%, #faf7f4 100%); + --mood-shadow: rgba(120, 60, 10, 0.06); + --mood-input-bg: #ffffff; + --mood-input-border: #c9bdb0; + --mood-input-focus: #d44a10; - --mood-status-prepa: #fed7aa; - --mood-status-prepa-text: #9a3412; - --mood-status-vote: #bfdbfe; - --mood-status-vote-text: #1e40af; - --mood-status-vigueur: #bbf7d0; - --mood-status-vigueur-text: #166534; - --mood-status-clos: #e5e7eb; - --mood-status-clos-text: #374151; + --mood-status-prepa: #b35c0a; + --mood-status-prepa-bg: rgba(179, 92, 10, 0.12); + --mood-status-vote: #1856a8; + --mood-status-vote-bg: rgba(24, 86, 168, 0.10); + --mood-status-vigueur: #18843b; + --mood-status-vigueur-bg: rgba(24, 132, 59, 0.10); + --mood-status-clos: #5c5c5c; + --mood-status-clos-bg: rgba(92, 92, 92, 0.08); } /* -------------------------------------------------------------------------- - Zen — Calme et serein (Light) + Zen — Ancre, equilibre, sobre (Light) -------------------------------------------------------------------------- */ .mood-zen { - --mood-bg: #f8faf8; + --mood-bg: #f7f9f7; --mood-surface: #ffffff; - --mood-surface-hover: #f0f7f2; - --mood-text: #1a2e1a; - --mood-text-muted: #5f7a5f; - --mood-accent: #4a9e6f; - --mood-accent-soft: #ecf5ef; + --mood-surface-hover: #edf3ee; + --mood-text: #141e14; + --mood-text-muted: #4a6650; + --mood-accent: #2d7a4a; + --mood-accent-soft: rgba(45, 122, 74, 0.07); --mood-accent-text: #ffffff; - --mood-border: #d4e7d9; - --mood-success: #34d399; - --mood-warning: #fbbf24; - --mood-error: #f87171; - --mood-gradient: linear-gradient(135deg, #f0f7f2 0%, #f8faf8 100%); - --mood-shadow: rgba(74, 158, 111, 0.08); + --mood-border: #c2d4c6; + --mood-success: #1d8a42; + --mood-warning: #b07309; + --mood-error: #be3232; + --mood-gradient: linear-gradient(160deg, #f4f8f4 0%, #ffffff 50%, #f5f8f5 100%); + --mood-shadow: rgba(30, 80, 50, 0.05); + --mood-input-bg: #ffffff; + --mood-input-border: #a8c0ad; + --mood-input-focus: #2d7a4a; - --mood-status-prepa: #fde68a; - --mood-status-prepa-text: #78350f; - --mood-status-vote: #a7f3d0; - --mood-status-vote-text: #065f46; - --mood-status-vigueur: #bbf7d0; - --mood-status-vigueur-text: #166534; - --mood-status-clos: #d1d5db; - --mood-status-clos-text: #374151; + --mood-status-prepa: #9e6b0a; + --mood-status-prepa-bg: rgba(158, 107, 10, 0.10); + --mood-status-vote: #1565a5; + --mood-status-vote-bg: rgba(21, 101, 165, 0.10); + --mood-status-vigueur: #1d8a42; + --mood-status-vigueur-bg: rgba(29, 138, 66, 0.10); + --mood-status-clos: #606060; + --mood-status-clos-bg: rgba(96, 96, 96, 0.08); } /* -------------------------------------------------------------------------- - Chagrine — Profond et subtil (Dark) + Chagrine — Dense, veloute, introspectif (Dark) -------------------------------------------------------------------------- */ .mood-chagrine { - --mood-bg: #1a1625; - --mood-surface: #231e30; - --mood-surface-hover: #2d2640; - --mood-text: #e8e0f0; - --mood-text-muted: #9b8fb5; - --mood-accent: #9b7fd4; - --mood-accent-soft: #2d2640; + --mood-bg: #16121e; + --mood-surface: #1e1828; + --mood-surface-hover: #281f36; + --mood-text: #e0d8ec; + --mood-text-muted: #8e80a8; + --mood-accent: #8b6cc4; + --mood-accent-soft: rgba(139, 108, 196, 0.12); --mood-accent-text: #ffffff; - --mood-border: #342d45; - --mood-success: #6ee7b7; - --mood-warning: #fcd34d; - --mood-error: #fca5a5; - --mood-gradient: linear-gradient(135deg, #1a1625 0%, #231e30 100%); - --mood-shadow: rgba(155, 127, 212, 0.12); + --mood-border: #2e2540; + --mood-success: #48c278; + --mood-warning: #d4a030; + --mood-error: #e06060; + --mood-gradient: linear-gradient(160deg, #16121e 0%, #1e1828 50%, #1a1524 100%); + --mood-shadow: rgba(100, 60, 180, 0.10); + --mood-input-bg: #1e1828; + --mood-input-border: #3a2e52; + --mood-input-focus: #8b6cc4; - --mood-status-prepa: #4c1d95; - --mood-status-prepa-text: #ddd6fe; - --mood-status-vote: #312e81; - --mood-status-vote-text: #c7d2fe; - --mood-status-vigueur: #064e3b; - --mood-status-vigueur-text: #a7f3d0; - --mood-status-clos: #2d2640; - --mood-status-clos-text: #9b8fb5; + --mood-status-prepa: #c4a050; + --mood-status-prepa-bg: rgba(196, 160, 80, 0.14); + --mood-status-vote: #7090d0; + --mood-status-vote-bg: rgba(112, 144, 208, 0.14); + --mood-status-vigueur: #48c278; + --mood-status-vigueur-bg: rgba(72, 194, 120, 0.14); + --mood-status-clos: #706080; + --mood-status-clos-bg: rgba(112, 96, 128, 0.12); } /* -------------------------------------------------------------------------- - Grave — Serieux et solennel (Dark) + Grave — Mineral, solennel, net (Dark) -------------------------------------------------------------------------- */ .mood-grave { - --mood-bg: #141518; - --mood-surface: #1c1d21; - --mood-surface-hover: #262420; - --mood-text: #e5e5e0; - --mood-text-muted: #8a8a85; - --mood-accent: #d4a545; - --mood-accent-soft: #262420; - --mood-accent-text: #141518; - --mood-border: #2a2b30; - --mood-success: #86efac; - --mood-warning: #fde68a; - --mood-error: #fca5a5; - --mood-gradient: linear-gradient(135deg, #141518 0%, #1c1d21 100%); - --mood-shadow: rgba(212, 165, 69, 0.10); + --mood-bg: #111214; + --mood-surface: #191a1e; + --mood-surface-hover: #22201c; + --mood-text: #e2e0d8; + --mood-text-muted: #8a877e; + --mood-accent: #c49530; + --mood-accent-soft: rgba(196, 149, 48, 0.10); + --mood-accent-text: #111214; + --mood-border: #2a2a2e; + --mood-success: #4ac070; + --mood-warning: #d4a530; + --mood-error: #d85050; + --mood-gradient: linear-gradient(160deg, #111214 0%, #191a1e 50%, #141518 100%); + --mood-shadow: rgba(160, 120, 30, 0.08); + --mood-input-bg: #191a1e; + --mood-input-border: #38362e; + --mood-input-focus: #c49530; - --mood-status-prepa: #78350f; - --mood-status-prepa-text: #fde68a; - --mood-status-vote: #1e3a5f; - --mood-status-vote-text: #93c5fd; - --mood-status-vigueur: #14532d; - --mood-status-vigueur-text: #86efac; - --mood-status-clos: #27272a; - --mood-status-clos-text: #a1a1aa; + --mood-status-prepa: #c49530; + --mood-status-prepa-bg: rgba(196, 149, 48, 0.14); + --mood-status-vote: #5a90c8; + --mood-status-vote-bg: rgba(90, 144, 200, 0.14); + --mood-status-vigueur: #4ac070; + --mood-status-vigueur-bg: rgba(74, 192, 112, 0.14); + --mood-status-clos: #686860; + --mood-status-clos-bg: rgba(104, 104, 96, 0.12); } /* ========================================================================== - Base Utilities + Global design tokens — modern, refined, thin ========================================================================== */ -/* Transition all mood changes smoothly */ body { transition: background-color 0.3s ease, color 0.3s ease; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -/* Status labels — clickable pill style */ +/* --- Status pills — compact, saturated, NO pastels --- */ .status-pill { display: inline-flex; align-items: center; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.75rem; + padding: 3px 10px; + border-radius: 4px; + font-size: 0.6875rem; font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.15s ease; user-select: none; + border: none; } .status-pill:hover { - filter: brightness(0.92); - transform: scale(1.05); + filter: brightness(1.1); } .status-pill.active { - ring: 2px; - ring-offset: 2px; + box-shadow: 0 0 0 2px var(--mood-accent); } .status-prepa { - background: var(--mood-status-prepa); - color: var(--mood-status-prepa-text); + background: var(--mood-status-prepa-bg); + color: var(--mood-status-prepa); } .status-vote { - background: var(--mood-status-vote); - color: var(--mood-status-vote-text); + background: var(--mood-status-vote-bg); + color: var(--mood-status-vote); } .status-vigueur { - background: var(--mood-status-vigueur); - color: var(--mood-status-vigueur-text); + background: var(--mood-status-vigueur-bg); + color: var(--mood-status-vigueur); } .status-clos { - background: var(--mood-status-clos); - color: var(--mood-status-clos-text); + background: var(--mood-status-clos-bg); + color: var(--mood-status-clos); +} + +/* ========================================================================== + Global overrides — Nuxt UI refinements + ========================================================================== */ + +/* Inputs: thin, clean, mood-aware */ +:root .mood-peps, +:root .mood-zen, +:root .mood-chagrine, +:root .mood-grave { + /* UInput / UTextarea */ + --ui-border: var(--mood-input-border); + --ui-bg: var(--mood-input-bg); + --ui-text-highlighted: var(--mood-accent); +} + +/* UButton refinements */ +:root .mood-peps button[class*="UButton"], +:root .mood-zen button[class*="UButton"], +:root .mood-chagrine button[class*="UButton"], +:root .mood-grave button[class*="UButton"] { + font-weight: 500; + letter-spacing: 0.01em; } diff --git a/frontend/app/components/common/MoodSwitcher.vue b/frontend/app/components/common/MoodSwitcher.vue index 06df126..1e3aa81 100644 --- a/frontend/app/components/common/MoodSwitcher.vue +++ b/frontend/app/components/common/MoodSwitcher.vue @@ -3,52 +3,51 @@ const { currentMood, moods, setMood } = useMood() diff --git a/frontend/app/components/common/SectionLayout.vue b/frontend/app/components/common/SectionLayout.vue index 47d41a9..9835c3c 100644 --- a/frontend/app/components/common/SectionLayout.vue +++ b/frontend/app/components/common/SectionLayout.vue @@ -1,10 +1,9 @@ diff --git a/frontend/app/components/common/ToolboxVignette.vue b/frontend/app/components/common/ToolboxVignette.vue index 0b1b35f..090b707 100644 --- a/frontend/app/components/common/ToolboxVignette.vue +++ b/frontend/app/components/common/ToolboxVignette.vue @@ -1,9 +1,6 @@ diff --git a/frontend/app/composables/useMood.ts b/frontend/app/composables/useMood.ts index 60e4b42..929b64e 100644 --- a/frontend/app/composables/useMood.ts +++ b/frontend/app/composables/useMood.ts @@ -5,16 +5,17 @@ export interface Mood { label: string description: string icon: string + color: string isDark: boolean } const STORAGE_KEY = 'glibredecision_mood' const moods: Mood[] = [ - { id: 'peps', label: 'Peps', description: 'Energique et chaleureux', icon: 'i-lucide-sun', isDark: false }, - { id: 'zen', label: 'Zen', description: 'Calme et serein', icon: 'i-lucide-leaf', isDark: false }, - { id: 'chagrine', label: 'Chagrine', description: 'Profond et subtil', icon: 'i-lucide-moon', isDark: true }, - { id: 'grave', label: 'Grave', description: 'Serieux et solennel', icon: 'i-lucide-shield', isDark: true }, + { id: 'peps', label: 'Peps', description: 'Energique et franc', icon: 'i-lucide-sun', color: '#d44a10', isDark: false }, + { id: 'zen', label: 'Zen', description: 'Ancre et sobre', icon: 'i-lucide-leaf', color: '#2d7a4a', isDark: false }, + { id: 'chagrine', label: 'Chagrine', description: 'Dense et veloute', icon: 'i-lucide-moon', color: '#8b6cc4', isDark: true }, + { id: 'grave', label: 'Grave', description: 'Mineral et solennel', icon: 'i-lucide-shield', color: '#c49530', isDark: true }, ] const currentMood: Ref = ref('peps') diff --git a/frontend/app/pages/index.vue b/frontend/app/pages/index.vue index de4629a..fd8e2cd 100644 --- a/frontend/app/pages/index.vue +++ b/frontend/app/pages/index.vue @@ -2,8 +2,8 @@ /** * Dashboard / Page d'accueil — Glibredecision. * - * Accueil chaleureux avec onboarding : cartes d'entree vers les sections, - * banniere de connexion, apercu de la boite a outils et activite recente. + * Accueil sobre et lisible : cartes d'entree, banniere connexion, + * apercu boite a outils et activite recente. */ const documents = useDocumentsStore() const decisions = useDecisionsStore() @@ -67,7 +67,6 @@ const recentDecisions = computed(() => { .slice(0, 5) }) -/** Format a date string to a localized relative or absolute string. */ function formatDate(dateStr: string): string { const date = new Date(dateStr) const now = new Date() @@ -76,7 +75,7 @@ function formatDate(dateStr: string): string { if (diffHours < 1) { const diffMinutes = Math.floor(diffMs / (1000 * 60)) - return diffMinutes <= 1 ? 'Il y a un instant' : `Il y a ${diffMinutes} min` + return diffMinutes <= 1 ? 'A l\'instant' : `Il y a ${diffMinutes} min` } if (diffHours < 24) { return `Il y a ${Math.floor(diffHours)}h` @@ -89,191 +88,147 @@ function formatDate(dateStr: string): string { diff --git a/frontend/app/pages/login.vue b/frontend/app/pages/login.vue index 8454361..5fc8648 100644 --- a/frontend/app/pages/login.vue +++ b/frontend/app/pages/login.vue @@ -19,11 +19,9 @@ async function handleLogin() { step.value = 'signing' await auth.login(address.value.trim()) step.value = 'success' - - // Redirect to home after a brief moment setTimeout(() => { router.push('/') - }, 1000) + }, 800) } catch (err: any) { errorMessage.value = err?.data?.detail || err?.message || 'Erreur lors de la connexion' step.value = 'input' @@ -31,37 +29,22 @@ async function handleLogin() { } const steps = computed(() => [ - { - title: 'Adresse Duniter', - description: 'Entrez votre adresse SS58 Duniter V2', - icon: 'i-lucide-user', - active: step.value === 'input', - complete: step.value !== 'input', - }, - { - title: 'Challenge cryptographique', - description: 'Un challenge aleatoire est genere par le serveur', - icon: 'i-lucide-shield', - active: step.value === 'challenge', - complete: step.value === 'signing' || step.value === 'success', - }, - { - title: 'Signature Ed25519', - description: 'Signez le challenge avec votre cle privee', - icon: 'i-lucide-key', - active: step.value === 'signing', - complete: step.value === 'success', - }, - { - title: 'Connexion', - description: 'Votre identite est verifiee et la session creee', - icon: 'i-lucide-check-circle', - active: step.value === 'success', - complete: false, - }, + { label: 'Adresse SS58', icon: 'i-lucide-user', done: step.value !== 'input' }, + { label: 'Challenge', icon: 'i-lucide-shield', done: step.value === 'signing' || step.value === 'success' }, + { label: 'Signature', icon: 'i-lucide-key', done: step.value === 'success' }, + { label: 'Connecte', icon: 'i-lucide-check', done: false }, ]) -// Redirect if already authenticated +const activeStepIndex = computed(() => { + switch (step.value) { + case 'input': return 0 + case 'challenge': return 1 + case 'signing': return 2 + case 'success': return 3 + default: return 0 + } +}) + onMounted(() => { if (auth.isAuthenticated) { router.push('/') @@ -70,108 +53,289 @@ onMounted(() => { + +