Deux réglages du moteur manquaient pour que l'aperçu fonctionne hors
navigateur, tous deux constatés en lançant la fenêtre native.
Qt WebEngine laisse PdfViewerEnabled à False et le conditionne à
PluginsEnabled : le volet n'affichait qu'un bouton « Ouvrir » au lieu du
document. pywebview n'expose pas ces attributs et construit son propre
profil, donc on les pose sur la vue au chargement de la fenêtre — bien
avant qu'un document, et donc une <iframe>, soit ouvert. Sans objet sous
Windows, où WebView2 embarque déjà son lecteur.
pywebview démarre par ailleurs en mode privé, sur un profil éphémère qui
ne conserve ni cookies ni localStorage. L'interface y range la largeur du
volet d'aperçu, et le lecteur PDF du moteur y garde l'état de sa barre
latérale de vignettes : les deux se seraient réinitialisés à chaque
lancement. L'application ne charge que son propre serveur local, rien de
tiers n'est stocké au passage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le volet PDF était figé à 40 % de la fenêtre. C'est le vrai levier de
lisibilité : élargir le volet augmente d'autant l'échelle du document, et
vers 768 px l'ajustement à la largeur vaut 97 %, soit 100 % de fait. La
poignée se glisse, la largeur est mémorisée et partagée entre l'écran
d'extraction et celui d'édition, le double-clic revient au défaut — porté
de 40 % à 50 % pour que la page tienne presque sans défiler.
Un voile couvre la fenêtre pendant le glissement : sans lui l'<iframe> du
PDF capte les évènements souris et le redimensionnement se fige dès qu'on
la survole.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le rendu maison sur <canvas> dimensionnait chaque page à la largeur du
volet, sans jamais pouvoir aller au-delà : sur une page A4 (595,3 pt) dans
un volet de 495 px — soit 40 % d'une fenêtre bureau de 1280 px — cela
faisait une échelle de 0,83x, et les tableaux du CRG composés en 7 pt
tombaient à 5,8 px de haut. Illisible, sans recours : aucun zoom, aucun
re-rendu au redimensionnement (la canvas était simplement étirée en CSS),
et devicePixelRatio à 1 sur écran non-HiDPI, donc aucun suréchantillonnage.
L'<iframe> rend au moteur le zoom, la recherche, l'impression et la
sélection de texte, tous absents jusqu'ici, et redessine à chaque niveau
de zoom. Le document s'ouvre à `#zoom=100` : « ajuster à la largeur » ne
sauverait rien dans un volet étroit (54 %, soit 5 px pour du 7 pt) alors
qu'à 100 % le 7 pt fait 9,3 px. Les mots-clés `page-width`, `view=FitH` et
`pagemode=none` sont ignorés par le lecteur de Chrome ; une valeur
numérique est honorée.
Un lien « ouvrir dans un nouvel onglet » sert de porte de sortie si le
moteur n'embarque pas de lecteur PDF.
pdfjs-dist n'a plus d'utilisateur : bundle principal 966 Ko -> 492 Ko, et
le worker de 1,3 Mo n'est plus émis.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La fiche du lot 15 annonçait « dernier loyer connu, arrêté après févr. 26 »
alors qu'un locataire y est entré le 8 juillet — et la courbe juste dessous,
où la barre de juillet est bien là, la contredisait.
Le loyer en vigueur se lit sur les paliers, et un mois facturé au seul
prorata n'en ouvre pas : celui d'avant s'arrête donc à la dernière mensualité
pleine, quatre mois avant le nouveau bail. Ne rien lire après lui confond deux
états que le compte rendu sépare : un lot qui n'est plus loué, et un lot entre
deux locataires.
La distinction tient à la frontière qui sert déjà à séparer loyer plein et
prorata, la période portée par le compte rendu :
- un prorata qui finit le dernier jour du mois sans partir du premier facture
la fin du mois : quelqu'un est entré, le bail court encore après ;
- un prorata qui part du premier sans l'atteindre facture le début : le bail
s'arrête là ;
- un montant négatif ne fait ni l'un ni l'autre. Un avoir porte parfois la
période exacte de la ligne qu'il annule, et le lire comme une entrée
inventerait une relocation là où rien n'a été loué.
L'entrée l'emporte sur la sortie parce qu'elle vient après : ce lot voit son
locataire partir le 10 mars et le suivant arriver le 8 juillet, et c'est le
second qui décrit son état.
Le montant affiché ne bouge pas — le nouveau bail n'a été facturé qu'au
prorata, et en tirer un loyer mensuel l'inventerait. Seul ce qu'on en dit
change : « dernier loyer plein · nouveau bail depuis juil. 26 ».
Sans relocation, la sortie date enfin la fin de la location au mois où elle a
lieu : « arrêté en cours de mars 26 » plutôt qu'« arrêté après févr. 26 », qui
l'avançait d'un mois.
Sur le parc : 31 lots loués, 1 relouté, 1 réellement arrêté.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Éditer une ligne depuis Dépenses ou Recettes ouvrait le document, puis
relâchait sur la liste des documents : il fallait revenir à la page
d'analyse et refaire ses filtres à la main pour corriger la ligne suivante.
Les deux tables passent maintenant à l'éditeur la vue d'où l'on part,
filtres compris, et il y ramène à ses trois sorties : enregistrement
terminé, abandon, et document introuvable. Sans cette destination — depuis
la liste des documents, ou un lien direct — le repli reste la liste.
La destination arrive par l'URL, donc fabricable par n'importe qui : seule
une page de l'application est suivie (utils/retour.js). Une URL absolue, un
chemin protocol-relatif que le navigateur lirait comme un domaine, ou des
contre-slashs retombent sur le repli.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les filtres des pages Dépenses et Recettes n'existaient que dans l'état du
composant : recharger la page les perdait, et une vue filtrée ne pouvait ni
se partager, ni se mettre en signet, ni servir de destination à un lien.
Chaque page déclare desormais comment ses filtres se lisent et s'écrivent
dans la query (utils/filtresUrl.js). Ce qui vaut sa valeur par défaut n'est
pas écrit : l'URL ne porte que ce qui a été choisi, et reste lisible. Ce qui
en arrive est validé — un identifiant qui n'est pas un entier ou une date au
mauvais format est ignoré plutôt qu'affiché de travers.
Le changement de filtre remplace l'entrée d'historique au lieu d'en empiler
une : le bouton Retour du navigateur quitte la page, il ne défait pas les
filtres un par un.
Côté Dépenses, la requête API dérive de cette même écriture : les paramètres
sont les mêmes des deux côtés, la vue et la requête ne peuvent plus diverger.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le résumé calculait déjà `by_tag`, la page Dépenses n'en faisait rien :
on lisait la dépense par catégorie comptable, jamais par poste. Les deux
répartitions se placent côte à côte — même total, deux découpages — et
l'évolution mensuelle prend toute la largeur, un axe de temps serré étant
le premier à devenir illisible.
Les dépenses sans tag y gardent leur part, en gris : c'est le travail de
tagging qui reste à faire, pas un trou dans le graphique.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le filtre fournisseur était une recherche de sous-chaîne : impossible de
comparer deux fournisseurs, et « PPR » ramenait ses homonymes au passage.
Il devient une sélection multiple exacte, prise dans un menu déroulant
filtrable au clavier (SelectionMultiple, générique et réutilisable). La
liste vient de /api/fournisseurs, remise en ordre alphabétique — l'API la
trie par montant, ce qui se lit bien dans un classement mais rend
introuvable un fournisseur qu'on cherche à l'œil.
Côté API, `fournisseur` devient répétable et s'entend comme un OU exact.
Le filtre tag, lui, ne savait pas demander « ce qui n'est pas encore
tagué » — c'est pourtant la question qui amorce le travail de tagging.
`tag_id=0` le demande, et l'option « Sans tag » l'ouvre depuis la page.
Au passage, les deux endpoints dupliquaient leurs filtres : ils partagent
désormais _appliquer_filtres, pour que les totaux du résumé ne puissent
plus porter sur d'autres lignes que la table.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deux niveaux de repli se cumulaient : la section « Recapitulatif des
operations » puis chaque carte de categorie. Lire une depense demandait
donc un clic sur la section plus un clic par categorie, avant toute
correction.
Les deux s'ouvrent desormais par defaut. « Tout replier » couvre le
besoin inverse, et le repli manuel par categorie reste disponible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un second sélecteur accompagne celui du lot : tout l'historique, trois
mois, six mois, un an, deux ans, cinq ans. Le défaut reste l'historique
entier — un filtre par défaut cacherait des opérations dès l'arrivée sur
la page, sans que rien ne le signale.
La période part dans l'URL, comme le lot : une fiche filtrée se partage
et se recharge telle qu'on la lisait. Une valeur qui ne correspond à
aucune durée proposée est ignorée et retirée de l'adresse.
Le filtrage est demandé au serveur plutôt que rejoué ici : le taux de
recouvrement et le restant dû sortent de requêtes SQL que le navigateur
ne peut pas refaire, et les recalculer de son côté les ferait diverger de
la chronologie affichée juste dessous.
Ce que la fenêtre écarte reste dit, là où le lecteur pourrait le croire
inexistant :
- un bandeau donne les bornes retenues, précise qu'elles sont calées sur
le dernier compte rendu du lot et non sur aujourd'hui, compte les lignes
antérieures exclues et offre de tout réafficher ;
- sous la courbe, le nombre de mois non tracés, et le rappel que le loyer
en vigueur, sa date d'effet et la comparaison au parc restent lus sur
tout l'historique — sans quoi « depuis mai 26 » au-dessus d'un graphe
qui commence en mai passerait pour une contradiction ;
- à côté des noms, le nombre d'occupants antérieurs à la période ;
- le bloc des régularisations hors courbe s'affiche même quand la période
n'en laisse aucune à lister, pour dire qu'il en existe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La fiche d'un lot rendait tout son historique, et rien d'autre. Sur un
parc suivi depuis 2023, lire ce qu'un lot a fait ces trois derniers mois
supposait de faire soi-même la soustraction. Le paramètre `mois` ouvre
une fenêtre sur les chiffres, la chronologie, les intervenants, les
locataires et la courbe du loyer.
Trois décisions la gouvernent, et chacune évite un chiffre qui
mentirait :
- **la fenêtre est calée sur le dernier compte rendu du lot**, jamais sur
aujourd'hui. Les derniers comptes rendus du parc datent de juillet
alors qu'on est en août : une fenêtre glissante depuis la date du jour
décalerait déjà tout d'un mois, et viderait entièrement la fiche d'un
lot sorti de la gestion — une page vide se lisant comme une absence
d'activité plutôt que comme un filtre trop court ;
- **le restant dû y échappe**, parce qu'un stock ne se borne pas comme un
flux. Le ramener à la fenêtre l'annulerait dès qu'aucun compte rendu
n'y tombe : un lot afficherait 0 € dû tout en devant plusieurs
milliers ;
- **le loyer en vigueur et la comparaison au parc y échappent aussi.**
La courbe, elle, est bien coupée, mais après coup : les paliers restent
lus sur la série entière, sans quoi « depuis mai 26 » daterait de la
borne du filtre au lieu de la révision qui a fixé ce loyer, et la
médiane du parc changerait de mois de référence à chaque changement de
période. Rien n'est coupé au-delà de la fenêtre : un bail trimestriel
facturé d'avance porte des mois postérieurs au dernier compte rendu, et
les retirer ferait croire que le lot cesse d'être loué.
Les locataires n'ont ni date d'entrée ni date de sortie — aucune n'est
extraite des comptes rendus. Leur seul rattachement au temps est donc le
document qui les porte. Hors fenêtre, la liste reste malgré tout celle de
la table : la base contient des locataires dont aucune ligne ne dépend, et
la vue par défaut ne filtre rien.
Un filtre qui masque doit dire ce qu'il masque : la réponse porte le
compte des lignes, des mois et des occupants laissés dehors, et les
bornes effectivement retenues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le passage en 0.1.1 avait laissé le verrou sur 0.1.0 ; `uv sync` le
corrige de lui-même à chaque installation, et la correction se
retrouvait donc dans l'arbre de travail de qui installait le projet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un bloc « Loyer » s'intercale entre les totaux et la chronologie. Il
porte le loyer en vigueur, sa date d'effet, le prix au mètre carré et la
dernière révision, puis deux graphiques.
Le premier suit le loyer mois par mois, charges empilées et prix au m²
sur son propre axe. Un mois vacant y reste un blanc, jamais une barre au
sol ; un mois de transition prend une couleur distincte et l'infobulle
dit lequel des deux on regarde. Ce que la courbe ne peut pas porter — une
régularisation à cheval sur plusieurs mois sans en couvrir aucun — est
listé dessous plutôt que rattaché de force à un mois.
Le second situe le lot dans le parc, surface en abscisse. Il corrige une
lecture que les médianes rendent fausse : le loyer au m² décroît
fortement avec la taille, si bien qu'un studio de 21 m² affiche +60 %
au-dessus de la médiane de son immeuble sans rien devoir à sa gestion.
Ce qui se lit n'est donc pas la hauteur d'un point, mais sa position
parmi les lots de surface voisine — et une phrase le dit au-dessus du
graphe. Les autres lots s'ouvrent d'un clic ; les locaux commerciaux
gardent leur place mais changent de forme, leur prix ne suivant pas la
même logique.
Sans surface saisie, chaque case reste vide et renvoie vers Logements :
la fiche montre le trou au lieu de le combler.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La fiche d'un lot cumulait tout son historique en un chiffre. Une
révision de loyer, une vacance ou un décrochage y étaient donc
invisibles. Le bloc `loyer` remet les lignes sur un axe de temps, dont
l'unité est le mois loué et non le mois du compte rendu : un rappel de
mars facturé en avril décrit mars.
Trois formes de lignes cohabitent sous le même `type_ligne = "loyer"`,
et les confondre fausse la courbe :
- le loyer d'un mois, cas courant ;
- le loyer d'un trimestre, forme réelle des baux commerciaux du parc,
réparti sur les mois qu'il couvre — sans quoi deux mois sur trois
paraîtraient vides alors que le local est loué ;
- le prorata d'entrée, de sortie ou l'avoir, rattaché à son mois mais
compté à part. Les additionner ferait passer un mois de changement de
locataire pour un mois à loyer effondré.
Un mois sans ligne reste vide plutôt qu'à zéro : zéro dirait « loué
gratuitement », ce qu'aucun compte rendu ne dit. Un mois facturé
seulement au prorata est signalé comme transition, pour ne pas se
confondre avec une vacance.
Le mètre carré vient de la fiche saisie, qu'aucun PDF ne porte : tant
qu'elle manque, le ratio reste nul et la page renvoie vers la saisie.
Les médianes qui situent le lot rejouent exactement la même répartition
pour les autres lots — les calculer autrement ne voudrait rien dire — et
s'accompagnent toujours de leur effectif et du nombre de lots exclus
faute de surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Passer d'un lot à un autre emprunte la même route : Vue réutilise le
composant sans le remonter, et `onMounted` ne rejoue pas. L'adresse
changeait donc en laissant à l'écran la fiche du lot précédent — l'écart
le plus trompeur qui soit entre l'URL et ce qu'on lit.
Le sélecteur ne montrait rien parce qu'il passe par `lotChoisi`, jamais
par l'URL. Un lien direct vers /lots/<id> depuis une fiche déjà ouverte
suffisait pourtant à produire le décalage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le compte rendu n'accorde pas de colonne au report de solde : il en écrit le
libellé et le montant dans la colonne « Période », et le reporte au même endroit
sur sa ligne « Totaux ». Le tableau d'édition suit cette mise en page, mais un
montant sous un en-tête « Periode » se lit mal, d'autant que le libellé y
répétait ce que la colonne « Type » affiche déjà deux cases plus loin.
L'en-tête devient « Periode / Libelle » — la colonne porte du texte libre, autant
le dire — et la ligne de report n'y garde que son montant. La ligne « Totaux »
conserve le sien, aucune colonne « Type » ne l'y expliquant.
Reste que le parser range ce montant dans le champ `loyers` de la ligne, faute
d'un champ à lui : le tableau compense à l'affichage, la donnée reste à corriger
à la racine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'ordre et les libellés des colonnes étaient retapés à six endroits : l'en-tête
du tableau, deux boucles de cellules, la ligne « Totaux », la table des libellés
et la liste des colonnes recoupées. Ces copies avaient déjà divergé — le solde
antérieur, rendu hors de la boucle, était comparé aux lignes sans que son écart
puisse s'afficher dans sa cellule. `COLONNES_CRG` devient la seule déclaration,
et tout le reste en dérive ; l'écart du solde antérieur s'affiche du coup là où
on le cherche.
`ecartsAvecLignes` rapporte maintenant l'extrait, le calculé et ce qui manque
entre les deux. Le composant refaisait cette soustraction pour son infobulle,
avec un `|| 0` là où l'utilitaire emploie `Number` et `Number.isFinite` : deux
règles de coercition pour un même calcul, libres de diverger.
Le contenu déplié d'un lot passe de `v-show` à `v-if`. Le tableau des lignes
compte une centaine de champs éditables ; les garder montés pour la vingtaine de
lots d'un document faisait re-rendre à chaque frappe deux mille champs que
personne ne regardait.
`setNestedValue` était recopié à l'identique dans trois composants et
`formatCurrency` redéfini dans le composant alors que `utils/format.js` existe
et dit lui-même que les nouveaux affichages passent par lui. Le premier part
dans `utils/chemin.js`, le second cède la place à `formatMontantPrecis`, dont le
formateur `Intl` est construit une fois pour toutes.
`EditableField` affirmait en dur que sa valeur venait du compte rendu. C'est vrai
des trois écrans qui l'emploient aujourd'hui, mais un formulaire de saisie
manuelle mentirait sans le savoir : l'origine devient une prop, avec cette
valeur par défaut.
Le type des lignes de report est défini des deux côtés de l'application sans
lien entre eux ; chacun renvoie désormais à l'autre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La carte d'un lot n'affichait que quatre des huit colonnes du compte rendu, la
colonne Total absente, et le bandeau replié montrait un total figé : corriger un
règlement ne le changeait pas. Pire, les quatre agrégats modifiables ne sont
jamais enregistrés — seules les lignes partent en base (`service.py`) — pendant
que les colonnes réellement écrites, dont Regles et Impayé, n'étaient éditables
nulle part. On corrigeait donc un champ sans effet, et pas celui qu'il fallait.
Le détail des lignes reprend maintenant le tableau du compte rendu, colonne pour
colonne, la ligne « Totaux » comprise en pied. Les dix champs d'une ligne et les
huit de la ligne Totaux sont modifiables, sans exception : cette page sert à
vérifier une extraction avant de l'enregistrer, l'utilisateur y a le dernier mot.
Rien n'est plus recalculé à l'affichage ni reporté d'un champ sur un autre.
Corriger une colonne ne déclenche que ce qui a été demandé — un total réécrit
d'office effacerait sans le dire ce que le compte rendu porte.
Un seul contrôle subsiste, en signalement pur : chaque colonne de la ligne
« Totaux » est confrontée à la somme de cette même colonne sur les lignes. Le
recoupement est celui que fait l'œil sur le tableau. Déduire le total des autres
colonnes laissait passer le cas le plus parlant — une colonne Total qui ne somme
visiblement pas, faute d'avoir extrait la valeur d'une ligne.
Sur les 388 lots des documents extraits, ce contrôle signale quatre lots, tous de
vraies extractions incomplètes : un règlement de 707,29 € qu'aucune ligne ne
porte, un « divers » de 308,76 € sauté à un changement de page, deux lots réglés
sans ligne. La colonne fautive passe en surbrillance et affiche la valeur
calculée sous le montant extrait, sans jamais s'y substituer.
Le solde antérieur quitte la colonne Loyers pour la colonne Période, où le
compte rendu l'imprime. Compté à part sans être affiché à part, il donnait une
colonne Loyers qui semblait ne pas sommer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le pointillé qui signale un champ modifiable n'apparaissait qu'au survol. Il
faut désormais le chercher à la souris pour savoir si un nombre se corrige —
supportable tant que la page n'affiche que de l'extraction, plus du tout dès
qu'une valeur déduite s'affiche à côté. Le pointillé reste donc visible au
repos : son absence devient le signal qu'un nombre ne se modifie pas.
L'infobulle disait « Cliquer pour modifier », ce qui décrit le geste mais tait
l'essentiel : d'où vient le nombre. Elle dit maintenant qu'il est extrait du
compte rendu, et distingue le champ vide — rien n'a été extrait, il reste à
saisir — du champ renseigné.
Le composant ne sert qu'aux trois sections de la page d'édition, toutes
alimentées par l'extraction : l'affirmation vaut partout où il est employé.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les sept onglets alignés dans le header ne disaient pas que trois natures de
travail s'y mêlaient. Ils passent en colonne, sous deux titres : Saisie
alimente la base (Documents, Fiches logements), Analyse en lit le contenu
(Recettes, Dépenses, Par lot). Config quitte la rangée pour un pied de menu,
c'est un réglage et non une destination de travail.
« Logements » et « Lots » se ressemblaient trop pour deux pages qui parlent du
même objet sans faire la même chose. Elles deviennent « Fiches logements » —
ce qu'on y saisit — et « Par lot » — ce qu'on y lit. Les URL ne bougent pas,
elles circulent dans des liens déjà partagés.
Le bouton « Importer un PDF » disparaît du menu : la page Documents porte déjà
le sien en tête de liste et l'accueil garde sa zone de dépôt. Ce troisième
exemplaire n'ajoutait rien.
La barre se replie sur ses seules icônes, le libellé passant en infobulle et un
filet prenant la place des titres de groupe. Le repli est gardé en
`localStorage` : c'est un réglage d'espace de travail, le retrouver déplié à
chaque rechargement serait une corvée.
Les flèches de Recettes et Dépenses sont celles des actions rapides de
l'accueil. Replié, le menu ne montre plus que ces icônes : elles doivent
désigner la même chose d'un écran à l'autre.
Le calcul de l'état actif suit l'entrée de menu dans `NavLien`, qui la rend
dépliée ou non. Une fiche de lot (`/lots/26`) garde ainsi son entrée allumée,
comme avant le déménagement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nouvel onglet « Lots » : identité, chiffres, chronologie et intervenants
d'un lot, choisi dans un sélecteur. Le lot vit dans l'URL (`/lots/26`) pour
qu'une fiche se partage et survive au rechargement.
La chronologie mêle recettes et dépenses dans l'ordre des comptes rendus :
c'est là que se lit la vie du lot, y compris ce que la base ne porte pas
ailleurs — les dates d'entrée et de sortie des locataires sont vides, mais
les honoraires de mise en location et les états des lieux les racontent.
Chaque entreprise du tableau des intervenants se déplie sur ses
interventions. Le détail reprend les lignes déjà chargées plutôt que d'en
redemander : un second calcul pourrait diverger du total affiché.
Le solde n'étant pas un résultat net, la page le dit sous les tuiles, à
l'endroit où le chiffre se lit, et rappelle le montant des charges
d'immeuble laissées hors des lots.
Le taux de recouvrement s'efface quand rien n'est facturé : les 100 %
renvoyés dans ce cas afficheraient un lot sain à côté de 49 000 € d'impayé.
Couleurs reprises des tuiles existantes — `.card` et `.badge` ne fixant
aucune couleur de texte, une valeur sans classe héritait du noir et
disparaissait sur le fond sombre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les pages existantes agrègent le parc ; aucune ne descend à un lot pour
remettre ses lignes bout à bout. `/api/lots/{id}/analyse` renvoie son
identité (fiche saisie comprise, trous laissés visibles), ses totaux, sa
chronologie de recettes et de dépenses, et les entreprises intervenues.
Deux limites sont assumées plutôt que contournées :
- les dépenses d'un lot sont celles que le compte rendu lui impute. Aucune
clé de répartition n'existe en base — ni tantièmes, ni surfaces complètes
— donc les charges d'immeuble ne sont pas ventilées : elles sont exposées
à part, et le solde d'un lot n'est pas un résultat net ;
- les lignes sont rendues telles qu'extraites, sans regroupement ni
dédoublonnage. Un acompte et son solde restent deux lignes.
Les totaux passent par `flux_par` et `restant_du_par` au lieu de resommer
sur place : ces fonctions portent la distinction entre ce qui se cumule et
ce qui est une photo, et la rejouer à la main ferait diverger cette page de
la page Recettes.
Le montant d'un intervenant est net du crédit, comme chaque ligne de la
chronologie, pour que le détail d'une entreprise retrouve son total — un
avoir rend d'ailleurs ce montant négatif, ce que le compte rendu porte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La fiche compte une dizaine de champs pour une vingtaine de lots : un
formulaire par lot imposerait autant d'allers-retours. Le tableau garde
l'ergonomie du tableur d'où viennent ces données — une ligne par lot, les
colonnes dans le même ordre, chaque cellule enregistrée en la quittant.
Ce que le serveur calcule est affiché comme tel et non saisissable :
échéance du DPE (rouge si périmé, ambre à moins d'un an) et écart de
surface. Le type vu par le PDF reste visible à côté du type saisi quand les
deux se contredisent, plutôt que d'être remplacé sans le dire.
Tri et filtre vivent dans l'en-tête de chaque colonne, jamais au-dessus du
tableau : c'est là qu'on les cherche en lisant la colonne. Une liste de
choix peut proposer, dans un groupe à part, ce qui n'est pas une valeur de
la colonne — rattachement d'un lot aux comptes rendus, désaccord de type.
L'en-tête se fige au défilement, ce qui demande de borner la hauteur du
tableau : sans conteneur à hauteur limitée, `sticky` n'a rien à quoi se
tenir.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trois manques que la saisie a fait apparaître :
Le tableau porte une colonne immeuble, la liste ne peut donc plus être
suspendue à un immeuble choisi d'avance : elle renvoie tout le parc, chaque
ligne emportant de quoi nommer son immeuble sans requête de plus.
Un lot qu'aucune ligne de compte rendu ne mentionne ne décrit rien : il
encombre la saisie et doit pouvoir disparaître. La garde est côté serveur —
un lot porteur de revenus ou de dépenses est refusé, ses montants
partiraient avec lui.
Le nom d'usage s'enregistre, et la réponse recalcule les compteurs de
l'immeuble plutôt que de les laisser à zéro : elle remplace l'immeuble dans
les listes du client, qui le croirait vide de lots.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les comptes rendus n'identifient un immeuble que par son code de gestion
(« 33689020 »), illisible partout où il s'affiche. La dénomination
(« Servient ») le remplace à l'écran sans toucher au code, qui reste la clé
venue des PDF.
La colonne s'ajoute à une table déjà installée : elle passe donc par le
rattrapage de schéma, qui accepte désormais l'absence de valeur de
rattrapage. Déduire un nom d'usage du code en inventerait un.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les lots n'étaient connus que par l'extraction PDF : un numéro, un type
souvent vide, et rien sur le bien lui-même. Cette table de caractéristiques
(surface, étage, bâtiment, chauffage, DPE, rapprochement impôts) donne au
référentiel une source de vérité indépendante des comptes rendus.
Table séparée de `lots` à dessein : une ré-extraction ne peut alors pas
écraser la saisie, et le désaccord sur le type de lot reste visible au lieu
d'être arbitré en silence. La fiche gagne, le PDF comble les trous.
Échéance du DPE et écart de surface ne sont pas stockés mais calculés : une
colonne dérivée finirait par mentir après une correction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les raccourcis de l'accueil parlaient de recettes quand la barre de
navigation disait « Revenus », et la page atteinte s'intitulait « Dashboard
Revenus Locatifs » : trois mots pour la meme chose sur un meme parcours.
Les libelles visibles suivent maintenant un seul vocabulaire, accents
compris. Les URL gardent leurs noms d'origine (/revenus, /analytics) :
elles peuvent figurer dans des signets, et un libelle n'a pas besoin de
coincider avec son adresse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'accueil ne decrivait qu'un document : les quatre tuiles lisaient le
dernier compte rendu importe, sans rien dire de ce que contient la base.
Il resume desormais une annee civile, avec un selecteur des annees
presentes. L'annee par defaut est celle du dernier compte rendu et non
l'annee en cours : la fenetre etait calee sur `date.today()`, si bien que
sans import depuis quelques mois l'ecran se serait vide alors que la base
est pleine.
Ce que les tuiles annoncent :
- recettes facturees, report exclu, avec l'encaisse et le recouvrement ;
- depenses, debit et credits recus ;
- net reverse, rapproche des soldes annonces par les comptes rendus ;
- restant du, date, car un stock ne se cumule pas d'un mois sur l'autre.
Le net reverse vaut « encaisse - debit + credit », et cette egalite tombe
au centime sur le solde extrait du PDF pour avril, mai et juin 2026. Elle
s'ecarte de 288,52 EUR en fevrier et de 1 188,03 EUR en mars, les deux mois
dont l'extraction a par ailleurs des defauts. L'ecart est donc affiche et
jamais lisse : c'est le seul controle de bout en bout dont on dispose sur
la qualite d'une extraction. Les regles vivent dans services/tresorerie.py,
a cote de celles des revenus.
Le lot compte est celui qui a ete facture dans l'annee : la table `lots`
retient deux ecritures par lot (« 0001 » et « 01 »), sequelle de la
normalisation des numeros, et en annoncerait 40 la ou il y en a 20.
Le graphique passe du vert et rouge au bleu et ambre : sous deuteranopie,
green-400 et red-400 ne se separent qu'a un delta E de 7,9, sous le seuil
de 8. La paire retenue tient a 30,2.
`/recent-revenus` disparait — c'etait le dernier agregat a compter les
lignes de report — et `/immeubles-shortcuts` est borne a la meme annee que
le reste de l'ecran, deux perimetres sur un ecran donnant deux montants
sans que rien ne les distingue.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les quatre raccourcis de l'accueil renvoyaient mal. « Importer un PDF »
repetait la zone de depot situee juste au-dessus. « Analyses » et
« Immeubles » ne disaient pas ce qu'on y trouve — des recettes et des
depenses. « Documents » ne menait pas a la page mais faisait defiler la
page d'accueil jusqu'a sa propre section, sans quitter l'ecran.
Trois destinations restent, nommees par ce qu'elles contiennent, et
chacune est un lien vers sa page. Le bleu et l'ambre sont ceux du
graphique mensuel : une couleur garde le meme sens d'un bout a l'autre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La section IA n'est pas prete a etre utilisee. Plutot que de supprimer le
code, deux interrupteurs alignes la retirent de l'application :
- frontend/src/features.js : onglet de navigation, page /ia (redirigee vers
l'accueil pour ne pas casser un signet) et section « Configuration LLM »
de la page Config. La page n'est plus embarquee dans le bundle.
- src/plesna_gerance/features.py : les routes /api/ia ne sont plus montees,
les endpoints n'existent donc pas du tout.
Les settings Ollama restent en base : repasser les deux interrupteurs a
true suffit a tout reactiver, sans ressaisie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'accueil et la page Revenus calculaient leurs totaux chacun de leur
côté : le premier lisait le dernier compte rendu, la seconde cumulait
tout. Les deux écrans affichaient donc deux impayés différents pour la
même notion — 49 374 € contre 247 354 €.
Les raccourcis immeubles cumulaient la dette, et les courbes mensuelles
réintégraient le report chaque mois, ce qui rendait deux mois
incomparables. Tous passent par les mêmes règles que la page Revenus.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Chaque compte rendu reporte la dette du précédent dans une ligne
solde_anterieur. Les agrégats sommaient ces lignes comme le reste : la
même créance était recomptée à chaque document, et un remboursement ne
pouvait jamais s'inscrire — une dette soldée restait affichée à vie.
Sur la base réelle, la page Revenus annonçait ainsi 247 354 € d'impayés
pour une dette de 49 374 €, et désignait comme deuxième et troisième
débiteurs deux locataires à jour depuis avril (SURBECK 690,10 € et
GUINAIS 445,81 €, tous deux soldés).
Deux natures cohabitent, et c'est la colonne qui la porte, pas la ligne :
une ligne de report a un `total` déjà compté le mois d'avant, mais ses
`regles` sont un encaissement bien réel de la période. Écarter la ligne
entière ferait disparaître de l'argent reçu (1 298,81 € ici).
- flux (facturé, encaissé) : cumulés sur la période, report exclu ;
- stock (restant dû) : lu dans le dernier compte rendu de chaque immeuble ;
- taux de recouvrement : réglé sur facturé, report exclu des deux côtés,
sans quoi rattraper une vieille dette ferait dépasser 100 %.
Résultat : 85 748 € facturés, 98,2 % de recouvrement, 49 374 € encore
dus. Les règles vivent dans services/revenus_query.py, pour que le
dashboard s'y branche au lieu de les réinventer.
/summary borne désormais tous ses blocs à la période demandée : les KPIs
et by_immeuble ignoraient `months` alors que by_month le respectait.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aucun appelant : le tableau de bord se construit sur financial-summary,
monthly-trends, recent-revenus et immeubles-shortcuts. Les compteurs
qu'il exposait restent disponibles en ligne de commande via
`plesna-gerance db-info`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'API et l'interface partagent le même port, et la route attrape-tout qui
sert le SPA passait avant le 404 : n'importe quelle URL /api/* erronée
repartait en index.html avec un 200. Côté client, une faute de frappe
dans une URL ne ressemblait pas à une erreur mais à une réponse vide, et
le vrai motif n'apparaissait qu'en inspectant le corps de la réponse.
Les URL commençant par /api/ sont désormais exclues de l'attrape-tout et
retombent sur un 404 JSON. Les routes de l'interface restent servies par
index.html.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code sans aucun appelant, retiré : DatabaseService.get_revenus_summary
(annoté list[dict] alors qu'il retournait un dict), get_depenses_summary,
get_tag_by_id et storage.file_exists.
Les tags étaient gérés à deux adresses : /api/tags (lecture, appelée par
trois composants) et /api/config/tags (lecture, création, renommage,
appelée par la seule page de configuration). Tout est regroupé sur
/api/tags, dans le module qui leur est dédié ; config.py ne garde que les
settings et ConfigPage est recâblée. Ces routes n'avaient aucun test :
sept en couvrent maintenant la création, le renommage, l'unicité et les
noms vides.
/api/stats disparaît également : sous-ensemble de /api/dashboard/stats,
il n'était appelé par personne.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les parseurs géométriques (par cellules de tableau) traitent les 5 PDF du
corpus sans jamais déclencher le repli : les parseurs texte étaient 586
lignes de regex fragiles, non testées, à maintenir à chaque évolution du
format de sortie. Les références golden sont inchangées après leur
suppression — l'extraction produit exactement la même chose.
Le parseur géométrique devient donc le seul chemin : ce qu'il ne lit pas
est perdu. L'orchestrateur distingue maintenant les deux cas :
- aucun lot lu -> ExtractionError (422 côté API). Un compte rendu sans
lot n'existe pas : c'est le tableau qui n'a pas été reconnu, et mieux
vaut échouer que d'enregistrer un document vide découvert bien plus
tard, au moment de relire les chiffres ;
- aucune opération -> accepté. Un mois sans dépense reste plausible.
_extract_lot_code_from_description était la seule fonction encore
utilisée : elle rejoint utils.lots sous le nom
extract_lot_numero_from_description, à côté de normalize_lot_numero.
extract_text_from_pdf, sans appelant, disparaît au passage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les agrégats par immeuble joignaient revenus et locataires à partir du
lot. Comme un lot accumule ses occupants successifs, chaque ligne de
revenu était comptée autant de fois qu'il y a eu de locataires : aucune
erreur levée, juste des montants faux qui dérivent avec l'ancienneté du
parc.
Sur la base réelle (40 lots, 46 locataires), /api/revenus/immeubles et
le bloc by_immeuble de /api/revenus/summary annonçaient :
total 348 111,95 € au lieu de 332 899,45 € (+4,6 %)
réglés 100 697,66 € au lieu de 85 545,16 € (+17,7 %)
taux 28,9 % au lieu de 25,7 %
Les montants sont désormais agrégés par immeuble AVANT la jointure des
locataires, qui ne sert plus qu'au dénombrement. Les deux endpoints
partagent ces sous-requêtes et la construction de la réponse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les parseurs sont la partie la plus exposée aux régressions silencieuses :
un décalage de colonne ne lève aucune exception, il produit des montants
faux. Rien ne les couvrait jusqu'ici.
Chaque PDF du corpus local produit une empreinte (structure des lots,
totaux par lot, montants par catégorie, détail de chaque opération)
comparée à une référence figée. Les libellés sensibles — locataire,
fournisseur, description — sont réduits à un hash court : un changement
reste détecté sans recopier la donnée.
Ni les PDF (`data/`) ni les références (`tests/golden/`) ne sont
versionnés : la suite se saute d'elle-même là où le corpus est absent.
Régénération après un changement volontaire de parseur :
uv run pytest tests/test_parsers_golden.py --regen-golden
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La page et le panneau de differences avaient ete ecrits avant que main ne pose
un design system unique : classes Tailwind brutes, et surtout un detail
depliable reste sur fond clair alors que son contenu venait de passer en sombre.
Reprend les classes du systeme (page, card, table, btn, badge, subcard,
empty-state, spinner) et ramene le detail au fond sombre. Les avertissements de
reecriture passent en amber-400, illisibles jusqu'ici sur fond sombre.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La liste des documents montrait la date d'import, figee au premier
enregistrement : c'est la date de la derniere extraction ayant produit les
donnees affichees qui renseigne, surtout apres un balayage de re-extraction. La
colonne `extracted_at` la porte, mise a jour a chaque enregistrement, et la date
d'import reste consultable en infobulle.
Le projet n'ayant pas d'outil de migration (`create_all` laisse intactes les
tables existantes), `init_db` rattrape les colonnes ajoutees apres coup : sans
cela une base deja installee cesserait de fonctionner. Les documents deja
presents recoivent leur date d'import, qui est bien celle de leur extraction.
Une fois la selection appliquee sans echec, le balayage renvoie vers la liste des
documents ; en cas d'echec on reste sur place, les lignes concernees portant leur
message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refuser une modification, c'est reenvoyer la valeur actuelle du document. Mais
l'enregistrement ramene les donnees a une forme canonique (aujourd'hui les
numeros de lot) : sur ces champs, le refus n'est pas conserve. L'interface le
laissait croire.
Plutot que de traiter le cas des lots en dur, le serveur devient seul juge de ce
qu'il stocke et l'annonce : `canonicalize_extraction` regroupe les reecritures
faites a la persistance, `save_document` et l'apercu passent par elle, et
/api/documents/{id}/re-extract renvoie `previous_canonical` a cote de
`previous_data`. Le front compare ces deux jeux et signale, sous chaque case
decochee, la valeur que l'enregistrement reecrira — sans connaitre aucune regle.
Une transformation ajoutee plus tard remontera donc toute seule dans l'interface.
Toutes les differences restent visibles et toutes les cases actives : voir ce
qui change et pouvoir se prononcer prime, quitte a etre prevenu que ce champ-la
reviendra reecrit.
Supprime au passage frontend/src/utils/lots.js, qui dupliquait la regle de
normalisation cote client.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le balayage n'imposait que le tout ou rien par document. Chaque modification a
desormais sa case : un champ de metadonnee, le nom ou le type d'un locataire,
une operation ajoutee ou supprimee. Les champs textuels retenus sont en outre
modifiables a la main, pour les cas ou ni l'ancienne ni la nouvelle valeur ne
convient. Tout reste coche par defaut : le cas courant tient toujours en un clic.
Les montants et lignes d'un locataire forment un seul bloc : les retenir
separement produirait un total ne correspondant plus a ses lignes, alors que ces
chiffres alimentent les revenus et les depenses.
Le document enregistre est construit par fusion (mergeExtraction) : on part des
donnees en base et on n'y applique que ce qui est retenu. Le diff porte donc
maintenant de quoi rejouer chaque changement (identifiant, chemin, valeurs
brutes) et l'index d'origine des operations supprimees, pour les remettre a leur
place si on refuse leur disparition. Les tags suivent la liste d'operations
finale, y compris ceux d'une operation restauree.
Une reecriture de forme d'un numero de lot ("0001" -> "01") est signalee comme
imposee et sa case verrouillee : le backend normalise tout enregistrement, une
case sans effet aurait laisse croire l'inverse.
Ajoute vitest : la fusion decide de ce qui est ecrit en base, elle est couverte
par 15 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nouvelle page /re-extraction, accessible depuis la liste des documents : elle
rejoue l'extraction de chaque PDF stocke, document par document, avec une
progression et une estimation du temps restant (une extraction prend plusieurs
secondes) et un bouton d'arret. Rien n'est ecrit en base pendant le balayage.
Les documents dont l'extraction change sont listes avec le nombre de
differences par section, deja coches ; les inchanges sont replies et les
documents sans PDF ou en erreur isoles. Le detail depliable reutilise
l'affichage de diff de la re-extraction unitaire (extrait dans
ExtractionDiffDetails). Un seul bouton enregistre toute la selection.
Les tags de depenses sont reportes par signature d'operation et non par index :
une operation ajoutee ou retiree par le nouveau parser ne decale plus les
categories. Ce qui n'est pas reapparie recoit une prediction, modifiable dans le
detail avant enregistrement.
L'edition d'un document et l'application du balayage passent par
PUT /api/documents/{id}, qui vise le document par son ID.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le remplacement par ID mettait a jour le document en le supprimant puis en le
recreant : la ligne changeait d'ID, cassant les liens qui la referencent (URL
d'edition, telechargement du PDF) juste apres un balayage de re-extraction. Le
document est desormais mis a jour sur place, seules les donnees derivees
(revenus, depenses) etant regenerees.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
POST /api/documents/{id}/re-extract retourne desormais, en plus des donnees
re-extraites, les donnees actuelles et les tags des depenses : un seul appel
suffit pour comparer avant/apres et reporter les tags. L'endpoint devient
synchrone pour que FastAPI l'execute dans un thread, l'extraction bloquant
plusieurs secondes par PDF.
PUT /api/documents/{id} enregistre la nouvelle extraction en visant le
document par son ID (save_document(replace_document_id=...)) : une extraction
qui corrige la reference ou la date met a jour le bon document au lieu d'en
creer un second, et refuse la collision avec un document voisin. Le PDF
stocke est conserve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>