Files
colibre/docs/superpowers/specs/2026-06-30-roadmap-votes-design.md
Colin Maudry d436fd4d9e docs: fix chemin src/utils dans spec roadmap #94
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:19:02 +02:00

145 lines
6.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Vote pour les prochaines fonctionnalités (Roadmap) — Design
Issue : [#94](https://github.com/ColinMaudry/decp.info/issues/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** :
1. Charger la ligne d'abonnement de l'utilisateur. Si absente → ne rien faire.
2. Si `votes_credited_until` est NULL **et** statut `active` (= fin d'essai
atteinte) → créditer les **+2 initiaux**, `votes_credited_until = maintenant`.
3. Si `votes_credited_until` posé **et** statut `active`
`semaines = floor((maintenant votes_credited_until) / 7 jours)` ;
si `semaines > 0` : `votes_balance += semaines` et avancer
`votes_credited_until` de `semaines × 7 jours`.
4. 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`) : appeler `credit_pending` pour
banquer les semaines acquises ; le statut `cancelled` bloque ensuite tout
crédit (le curseur reste figé).
- **Réabonnement** (`cancelled``active`) : remettre
`votes_credited_until = maintenant` pour 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` :
```sql
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é par `issue_number`.
- Décomptes **publics** (visibles sur `/a-propos/roadmap`).
Migrations ajoutées dans `src/migrations.py` (`_MIGRATIONS`) :
- ajout de `votes_balance` et `votes_credited_until` sur `subscriptions`
- création de la table `feature_votes`
## Intégration GitHub & cache
Nouveau module `src/utils/roadmap.py` :
- `fetch_roadmap_issues()`, décoré `@cache.memoize(timeout=3600)` (cache 1 h) :
`httpx.get` sur `GET /repos/ColinMaudry/decp.info/issues?state=open`, puis
filtrage local par label. Retourne deux listes de dicts
`{number, title, html_url}` : les `en cours` et les `mis 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 de `requests`.
`src/utils/roadmap.py` centralise aussi :
- la récupération des décomptes (`COUNT` groupé depuis `feature_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`, via `account_shell`.
- À l'entrée : `credit_pending(user_id)`.
- Contenu :
1. Bandeau « Il te reste **N** votes » (solde courant).
2. Section **« En cours »** (label `en cours`) — titres liés vers GitHub, pas
de vote.
3. 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).
4. **Changelog** (`CHANGELOG.md` rendu via `dcc.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`, via `apropos_shell`.
- Identique mais `render_roadmap(editable=False)` : décomptes publics visibles,
**aucun bouton**, pas de bandeau de solde.
### Navigation
- Ajout d'une entrée `roadmap` dans `SECTIONS` de `_compte_shell.py`
(`require_subscription=True`).
- Ajout d'une entrée `roadmap` dans `SECTIONS` de `_apropos_shell.py`.
## Lien version & changelog
- Dans `src/app.py` (~ligne 194), le lien du numéro de version pointe vers
`/a-propos/roadmap` au lieu de l'URL GitHub du `CHANGELOG.md`.
- Lecture de `CHANGELOG.md` (racine du dépôt) rendue via `dcc.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).