Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6.0 KiB
Vote pour les prochaines fonctionnalités (Roadmap) — Design
Issue : #94 Date : 2026-06-30
Objectif
Permettre aux abonnés de voter pour les prochaines fonctionnalités depuis une
section Roadmap réservée aux abonnés (/compte/roadmap). La même roadmap est
exposée en lecture seule au public (/a-propos/roadmap). Les fonctionnalités
sont gérées dans GitHub via des labels ; le changelog du dépôt est affiché en
bas des deux pages.
Modèle d'attribution des votes
- Un nouvel abonné reçoit 2 votes au moment où sa période d'essai se
termine (passage du statut
trial/pending àactive). - Il gagne ensuite +1 vote par semaine tant que son abonnement est actif.
- Les votes ne s'accumulent pas pendant une période sans abonnement (gel).
- Au réabonnement, l'accumulation reprend mais les 2 votes initiaux ne sont pas re-crédités.
- Un abonné peut voter plusieurs fois pour la même fonctionnalité.
- Un vote dépensé est définitif : pas de retrait possible.
Accumulation paresseuse (pas de cronjob)
État stocké sur la table subscriptions (2 colonnes ajoutées par migration) :
| Colonne | Type | Rôle |
|---|---|---|
votes_balance |
INTEGER NOT NULL DEFAULT 0 |
Solde de votes dépensable |
votes_credited_until |
TEXT (NULL par défaut) |
Curseur d'accumulation ; NULL tant que jamais activé |
Fonction credit_pending(user_id), appelée au chargement de
/compte/roadmap et avant chaque vote :
- Charger la ligne d'abonnement de l'utilisateur. Si absente → ne rien faire.
- Si
votes_credited_untilest NULL et statutactive(= fin d'essai atteinte) → créditer les +2 initiaux,votes_credited_until = maintenant. - Si
votes_credited_untilposé et statutactive→semaines = floor((maintenant − votes_credited_until) / 7 jours); sisemaines > 0:votes_balance += semaineset avancervotes_credited_untildesemaines × 7 jours. - Statut non-
active→ aucun crédit (gel).
Cette fonction est idempotente : recharger la page le même jour ne crédite rien de plus, car le curseur n'avance que par semaines pleines.
Gel au désabonnement / réabonnement
Dans update_from_webhook (src/subscriptions/db.py) :
- Résiliation (
active→cancelled) : appelercredit_pendingpour banquer les semaines acquises ; le statutcancelledbloque ensuite tout crédit (le curseur reste figé). - Réabonnement (
cancelled→active) : remettrevotes_credited_until = maintenantpour ne pas créditer la période sans abonnement, sans re-créditer les +2 (curseur non-NULL).
Registre des votes émis
Nouvelle table dans users.sqlite :
CREATE TABLE feature_votes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
issue_number INTEGER NOT NULL,
created_at TEXT NOT NULL
);
- Une ligne par vote émis (vote multiple = plusieurs lignes).
- Décompte d'une fonctionnalité =
COUNT(*)groupé parissue_number. - Décomptes publics (visibles sur
/a-propos/roadmap).
Migrations ajoutées dans src/migrations.py (_MIGRATIONS) :
- ajout de
votes_balanceetvotes_credited_untilsursubscriptions - création de la table
feature_votes
Intégration GitHub & cache
Nouveau module src/util/roadmap.py :
fetch_roadmap_issues(), décoré@cache.memoize(timeout=3600)(cache 1 h) :httpx.getsurGET /repos/ColinMaudry/decp.info/issues?state=open, puis filtrage local par label. Retourne deux listes de dicts{number, title, html_url}: lesen courset lesmis au vote.- Appel anonyme (pas de
GITHUB_TOKEN) : suffisant pour un dépôt public avec un cache d'1 heure. - On reste sur
httpx, déjà dépendance du projet (Dash,src/utils/data.py,src/utils/tracking.py) — pas d'ajout derequests.
src/util/roadmap.py centralise aussi :
- la récupération des décomptes (
COUNTgroupé depuisfeature_votes) ; - un constructeur de composants
render_roadmap(editable: bool)partagé par les deux pages.
Pages & navigation
Page abonné — src/pages/compte_roadmap.py
- Route
/compte/roadmap,require_subscription=True, viaaccount_shell. - À l'entrée :
credit_pending(user_id). - Contenu :
- Bandeau « Il te reste N votes » (solde courant).
- Section « En cours » (label
en cours) — titres liés vers GitHub, pas de vote. - Section « Au vote » (label
mis au vote) — triée par votes décroissants ; chaque fonctionnalité affiche son décompte + un bouton « Voter » (désactivé si solde = 0). - Changelog (
CHANGELOG.mdrendu viadcc.Markdown).
Action de vote : vérifier votes_balance > 0, décrémenter votes_balance,
insérer une ligne feature_votes, rafraîchir l'affichage.
Page publique — src/pages/a_propos/roadmap.py
- Route
/a-propos/roadmap, viaapropos_shell. - Identique mais
render_roadmap(editable=False): décomptes publics visibles, aucun bouton, pas de bandeau de solde.
Navigation
- Ajout d'une entrée
roadmapdansSECTIONSde_compte_shell.py(require_subscription=True). - Ajout d'une entrée
roadmapdansSECTIONSde_apropos_shell.py.
Lien version & changelog
- Dans
src/app.py(~ligne 194), le lien du numéro de version pointe vers/a-propos/roadmapau lieu de l'URL GitHub duCHANGELOG.md. - Lecture de
CHANGELOG.md(racine du dépôt) rendue viadcc.Markdown, partagée par les deux pages.
Hors périmètre (YAGNI)
- Pas de cronjob / timer pour l'accumulation.
- Pas de retrait de vote.
- Pas de
GITHUB_TOKEN. - Pas de gestion d'écriture vers GitHub (les fonctionnalités restent gérées manuellement via les labels GitHub).