Associer les relations aux preuves #56

Closed
opened 2026-07-20 22:21:17 +02:00 by fy59 · 0 comments
Owner

Associer les relations aux preuves

Contexte

Une relation entre deux entités ne doit pas être considérée comme fiable sans
pouvoir indiquer les éléments qui la justifient.

Exemples :

  • une capture de conversation montre qu'une personne utilise une adresse
    e-mail ;
  • un relevé bancaire justifie qu'un compte a reçu un virement ;
  • une réponse DNS justifie qu'un domaine pointe vers une adresse IP ;
  • un document ou une photographie permet de relier une identité à un compte.

Les modèles et DAO suivants existent désormais :

  • EvidenceRecord et EvidenceDao ;
  • EntityRecord et EntityDao ;
  • RelationRecord et RelationDao ;
  • EvidenceEntityDao pour les associations entre preuves et entités.

La table SQLite relation_preuves existe déjà dans le schéma V1. Elle
contient une clé primaire composée de relation_id et preuve_id, ainsi que
des clés étrangères vers relations et preuves.

Objectif

Ajouter un DAO spécialisé permettant de créer, consulter et supprimer les
associations entre les relations et les preuves qui les justifient.

Le composant doit rester indépendant de GTK et du futur graphe interactif.

Nom du composant

Créer :

include/dao/relation_evidence_dao.h
src/dao/relation_evidence_dao.c
tests/test_relation_evidence_dao.c

Le type public doit être nommé :

typedef struct RelationEvidenceDao RelationEvidenceDao;

Le DAO emprunte une connexion Database existante. Il ne doit ni ouvrir une
nouvelle connexion ni fermer la connexion reçue.

Choix d'architecture

RelationEvidenceDao représente uniquement l'association entre deux UUID.

Il ne doit pas reconstruire directement des RelationRecord ou des
EvidenceRecord, afin de ne pas dupliquer la logique de lecture déjà présente
dans RelationDao et EvidenceDao.

Les méthodes de liste doivent donc retourner des tableaux d'UUID :

  • relation vers preuves ;
  • preuve vers relations.

La couche applicative pourra ensuite charger les modèles complets à l'aide des
DAO spécialisés.

API publique

Domaine d'erreur

Créer :

typedef enum
{
    RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT,
    RELATION_EVIDENCE_DAO_ERROR_MEMORY,
    RELATION_EVIDENCE_DAO_ERROR_PREPARE,
    RELATION_EVIDENCE_DAO_ERROR_BIND,
    RELATION_EVIDENCE_DAO_ERROR_EXECUTE,
    RELATION_EVIDENCE_DAO_ERROR_CONSTRAINT,
    RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND,
    RELATION_EVIDENCE_DAO_ERROR_READ,
    RELATION_EVIDENCE_DAO_ERROR_SCHEMA
} RelationEvidenceDaoError;

Ajouter :

#define RELATION_EVIDENCE_DAO_ERROR \
    relation_evidence_dao_error_quark()

GQuark relation_evidence_dao_error_quark(void);

RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND doit pouvoir signaler :

  • une relation inexistante lors de la création d'une association ;
  • une preuve inexistante lors de la création d'une association ;
  • une association inexistante lors de sa suppression.

Construction

RelationEvidenceDao *relation_evidence_dao_new(
    Database *database,
    GError **error
);

La fonction doit :

  • refuser une connexion NULL ;
  • emprunter la connexion ;
  • retourner une erreur lisible en cas d'échec ;
  • accepter un pointeur GError ** facultatif.

Destruction

void relation_evidence_dao_free(
    RelationEvidenceDao *relation_evidence_dao
);

La fonction doit :

  • accepter NULL ;
  • ne jamais fermer la connexion Database empruntée.

Création d'une association

gboolean relation_evidence_dao_link(
    RelationEvidenceDao *relation_evidence_dao,
    const char *relation_identifier,
    const char *evidence_identifier,
    GError **error
);

La fonction doit :

  • valider les deux UUID ;
  • vérifier que la relation existe ;
  • vérifier que la preuve existe ;
  • refuser une association déjà présente ;
  • utiliser une requête préparée ;
  • ne jamais utiliser INSERT OR REPLACE, REPLACE ou un upsert ;
  • ne démarrer aucune transaction cachée ;
  • fonctionner à l'intérieur d'une transaction externe.

Une relation ou une preuve inexistante doit produire
RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND.

Un doublon doit produire RELATION_EVIDENCE_DAO_ERROR_CONSTRAINT.

Suppression d'une association

gboolean relation_evidence_dao_unlink(
    RelationEvidenceDao *relation_evidence_dao,
    const char *relation_identifier,
    const char *evidence_identifier,
    GError **error
);

La fonction doit :

  • valider les UUID ;
  • supprimer uniquement la ligne de relation_preuves ;
  • ne jamais supprimer la relation ;
  • ne jamais supprimer la preuve ;
  • retourner RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND lorsque l'association
    n'existe pas ;
  • utiliser une requête préparée.

Vérification d'existence

gboolean relation_evidence_dao_exists(
    RelationEvidenceDao *relation_evidence_dao,
    const char *relation_identifier,
    const char *evidence_identifier,
    gboolean *out_exists,
    GError **error
);

La fonction doit :

  • valider le DAO, les UUID et out_exists ;
  • retourner TRUE lorsque la requête a été exécutée correctement ;
  • placer TRUE ou FALSE dans out_exists ;
  • ne pas produire d'erreur lorsque l'association est simplement absente.

Liste des preuves d'une relation

GPtrArray *relation_evidence_dao_list_evidence_identifiers(
    RelationEvidenceDao *relation_evidence_dao,
    const char *relation_identifier,
    GError **error
);

Le tableau doit :

  • contenir des chaînes UUID ;
  • utiliser g_free() comme fonction de destruction ;
  • appartenir à l'appelant ;
  • être trié par UUID croissant ;
  • être vide sans erreur lorsqu'aucune preuve n'est associée.

La méthode ne doit pas charger de EvidenceRecord.

Liste des relations d'une preuve

GPtrArray *relation_evidence_dao_list_relation_identifiers(
    RelationEvidenceDao *relation_evidence_dao,
    const char *evidence_identifier,
    GError **error
);

Le tableau doit :

  • contenir des chaînes UUID ;
  • utiliser g_free() comme fonction de destruction ;
  • appartenir à l'appelant ;
  • être trié par UUID croissant ;
  • être vide sans erreur lorsqu'aucune relation n'est associée.

La méthode ne doit pas charger de RelationRecord.

Requêtes SQL attendues

Le DAO doit utiliser des requêtes préparées pour :

  • vérifier l'existence d'une relation ;
  • vérifier l'existence d'une preuve ;
  • vérifier l'existence d'une association ;
  • insérer une association ;
  • supprimer une association ;
  • lister les preuves d'une relation ;
  • lister les relations d'une preuve.

Toutes les requêtes doivent utiliser des paramètres liés. Aucun UUID ne doit
être concaténé dans une chaîne SQL.

Tous les DatabaseStatement doivent être finalisés sur chaque chemin de
sortie, y compris après :

  • une erreur de préparation ;
  • une erreur de liaison ;
  • une erreur d'exécution ;
  • une ligne invalide ;
  • une allocation échouée.

Propriété mémoire

Le DAO :

  • possède uniquement sa structure privée ;
  • emprunte Database ;
  • ne possède aucun RelationRecord ;
  • ne possède aucun EvidenceRecord.

Les tableaux retournés :

  • appartiennent à l'appelant ;
  • possèdent les chaînes UUID qu'ils contiennent ;
  • utilisent g_free() pour détruire ces chaînes.

Tests

Créer :

tests/test_relation_evidence_dao.c

Les tests doivent utiliser une base SQLite temporaire initialisée avec le
schéma réel de l'application.

Ils doivent utiliser les DAO existants pour insérer les entités, relations et
preuves nécessaires, sauf lorsqu'une insertion SQL directe est précisément
destinée à tester une contrainte du schéma.

Scénarios minimaux

  1. refus d'une connexion NULL ;
  2. création et destruction d'un DAO valide ;
  3. relation_evidence_dao_free(NULL) ;
  4. création d'une association valide ;
  5. confirmation de l'association avec exists() ;
  6. résultat FALSE de exists() lorsque l'association est absente ;
  7. refus d'une association dupliquée ;
  8. refus d'une relation inexistante ;
  9. refus d'une preuve inexistante ;
  10. suppression d'une association existante ;
  11. refus de la suppression d'une association absente ;
  12. liste vide des preuves d'une relation ;
  13. liste de plusieurs preuves triées par UUID ;
  14. liste vide des relations d'une preuve ;
  15. liste de plusieurs relations triées par UUID ;
  16. possibilité d'associer une preuve à plusieurs relations ;
  17. possibilité d'associer une relation à plusieurs preuves ;
  18. refus d'un UUID de relation invalide ;
  19. refus d'un UUID de preuve invalide ;
  20. refus d'un pointeur out_exists absent ;
  21. refus d'un DAO absent pour chaque opération publique ;
  22. signalement d'une table relation_preuves absente ;
  23. vérification de la clé primaire composée ;
  24. vérification des contraintes de clés étrangères ;
  25. vérification de ON DELETE CASCADE lors de la suppression d'une relation
  26. vérification de ON DELETE RESTRICT pour une preuve associée ;
  27. absence de suppression de la relation après unlink() ;
  28. absence de suppression de la preuve après unlink() ;
  29. GError ** facultatif sur les erreurs prévues ;
  30. tous les anciens tests restent valides.

Les avertissements SQLite déclenchés volontairement par les tests de
contraintes doivent être déclarés avec :

g_test_expect_message();
g_test_assert_expected_messages();

La sortie normale du test doit rester propre.

Makefile

Ajouter :

RELATION_EVIDENCE_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic
TEST_RELATION_EVIDENCE_DAO := tests/test_relation_evidence_dao

Ajouter une cible dédiée compilant uniquement :

  • tests/test_relation_evidence_dao.c ;
  • src/dao/relation_evidence_dao.c ;
  • les DAO nécessaires à la préparation des données ;
  • les modèles nécessaires ;
  • les modules database nécessaires.

La cible ne doit inclure aucun autre fichier tests/test_*.c, afin d'éviter
plusieurs définitions de main().

Ajouter $(TEST_RELATION_EVIDENCE_DAO) :

  • aux dépendances de make test ;
  • aux exécutables lancés par make test ;
  • aux fichiers supprimés par make clean.

La compilation doit conserver :

-std=c17 -Wall -Wextra -Wpedantic -Werror

Hors périmètre

Ce ticket ne couvre pas :

  • l'interface GTK ;
  • la sélection graphique des preuves ;
  • la création ou la modification d'une relation ;
  • l'import des preuves ;
  • la création d'entités ;
  • la chronologie ;
  • les hypothèses ;
  • les tags ;
  • le journal d'audit ;
  • le futur graphe interactif ;
  • l'affichage des preuves autour d'une relation ;
  • une migration SQLite ;
  • l'ajout d'un rôle ou d'un type à l'association.

Une évolution future du schéma pourra ajouter une qualification de la preuve,
par exemple :

  • soutient la relation ;
  • contredit la relation ;
  • confirme la relation.

Cette évolution ne doit pas être anticipée dans ce ticket puisque la table
actuelle ne contient que relation_id et preuve_id.

Critères d'acceptation

  • RelationEvidenceDao est opaque.
  • Le DAO emprunte la connexion Database.
  • Une relation peut être associée à une preuve existante.
  • Une relation peut posséder plusieurs preuves.
  • Une preuve peut justifier plusieurs relations.
  • Les doublons sont refusés explicitement.
  • Les UUID invalides sont refusés avant l'accès SQL.
  • Les références inexistantes produisent une erreur NOT_FOUND.
  • Une association peut être supprimée sans supprimer ses objets.
  • exists() différencie correctement absence et erreur.
  • Les deux listes sont triées et possèdent leurs chaînes.
  • Les clés étrangères et ON DELETE RESTRICT sont testés.
  • Tous les statements sont finalisés sur chaque chemin.
  • Aucun modèle métier existant n'est dupliqué dans le DAO.
  • Aucun changement GTK n'est introduit.
  • Aucune migration SQLite n'est ajoutée.
  • Les tests ciblés passent.
  • make clean && make && make test réussit.
  • git diff --check ne retourne aucune erreur.
# Associer les relations aux preuves ## Contexte Une relation entre deux entités ne doit pas être considérée comme fiable sans pouvoir indiquer les éléments qui la justifient. Exemples : - une capture de conversation montre qu'une personne utilise une adresse e-mail ; - un relevé bancaire justifie qu'un compte a reçu un virement ; - une réponse DNS justifie qu'un domaine pointe vers une adresse IP ; - un document ou une photographie permet de relier une identité à un compte. Les modèles et DAO suivants existent désormais : - `EvidenceRecord` et `EvidenceDao` ; - `EntityRecord` et `EntityDao` ; - `RelationRecord` et `RelationDao` ; - `EvidenceEntityDao` pour les associations entre preuves et entités. La table SQLite `relation_preuves` existe déjà dans le schéma V1. Elle contient une clé primaire composée de `relation_id` et `preuve_id`, ainsi que des clés étrangères vers `relations` et `preuves`. ## Objectif Ajouter un DAO spécialisé permettant de créer, consulter et supprimer les associations entre les relations et les preuves qui les justifient. Le composant doit rester indépendant de GTK et du futur graphe interactif. ## Nom du composant Créer : ```text include/dao/relation_evidence_dao.h src/dao/relation_evidence_dao.c tests/test_relation_evidence_dao.c ``` Le type public doit être nommé : ```c typedef struct RelationEvidenceDao RelationEvidenceDao; ``` Le DAO emprunte une connexion `Database` existante. Il ne doit ni ouvrir une nouvelle connexion ni fermer la connexion reçue. ## Choix d'architecture `RelationEvidenceDao` représente uniquement l'association entre deux UUID. Il ne doit pas reconstruire directement des `RelationRecord` ou des `EvidenceRecord`, afin de ne pas dupliquer la logique de lecture déjà présente dans `RelationDao` et `EvidenceDao`. Les méthodes de liste doivent donc retourner des tableaux d'UUID : - relation vers preuves ; - preuve vers relations. La couche applicative pourra ensuite charger les modèles complets à l'aide des DAO spécialisés. ## API publique ### Domaine d'erreur Créer : ```c typedef enum { RELATION_EVIDENCE_DAO_ERROR_INVALID_ARGUMENT, RELATION_EVIDENCE_DAO_ERROR_MEMORY, RELATION_EVIDENCE_DAO_ERROR_PREPARE, RELATION_EVIDENCE_DAO_ERROR_BIND, RELATION_EVIDENCE_DAO_ERROR_EXECUTE, RELATION_EVIDENCE_DAO_ERROR_CONSTRAINT, RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND, RELATION_EVIDENCE_DAO_ERROR_READ, RELATION_EVIDENCE_DAO_ERROR_SCHEMA } RelationEvidenceDaoError; ``` Ajouter : ```c #define RELATION_EVIDENCE_DAO_ERROR \ relation_evidence_dao_error_quark() GQuark relation_evidence_dao_error_quark(void); ``` `RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND` doit pouvoir signaler : - une relation inexistante lors de la création d'une association ; - une preuve inexistante lors de la création d'une association ; - une association inexistante lors de sa suppression. ### Construction ```c RelationEvidenceDao *relation_evidence_dao_new( Database *database, GError **error ); ``` La fonction doit : - refuser une connexion `NULL` ; - emprunter la connexion ; - retourner une erreur lisible en cas d'échec ; - accepter un pointeur `GError **` facultatif. ### Destruction ```c void relation_evidence_dao_free( RelationEvidenceDao *relation_evidence_dao ); ``` La fonction doit : - accepter `NULL` ; - ne jamais fermer la connexion `Database` empruntée. ### Création d'une association ```c gboolean relation_evidence_dao_link( RelationEvidenceDao *relation_evidence_dao, const char *relation_identifier, const char *evidence_identifier, GError **error ); ``` La fonction doit : - valider les deux UUID ; - vérifier que la relation existe ; - vérifier que la preuve existe ; - refuser une association déjà présente ; - utiliser une requête préparée ; - ne jamais utiliser `INSERT OR REPLACE`, `REPLACE` ou un upsert ; - ne démarrer aucune transaction cachée ; - fonctionner à l'intérieur d'une transaction externe. Une relation ou une preuve inexistante doit produire `RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND`. Un doublon doit produire `RELATION_EVIDENCE_DAO_ERROR_CONSTRAINT`. ### Suppression d'une association ```c gboolean relation_evidence_dao_unlink( RelationEvidenceDao *relation_evidence_dao, const char *relation_identifier, const char *evidence_identifier, GError **error ); ``` La fonction doit : - valider les UUID ; - supprimer uniquement la ligne de `relation_preuves` ; - ne jamais supprimer la relation ; - ne jamais supprimer la preuve ; - retourner `RELATION_EVIDENCE_DAO_ERROR_NOT_FOUND` lorsque l'association n'existe pas ; - utiliser une requête préparée. ### Vérification d'existence ```c gboolean relation_evidence_dao_exists( RelationEvidenceDao *relation_evidence_dao, const char *relation_identifier, const char *evidence_identifier, gboolean *out_exists, GError **error ); ``` La fonction doit : - valider le DAO, les UUID et `out_exists` ; - retourner `TRUE` lorsque la requête a été exécutée correctement ; - placer `TRUE` ou `FALSE` dans `out_exists` ; - ne pas produire d'erreur lorsque l'association est simplement absente. ### Liste des preuves d'une relation ```c GPtrArray *relation_evidence_dao_list_evidence_identifiers( RelationEvidenceDao *relation_evidence_dao, const char *relation_identifier, GError **error ); ``` Le tableau doit : - contenir des chaînes UUID ; - utiliser `g_free()` comme fonction de destruction ; - appartenir à l'appelant ; - être trié par UUID croissant ; - être vide sans erreur lorsqu'aucune preuve n'est associée. La méthode ne doit pas charger de `EvidenceRecord`. ### Liste des relations d'une preuve ```c GPtrArray *relation_evidence_dao_list_relation_identifiers( RelationEvidenceDao *relation_evidence_dao, const char *evidence_identifier, GError **error ); ``` Le tableau doit : - contenir des chaînes UUID ; - utiliser `g_free()` comme fonction de destruction ; - appartenir à l'appelant ; - être trié par UUID croissant ; - être vide sans erreur lorsqu'aucune relation n'est associée. La méthode ne doit pas charger de `RelationRecord`. ## Requêtes SQL attendues Le DAO doit utiliser des requêtes préparées pour : - vérifier l'existence d'une relation ; - vérifier l'existence d'une preuve ; - vérifier l'existence d'une association ; - insérer une association ; - supprimer une association ; - lister les preuves d'une relation ; - lister les relations d'une preuve. Toutes les requêtes doivent utiliser des paramètres liés. Aucun UUID ne doit être concaténé dans une chaîne SQL. Tous les `DatabaseStatement` doivent être finalisés sur chaque chemin de sortie, y compris après : - une erreur de préparation ; - une erreur de liaison ; - une erreur d'exécution ; - une ligne invalide ; - une allocation échouée. ## Propriété mémoire Le DAO : - possède uniquement sa structure privée ; - emprunte `Database` ; - ne possède aucun `RelationRecord` ; - ne possède aucun `EvidenceRecord`. Les tableaux retournés : - appartiennent à l'appelant ; - possèdent les chaînes UUID qu'ils contiennent ; - utilisent `g_free()` pour détruire ces chaînes. ## Tests Créer : ```text tests/test_relation_evidence_dao.c ``` Les tests doivent utiliser une base SQLite temporaire initialisée avec le schéma réel de l'application. Ils doivent utiliser les DAO existants pour insérer les entités, relations et preuves nécessaires, sauf lorsqu'une insertion SQL directe est précisément destinée à tester une contrainte du schéma. ### Scénarios minimaux 1. refus d'une connexion `NULL` ; 2. création et destruction d'un DAO valide ; 3. `relation_evidence_dao_free(NULL)` ; 4. création d'une association valide ; 5. confirmation de l'association avec `exists()` ; 6. résultat `FALSE` de `exists()` lorsque l'association est absente ; 7. refus d'une association dupliquée ; 8. refus d'une relation inexistante ; 9. refus d'une preuve inexistante ; 10. suppression d'une association existante ; 11. refus de la suppression d'une association absente ; 12. liste vide des preuves d'une relation ; 13. liste de plusieurs preuves triées par UUID ; 14. liste vide des relations d'une preuve ; 15. liste de plusieurs relations triées par UUID ; 16. possibilité d'associer une preuve à plusieurs relations ; 17. possibilité d'associer une relation à plusieurs preuves ; 18. refus d'un UUID de relation invalide ; 19. refus d'un UUID de preuve invalide ; 20. refus d'un pointeur `out_exists` absent ; 21. refus d'un DAO absent pour chaque opération publique ; 22. signalement d'une table `relation_preuves` absente ; 23. vérification de la clé primaire composée ; 24. vérification des contraintes de clés étrangères ; 25. vérification de ON DELETE CASCADE lors de la suppression d'une relation 26. vérification de `ON DELETE RESTRICT` pour une preuve associée ; 27. absence de suppression de la relation après `unlink()` ; 28. absence de suppression de la preuve après `unlink()` ; 29. `GError **` facultatif sur les erreurs prévues ; 30. tous les anciens tests restent valides. Les avertissements SQLite déclenchés volontairement par les tests de contraintes doivent être déclarés avec : ```c g_test_expect_message(); g_test_assert_expected_messages(); ``` La sortie normale du test doit rester propre. ## Makefile Ajouter : ```make RELATION_EVIDENCE_DAO_TEST_CFLAGS := $(TEST_CFLAGS) -Wpedantic TEST_RELATION_EVIDENCE_DAO := tests/test_relation_evidence_dao ``` Ajouter une cible dédiée compilant uniquement : - `tests/test_relation_evidence_dao.c` ; - `src/dao/relation_evidence_dao.c` ; - les DAO nécessaires à la préparation des données ; - les modèles nécessaires ; - les modules `database` nécessaires. La cible ne doit inclure aucun autre fichier `tests/test_*.c`, afin d'éviter plusieurs définitions de `main()`. Ajouter `$(TEST_RELATION_EVIDENCE_DAO)` : - aux dépendances de `make test` ; - aux exécutables lancés par `make test` ; - aux fichiers supprimés par `make clean`. La compilation doit conserver : ```text -std=c17 -Wall -Wextra -Wpedantic -Werror ``` ## Hors périmètre Ce ticket ne couvre pas : - l'interface GTK ; - la sélection graphique des preuves ; - la création ou la modification d'une relation ; - l'import des preuves ; - la création d'entités ; - la chronologie ; - les hypothèses ; - les tags ; - le journal d'audit ; - le futur graphe interactif ; - l'affichage des preuves autour d'une relation ; - une migration SQLite ; - l'ajout d'un rôle ou d'un type à l'association. Une évolution future du schéma pourra ajouter une qualification de la preuve, par exemple : - soutient la relation ; - contredit la relation ; - confirme la relation. Cette évolution ne doit pas être anticipée dans ce ticket puisque la table actuelle ne contient que `relation_id` et `preuve_id`. ## Critères d'acceptation - [x] `RelationEvidenceDao` est opaque. - [x] Le DAO emprunte la connexion `Database`. - [x] Une relation peut être associée à une preuve existante. - [x] Une relation peut posséder plusieurs preuves. - [x] Une preuve peut justifier plusieurs relations. - [x] Les doublons sont refusés explicitement. - [x] Les UUID invalides sont refusés avant l'accès SQL. - [x] Les références inexistantes produisent une erreur `NOT_FOUND`. - [x] Une association peut être supprimée sans supprimer ses objets. - [x] `exists()` différencie correctement absence et erreur. - [x] Les deux listes sont triées et possèdent leurs chaînes. - [x] Les clés étrangères et `ON DELETE RESTRICT` sont testés. - [x] Tous les statements sont finalisés sur chaque chemin. - [x] Aucun modèle métier existant n'est dupliqué dans le DAO. - [x] Aucun changement GTK n'est introduit. - [x] Aucune migration SQLite n'est ajoutée. - [x] Les tests ciblés passent. - [x] `make clean && make && make test` réussit. - [x] `git diff --check` ne retourne aucune erreur.
fy59 closed this issue 2026-07-21 07:32:03 +02:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: fy59/labfy-investigation#56
No description provided.