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. 
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. 
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→ extrayezsqlpage.exe - Linux :
sqlpage-linux.tgz→ extrayezsqlpage - macOS :
sqlpage-macos.tgz→ extrayezsqlpage
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"
}
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
INSERTdans des tables avec géométrie, vous risquez l'erreurno 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.
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. 
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. 
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
). Après l'insertion, la redirection renvoie l'utilisateur vers la liste.
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. 
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. 
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
- Documentation SQLPage : https://sql.ophir.dev/ — très bien faite, avec plein d'exemples pour chaque composant
- Liste des composants : https://sql.ophir.dev/documentation.sql
- Tabler Icons (pour les icônes) : https://tabler.io/icons
- Releases GitHub : https://github.com/sqlpage/sqlpage/releases
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 
SQLPage protège automatiquement contre les injections SQL en paramétrant toutes les variables