J'ai une centaine de tâches vivantes : un site à réparer, un client à relancer, un article à
finir, une facture à vérifier. Pendant des années j'ai utilisé un outil en ligne, avec des
colonnes, des étiquettes, des glisser-déposer et une application mobile.
Le problème n'était pas l'outil. Le problème, c'est que je passais plus de temps à organiser
mes tâches qu'à les faire, et qu'aucune de ces tâches ne laissait de trace vérifiable une fois
« archivée ».
Aujourd'hui, tout tient dans un fichier de 192 Ko et une commande en ligne.
Ce que j'ai réellement besoin de stocker
Quatre choses, pas plus :
- quoi (un titre),
- pour quel projet,
- pour quand (une échéance, souvent absente),
- et où j'en suis (un statut).
Plus deux champs qui ne viennent pas des outils classiques : le type (est-ce moi qui dois
le faire, ou un agent/une machine ?) et la preuve.
CREATE TABLE missions ( id TEXT PRIMARY KEY, projet TEXT, titre TEXT NOT NULL, statut TEXT NOT NULL DEFAULT 'todo', type TEXT NOT NULL DEFAULT 'agent', due_date TEXT, owner TEXT, preuve TEXT, note TEXT, cree_le TEXT, maj_le TEXT ); CREATE INDEX idx_due ON missions(due_date, statut); statut vaut todo, doing, done ou blocked. Pas de table de statuts, pas de
métadonnées imbriquées : quatre chaînes et une contrainte applicative.
Total : 190 lignes et 192 Ko pour une centaine de tâches et leur historique. La base entière
tient dans une pièce jointe.
La colonne qui a tout changé : preuve
J'ai eu un problème répété : des tâches marquées « faites » qui ne l'étaient pas. Un serveur
« installé » qui n'existait pas. Un site « livré » dont personne ne retrouvait l'URL. Dans les
deux cas, la case était cochée par quelqu'un de bonne foi, pressé, qui avait fait presque
le travail.
J'ai ajouté une règle, puis une colonne pour la rendre mécanique : une tâche n'est terminée
que si elle peut citer un artefact vérifiable.
-- la contrainte, côté applicatif : pas de preuve, pas de clôture UPDATE missions SET statut = 'done', preuve = :chemin, -- /home/moi/livrables/rapport.pdf maj_le = datetime('now') WHERE id = :id AND :preuve IS NOT NULL AND :preuve <> ''; Trois formes de preuve acceptées, et trois seulement :
- un chemin de fichier qui existe et qui a été produit après la création de la tâche ;
- une URL qui répond HTTP 200 testée au moment de la clôture ;
- un hash de commit.
python3 pilotage.py done m-42 --preuve "/home/moi/livrables/rapport.pdf" Et le contrôle qui va avec, parce qu'une règle qu'on ne vérifie pas n'est pas une règle :
test -f "$(sqlite3 missions.db "SELECT preuve FROM missions WHERE id='m-42';")" \ && echo "preuve OK" Depuis, la question « où en est ce chantier ? » a une réponse en une requête, et non plus en
une conversation.
Les quatre requêtes qui servent tous les jours
Ce qui est en retard — la seule requête que je regarde le matin :
SELECT id, titre, due_date, projet FROM missions WHERE statut <> 'done' AND due_date IS NOT NULL AND date(due_date) <= date('now', '+14 day') ORDER BY date(due_date) ASC; Ce que je dois faire moi (par opposition à ce que les scripts font) :
SELECT id, titre, projet FROM missions WHERE type = 'ludo' AND statut = 'todo'; Cette séparation est plus utile qu'elle n'en a l'air : elle évite de demander à une machine
un geste qu'elle n'a pas le droit de faire (payer, signer, téléphoner), et elle évite de
s'étonner qu'un chantier n'avance pas alors qu'il attend un humain.
La charge par projet :
SELECT projet, SUM(statut = 'todo') AS a_faire, SUM(statut = 'doing') AS en_cours, SUM(statut = 'blocked') AS bloque FROM missions GROUP BY projet ORDER BY a_faire DESC; Ce qui est bloqué depuis trop longtemps :
SELECT id, titre, ROUND(julianday('now') - julianday(maj_le)) AS jours FROM missions WHERE statut = 'blocked' AND julianday('now') - julianday(maj_le) > 14 ORDER BY jours DESC; La commande, en 120 lignes d'argparse
SQLite sans interface, c'est vite pénible. Un petit CLI suffit :
import argparse, sqlite3, subprocess, datetime def db(): con = sqlite3.connect("missions.db") con.row_factory = sqlite3.Row return con p = argparse.ArgumentParser() sub = p.add_subparsers(dest="cmd", required=True) d = sub.add_parser("due"); d.add_argument("--jours", type=int, default=14) l = sub.add_parser("list"); l.add_argument("--statut", default="todo") c = sub.add_parser("done"); c.add_argument("id"); c.add_argument("--preuve", required=True) a = p.parse_args() if a.cmd == "due": for r in db().execute( "SELECT id, titre, due_date FROM missions WHERE statut<>'done' " "AND due_date IS NOT NULL AND date(due_date)<=date('now',?) ORDER BY due_date", (f"+{a.jours} day",)): print(f"{r['due_date']} {r['id']:<12} {r['titre'][:60]}") En pratique je n'utilise que trois commandes : due, list, done. Tout le reste, c'est du
SQL tapé une fois par mois.
Sauvegarde : cp ne suffit pas
Le premier réflexe — cp missions.db backup/ — est faux si un job écrit pendant la copie :
on récupère un fichier dont la moitié des pages est incohérente. Trois réglages et une
commande règlent le problème.
PRAGMA journal_mode = WAL; -- lectures concurrentes sans blocage PRAGMA busy_timeout = 5000; -- attendre 5 s plutôt que planter PRAGMA synchronous = NORMAL; -- bon compromis sur un poste de travail sqlite3 missions.db ".backup '/home/moi/backups/missions-$(date +%F).db'" .backup utilise l'API de sauvegarde en ligne : cohérent, même si une écriture est en cours.
Une ligne dans le cron, et le sujet est clos.
Ce que SQLite ne fait pas
Je préfère le dire clairement, parce que la mode du « tout dans un fichier » produit aussi des
désillusions :
- Pas de multi-utilisateur concurrent. Un écrivain à la fois. Pour une équipe de trois qui écrit toute la journée, ça devient un sujet. Pour un poste de travail et quelques scripts, c'est un non-problème.
- Pas de synchronisation mobile. Un fichier sur un disque local ne suit pas en déplacement. Si vous avez besoin de cocher des tâches dans le train, SQLite n'est pas la réponse.
- Pas de vue partagée. Dès qu'un tiers doit voir l'état, il faut exporter (un fichier markdown, un JSON, un petit serveur) — pas donner accès au fichier.
- Pas de sauvegarde automatique. Le fichier est unique : sa perte est totale. Une sauvegarde quotidienne n'est pas optionnelle.
Et une limite plus discrète : les schémas évoluent. Ajouter une colonne est trivial
(ALTER TABLE ... ADD COLUMN), la renommer ou la supprimer ne l'est pas. Je garde donc les
migrations dans un dossier migrations/ numéroté, avec la version courante dans
PRAGMA user_version. Dix lignes de code, et plus de mauvaise surprise.
Pourquoi je ne reviens pas en arrière
Parce que ma base de tâches est maintenant une donnée, pas une interface. Je peux la lire
avec sqlite3, l'exporter en CSV, la versionner, l'interroger depuis un script Python, la
sauvegarder par un simple .backup. Aucune de ces quatre choses n'était possible avec mon
outil en ligne, qui me laissait au mieux un export JSON à la mise en forme douteuse.
Et puis il y a le silence. Pas de notification, pas de rappel, pas d'e-mail « vous n'avez pas
ouvert votre tableau de bord cette semaine ». Une commande, une réponse, rien d'autre.
Deux cents tâches, cent quatre-vingt-douze kilo-octets, zéro euro par mois. Ce n'est pas une
prouesse technique. C'est juste un outil qui fait le travail et se tait.