Une interface web avec SQLPage

Streamlit (le chapitre suivant), c'est bien, mais ça demande d'écrire du Python. SQLPage, c'est le même objectif — une interface web pour saisir et consulter des données — mais en SQL pur (des mangemorts de la base de données). Pas de Python, pas de JavaScript, pas de HTML : vous écrivez des requêtes SQL dans des fichiers texte, SQLPage les exécute et génère les pages web automatiquement. C'est un peu magique, et c'est particulièrement adapté aux gens qui maîtrisent déjà le SQL et qui ne veulent pas apprendre un langage de plus. :mage:

L'autre avantage : SQLPage est un fichier binaire à télécharger. Pas de gestionnaire de paquets, pas d'environnement virtuel, pas de dépendances à gérer. On télécharge, on configure, on lance. :rocket:

C'est quoi SQLPage exactement ?

SQLPage est un serveur web qui lit des fichiers .sql dans un dossier, exécute les requêtes contre votre base de données, et transforme les résultats en pages HTML grâce à des composants prédéfinis (tableaux, formulaires, cartes, graphiques...). La logique de la page, c'est du SQL. La mise en page, SQLPage s'en occupe.

Concrètement, si vous créez un fichier index.sql avec dedans :

SELECT 'table' AS component, 'Mon mobilier' AS title;
SELECT identifiant, commentaires FROM T_mobilier;

SQLPage génère une page web avec un tableau bien mis en forme. C'est tout. :smile_cat:

Installation

Télécharger le binaire

Rendez-vous sur https://github.com/sqlpage/sqlpage/releases et téléchargez la version correspondant à votre système :

  • Windows : sqlpage-windows.zip → extrayez sqlpage.exe
  • Linux : sqlpage-linux.tgz → extrayez sqlpage
  • macOS : sqlpage-macos.tgz → extrayez sqlpage

Placez le binaire dans votre dossier de projet. Sur Linux/macOS, pensez à le rendre exécutable :

chmod +x sqlpage

Ça c'est pour se la péter, sinon vous pouvez aussi faire un clic droit et cocher le truc qui indique que c'est un exécutable...

Structure du projet

Créez un dossier de travail avec cette structure :

prospections/
├── sqlpage.json          ← configuration
├── sqlpage/
│   └── migrations/       ← optionnel, pour créer la base à la volée
├── index.sql             ← page d'accueil
├── mobilier.sql          ← page mobilier
└── ...

Le sous-dossier sqlpage/ est réservé à SQLPage (ses propres fichiers internes). Vos pages SQL vont directement à la racine du dossier.

Configuration

Créez le fichier sqlpage.json à la racine :

{
    "database_url": "sqlite://./prospections_foret.db"
}

Pour un GeoPackage :

{
    "database_url": "sqlite://./prospections_foret.gpkg"
}

:warning: Comme pour Streamlit, le GeoPackage contient des triggers spatiaux. SQLPage utilise SQLite en lecture/écriture mais ne charge pas SpatiaLite automatiquement. Si vous faites des INSERT dans des tables avec géométrie, vous risquez l'erreur no such function: ST_IsEmpty. Pour l'instant, on va saisir les données attributaires depuis SQLPage et laisser QGIS gérer les géométries. C'est une bonne séparation des responsabilités. :relieved:

Lancer SQLPage

Depuis un terminal, dans votre dossier de projet :

# Linux / macOS
./sqlpage

# Windows
sqlpage.exe

SQLPage démarre et vous indique l'adresse — en général http://localhost:8080. Ouvrez votre navigateur sur cette adresse. SQLPage cherche automatiquement un fichier index.sql et l'affiche comme page d'accueil. :tada:

Les composants

L'interface de SQLPage est construite à partir de composants. Chaque composant est activé par un SELECT spécial, puis alimenté par les SELECT suivants. Le principe est toujours le même :

-- 1. On déclare le composant
SELECT 'nom_du_composant' AS component, /* options du composant */;

-- 2. On lui envoie les données
SELECT colonne1, colonne2 FROM ma_table;

Voici les composants les plus utiles pour notre usage.

table — afficher un tableau

SELECT 'table' AS component,
    'Mobilier enregistré' AS title,
    TRUE AS search,       -- ajoute une barre de recherche
    TRUE AS sort;         -- ajoute le tri par colonne au clic

SELECT
    m.identifiant,
    z.nom       AS zone,
    n.nature_mob AS nature,
    c.contexte  AS contexte,
    a.auteurice AS auteurice,
    m.date_creation,
    m.commentaires
FROM T_mobilier m
LEFT JOIN T_zones       z ON m.zone        = z.id
LEFT JOIN L_natures_mob n ON m.nature       = n.id
LEFT JOIN L_contextes   c ON m.contexte_sol = c.id
LEFT JOIN L_auteurices  a ON m.auteurice    = a.id
ORDER BY m.date_creation DESC;

Résultat : un beau tableau triable et filtrable, généré automatiquement. :sparkles:

form — formulaire de saisie

Le composant form est un peu plus élaboré : on déclare d'abord le formulaire (son titre, son action), puis chaque champ est une ligne du SELECT suivant.

SELECT 'form' AS component,
    'Enregistrer un mobilier' AS title,
    'enregistrer_mobilier.sql' AS action;  -- la page qui traitera l'envoi

SELECT
    'identifiant'   AS name,
    'Identifiant'   AS label,
    'text'          AS type,
    TRUE            AS required,
    'Ex: MOB-042'   AS placeholder;

SELECT
    'zone'          AS name,
    'Zone'          AS label,
    'select'        AS type,
    TRUE            AS required,
    (SELECT json_group_array(json_object('label', nom, 'value', id))
     FROM T_zones ORDER BY nom) AS options;

SELECT
    'nature'        AS name,
    'Nature'        AS label,
    'select'        AS type,
    (SELECT json_group_array(json_object('label', nature_mob, 'value', id))
     FROM L_natures_mob ORDER BY nature_mob) AS options;

SELECT
    'contexte_sol'  AS name,
    'Contexte'      AS label,
    'select'        AS type,
    (SELECT json_group_array(json_object('label', contexte, 'value', id))
     FROM L_contextes ORDER BY contexte) AS options;

SELECT
    'auteurice'     AS name,
    'Auteur·ice'    AS label,
    'select'        AS type,
    (SELECT json_group_array(json_object('label', auteurice, 'value', id))
     FROM L_auteurices ORDER BY auteurice) AS options;

SELECT
    'commentaires'  AS name,
    'Commentaires'  AS label,
    'textarea'      AS type;

Les menus déroulants ('select') sont alimentés par des sous-requêtes qui génèrent du JSON. C'est la syntaxe propre à SQLPage pour les options dynamiques. La fonction json_group_array et json_object sont des fonctions SQLite standard.

La page de traitement du formulaire

Quand le formulaire est soumis, SQLPage envoie les valeurs à la page indiquée dans action. Créez enregistrer_mobilier.sql :

-- On insère l'enregistrement
INSERT INTO T_mobilier
    (identifiant, zone, nature, contexte_sol, auteurice, commentaires,
     date_creation, date_modification)
VALUES (
    $identifiant,
    $zone,
    $nature,
    $contexte_sol,
    $auteurice,
    $commentaires,
    datetime('now'),
    datetime('now')
);

-- On redirige vers la liste du mobilier avec un message de confirmation
SELECT 'redirect' AS component, 'mobilier.sql' AS link;

Les variables $identifiant, $zone, etc. sont automatiquement extraites des données envoyées par le formulaire. SQLPage les paramètre proprement (pas d'injection SQL possible :lock:). Après l'insertion, la redirection renvoie l'utilisateur vers la liste.

:bulb: SQLPage protège automatiquement contre les injections SQL en paramétrant toutes les variables $. Ne les concaténez jamais manuellement dans une requête — laissez SQLPage faire son travail.

card — afficher des cartes

SELECT 'card' AS component, 3 AS columns;  -- affiche 3 cartes par ligne

SELECT
    z.nom         AS title,
    p.priorite    AS description,
    CASE z.fait WHEN 1 THEN 'Prospectée' ELSE 'À prospecter' END AS footer,
    CASE z.fait WHEN 1 THEN 'green' ELSE 'red' END AS color
FROM T_zones z
LEFT JOIN L_priorites p ON z.priorite = p.id
ORDER BY z.nom;

hero — en-tête de page

SELECT 'hero' AS component,
    'Prospections forestières' AS title,
    'Base de données de terrain' AS description;

text — du texte simple

SELECT 'text' AS component,
    '## Bienvenue

Sélectionnez une section dans le menu pour saisir ou consulter des données.' AS contents_md;

SQLPage supporte le Markdown dans les composants texte. :writing_hand:

Construire la navigation

SQLPage génère automatiquement une barre de navigation à partir d'un fichier spécial : sqlpage/navbar.sql. Créez-le :

SELECT 'dynamic' AS component, json_group_array(json_object(
    'title', title,
    'link', link,
    'icon', icon
)) AS properties
FROM (VALUES
    ('Accueil',   'index.sql',   'home'),
    ('Mobilier',  'mobilier.sql', 'shovel'),
    ('Structures','structures.sql', 'building'),
    ('Anomalies', 'anomalies.sql', 'alert-triangle'),
    ('Zones',     'zones.sql',   'map')
) AS nav(title, link, icon);

Les icônes viennent de Tabler Icons — des centaines d'icônes disponibles, il suffit de mettre le nom. :art:

Assembler les pages complètes

index.sql — page d'accueil

SELECT 'hero' AS component,
    'Prospections forestières' AS title,
    'Interface de saisie et de consultation' AS description;

SELECT 'card' AS component, 3 AS columns;

SELECT 'Mobilier' AS title,
    'Saisir et consulter le mobilier archéologique' AS description,
    'mobilier.sql' AS link,
    'shovel' AS icon;

SELECT 'Structures' AS title,
    'Saisir et consulter les structures' AS description,
    'structures.sql' AS link,
    'building' AS icon;

SELECT 'Anomalies' AS title,
    'Saisir et consulter les anomalies' AS description,
    'anomalies.sql' AS link,
    'alert-triangle' AS icon;

mobilier.sql — liste et formulaire

SELECT 'hero' AS component, 'Mobilier archéologique' AS title;

-- Le formulaire de saisie
SELECT 'form' AS component,
    'Enregistrer un nouveau mobilier' AS title,
    'enregistrer_mobilier.sql' AS action;

SELECT 'identifiant' AS name, 'Identifiant' AS label, 'text' AS type,
    TRUE AS required, 'Ex: MOB-042' AS placeholder;

SELECT 'zone' AS name, 'Zone' AS label, 'select' AS type,
    (SELECT json_group_array(json_object('label', nom, 'value', CAST(id AS TEXT)))
     FROM T_zones ORDER BY nom) AS options;

SELECT 'nature' AS name, 'Nature' AS label, 'select' AS type,
    (SELECT json_group_array(json_object('label', nature_mob, 'value', CAST(id AS TEXT)))
     FROM L_natures_mob ORDER BY nature_mob) AS options;

SELECT 'contexte_sol' AS name, 'Contexte' AS label, 'select' AS type,
    (SELECT json_group_array(json_object('label', contexte, 'value', CAST(id AS TEXT)))
     FROM L_contextes ORDER BY contexte) AS options;

SELECT 'auteurice' AS name, 'Auteur·ice' AS label, 'select' AS type,
    (SELECT json_group_array(json_object('label', auteurice, 'value', CAST(id AS TEXT)))
     FROM L_auteurices ORDER BY auteurice) AS options;

SELECT 'commentaires' AS name, 'Commentaires' AS label, 'textarea' AS type;

-- La liste du mobilier existant
SELECT 'table' AS component, 'Mobilier enregistré' AS title,
    TRUE AS search, TRUE AS sort;

SELECT
    m.identifiant,
    z.nom        AS zone,
    n.nature_mob AS nature,
    c.contexte   AS contexte,
    a.auteurice  AS auteurice,
    m.date_creation,
    m.commentaires
FROM T_mobilier m
LEFT JOIN T_zones       z ON m.zone        = z.id
LEFT JOIN L_natures_mob n ON m.nature       = n.id
LEFT JOIN L_contextes   c ON m.contexte_sol = c.id
LEFT JOIN L_auteurices  a ON m.auteurice    = a.id
ORDER BY m.date_creation DESC;

Ça, c'est une page complète avec formulaire de saisie et tableau de consultation en dessous. Tout ça en SQL. :exploding_head:

Quelques astuces utiles

Afficher un message de confirmation après enregistrement

Sur la page de liste, vous pouvez afficher un message si on vient de faire une insertion. SQLPage transmet automatiquement un paramètre $success si vous le définissez dans la redirection :

-- Dans enregistrer_mobilier.sql
SELECT 'redirect' AS component,
    'mobilier.sql?success=Mobilier enregistré avec succès !' AS link;

-- Dans mobilier.sql, au début
SELECT 'alert' AS component, 'success' AS type, $success AS contents
WHERE $success IS NOT NULL;

Valider les données côté SQL

-- Dans enregistrer_mobilier.sql
SELECT 'error' AS component,
    'L''identifiant ne peut pas être vide.' AS description
WHERE $identifiant IS NULL OR $identifiant = '';

INSERT INTO T_mobilier ...
WHERE $identifiant IS NOT NULL AND $identifiant != '';

Comptes et statistiques

SELECT 'big_number' AS component;

SELECT COUNT(*) AS value, 'Mobiliers enregistrés' AS title,
    'shovel' AS icon, 'blue' AS color
FROM T_mobilier;

SELECT COUNT(*) AS value, 'Zones prospectées' AS title,
    'check' AS icon, 'green' AS color
FROM T_zones WHERE fait = 1;

SELECT COUNT(*) AS value, 'Zones restantes' AS title,
    'clock' AS icon, 'orange' AS color
FROM T_zones WHERE fait = 0 OR fait IS NULL;

Rendre l'application accessible à l'équipe

Si vous voulez que toute l'équipe accède à l'interface sur le même réseau (le WiFi de la maison de fouilles, par exemple), lancez SQLPage en spécifiant l'adresse d'écoute dans sqlpage.json :

{
    "database_url": "sqlite://./prospections_foret.db",
    "listen_address": "0.0.0.0:8080"
}

Communiquez votre adresse IP locale à l'équipe (genre 192.168.1.42:8080) et tout le monde peut accéder à l'interface depuis son navigateur. :globe_with_meridians:

:warning: SQLite ne supporte pas bien les écritures simultanées. Si deux personnes soumettent un formulaire exactement au même moment, l'une des deux risque une erreur. Dans la pratique, sur des petites équipes de terrain, c'est rarement un problème. Mais si vous avez besoin de vraie concurrence, il faudra passer à PostgreSQL — SQLPage le supporte très bien.

Ressources

powered by GitbookLes ateliers du MIAOU 27-04-2026 10:08:40

results matching ""

    No results matching ""