Nouvelle version de sw.js pour prendre en compte les nouvelles version (pb de cache sinon) + nouveaux accès pour passer de l'API Poc à la version définitive

This commit is contained in:
2026-10-06 07:54:37 +02:00
parent 27f9ce7a16
commit 43a52ca66a
3 changed files with 118 additions and 172 deletions
+51 -113
View File
@@ -2,13 +2,57 @@
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUrl>
#include <QUrlQuery>
#ifdef Q_OS_WASM
#include <emscripten/val.h>
#endif
DataProvider::DataProvider(QObject *parent) : QObject(parent)
{
}
// ----------------------------------------------------------------------------
// Adresse de l'API (synthèse §49) : "/api" sur l'ORIGINE de la page qui a chargé le wasm (protocole + hôte + port),
// lue dans window.location.origin :
// - à distance : https://b<N>.ship2net.fr/api -> Traefik (session Live vérifiée à chaque requête) -> nginx du CM5
// -> API (port 8080). Même origine que la page : le cookie de session est joint automatiquement par le
// navigateur, et il n'y a plus de requête entre origines (CORS).
// - en local : https://<IP du CM5>/api -> nginx du CM5 -> API.
// Hors navigateur (exécution native pour la mise au point) : variable d'environnement SHIPHEART_API, ou l'API locale.
// ----------------------------------------------------------------------------
QString DataProvider::adresseApi()
{
#ifdef Q_OS_WASM
const std::string origine = emscripten::val::global("location")["origin"].as<std::string>();
return QString::fromStdString(origine) + QStringLiteral("/api");
#else
return qEnvironmentVariable("SHIPHEART_API", QStringLiteral("http://127.0.0.1:8080"));
#endif
}
// ----------------------------------------------------------------------------
// Traitement commun d'une réponse en erreur. Une réponse 401 vient du vérificateur du Live (session expirée, ou accès
// suspendu/révoqué depuis le portail) : elle est signalée à part (sessionExpiree), car elle ne se résoudra pas seule -
// contrairement à une coupure réseau, qui relève du simple "hors ligne".
// Retour : true si la réponse est en erreur (déjà signalée : l'appelant n'a plus qu'à sortir).
// ----------------------------------------------------------------------------
bool DataProvider::reponseEnErreur(QNetworkReply *reply)
{
if (reply->error() == QNetworkReply::NoError) return false;
const int statut = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statut == 401 && !m_sessionExpiree) {
m_sessionExpiree = true;
emit sessionExpireeChange();
}
signalerEchec();
emit erreur(reply->errorString());
return true;
}
void DataProvider::signalerSucces()
{
m_connecte = true;
@@ -18,6 +62,8 @@ void DataProvider::signalerSucces()
void DataProvider::signalerEchec()
{
// Ne touche JAMAIS m_derniereConnexionReussie ici : on veut garder la
// trace du dernier succès réel, pas la remplacer par l'instant de l'échec.
if (m_connecte) {
m_connecte = false;
emit etatConnexionChange();
@@ -26,28 +72,15 @@ void DataProvider::signalerEchec()
void DataProvider::get_familles(const QString &type)
{
if (m_replyFamillesEnCours) m_replyFamillesEnCours->abort();
QUrl url(m_baseUrl + "/elements/familles");
QUrlQuery query;
query.addQueryItem("type", type);
url.setQuery(query);
QNetworkRequest request(url);
request.setTransferTimeout(5000);
auto reply = m_manager.get(request);
m_replyFamillesEnCours = reply;
auto reply = m_manager.get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, this, [this, reply, type]() {
reply->deleteLater();
if (m_replyFamillesEnCours == reply) m_replyFamillesEnCours = nullptr;
if (reply->error() == QNetworkReply::OperationCanceledError) return;
if (reply->error() != QNetworkReply::NoError) {
signalerEchec();
emit erreur(reply->errorString());
return;
}
if (reponseEnErreur(reply)) return;
const auto doc = QJsonDocument::fromJson(reply->readAll());
const auto obj = doc.object();
if (obj["status"].toString() != "ok") {
@@ -62,29 +95,16 @@ void DataProvider::get_familles(const QString &type)
void DataProvider::get_elements(const QString &type, int famille)
{
if (m_replyElementsEnCours) m_replyElementsEnCours->abort();
QUrl url(m_baseUrl + "/elements");
QUrlQuery query;
query.addQueryItem("type", type);
query.addQueryItem("famille", QString::number(famille));
url.setQuery(query);
QNetworkRequest request(url);
request.setTransferTimeout(5000);
auto reply = m_manager.get(request);
m_replyElementsEnCours = reply;
auto reply = m_manager.get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, this, [this, reply, type, famille]() {
reply->deleteLater();
if (m_replyElementsEnCours == reply) m_replyElementsEnCours = nullptr;
if (reply->error() == QNetworkReply::OperationCanceledError) return;
if (reply->error() != QNetworkReply::NoError) {
signalerEchec();
emit erreur(reply->errorString());
return;
}
if (reponseEnErreur(reply)) return;
const auto doc = QJsonDocument::fromJson(reply->readAll());
const auto obj = doc.object();
if (obj["status"].toString() != "ok") {
@@ -102,7 +122,6 @@ void DataProvider::set_circuit(int id, int consigne)
QUrl url(m_baseUrl + "/elements/circuit/" + QString::number(id));
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setTransferTimeout(5000);
QJsonObject body;
body["consigne"] = consigne;
@@ -111,89 +130,8 @@ void DataProvider::set_circuit(int id, int consigne)
auto reply = m_manager.sendCustomRequest(request, "PATCH", data);
connect(reply, &QNetworkReply::finished, this, [this, reply, id]() {
reply->deleteLater();
const bool succes = (reply->error() == QNetworkReply::NoError);
const bool succes = !reponseEnErreur(reply);
if (succes) signalerSucces();
else {
signalerEchec();
emit erreur(reply->errorString());
}
emit circuitModifie(id, succes);
});
}
void DataProvider::get_mesures(const QString &type, int id, int periode, const QString &variable)
{
if (m_replyMesuresEnCours) m_replyMesuresEnCours->abort();
QUrl url(m_baseUrl + "/mesures");
QUrlQuery query;
query.addQueryItem("type", type);
query.addQueryItem("id", QString::number(id));
query.addQueryItem("periode", QString::number(periode));
if (!variable.isEmpty()) query.addQueryItem("variable", variable);
url.setQuery(query);
QNetworkRequest request(url);
request.setTransferTimeout(8000); // Requête potentiellement plus lourde (jusqu'à 200 points) qu'un simple /elements - délai un peu plus généreux.
auto reply = m_manager.get(request);
m_replyMesuresEnCours = reply;
connect(reply, &QNetworkReply::finished, this, [this, reply, type, id, periode, variable]() {
reply->deleteLater();
if (m_replyMesuresEnCours == reply) m_replyMesuresEnCours = nullptr;
if (reply->error() == QNetworkReply::OperationCanceledError) return;
if (reply->error() != QNetworkReply::NoError) {
signalerEchec();
emit erreur(reply->errorString());
return;
}
const auto doc = QJsonDocument::fromJson(reply->readAll());
const auto obj = doc.object();
if (obj["status"].toString() != "ok") {
signalerEchec();
emit erreur(obj["message"].toString());
return;
}
signalerSucces();
emit mesuresRecues(type, id, periode, variable, obj);
});
}
void DataProvider::get_energie(const QString &type, int id, int periode)
{
if (m_replyEnergieEnCours) m_replyEnergieEnCours->abort();
QUrl url(m_baseUrl + "/energie");
QUrlQuery query;
query.addQueryItem("type", type);
query.addQueryItem("id", QString::number(id));
query.addQueryItem("periode", QString::number(periode));
url.setQuery(query);
QNetworkRequest request(url);
request.setTransferTimeout(8000);
auto reply = m_manager.get(request);
m_replyEnergieEnCours = reply;
connect(reply, &QNetworkReply::finished, this, [this, reply, type, id, periode]() {
reply->deleteLater();
if (m_replyEnergieEnCours == reply) m_replyEnergieEnCours = nullptr;
if (reply->error() == QNetworkReply::OperationCanceledError) return;
if (reply->error() != QNetworkReply::NoError) {
signalerEchec();
emit erreur(reply->errorString());
return;
}
const auto doc = QJsonDocument::fromJson(reply->readAll());
const auto obj = doc.object();
if (obj["status"].toString() != "ok") {
signalerEchec();
emit erreur(obj["message"].toString());
return;
}
signalerSucces();
emit energieRecue(type, id, periode, obj);
});
}
+21 -31
View File
@@ -2,11 +2,8 @@
#include <QObject>
#include <QNetworkAccessManager>
#include <QJsonArray>
#include <QJsonObject>
#include <QDateTime>
class QNetworkReply;
// ============================================================================
// DataProvider - couche d'accès aux données du client WASM (cas 2/3).
// ----------------------------------------------------------------------------
@@ -15,14 +12,13 @@ class QNetworkReply;
// HTTP/JSON est encapsulée ici - le QML ne voit que des signaux avec des
// tableaux déjà prêts à l'emploi.
//
// Portée actuelle : les 4 domaines (circuits, batteries, capteurs, reservoirs)
// en lecture, écriture (consigne/mode auto) sur circuits uniquement, et
// désormais l'historique/courbe (capteurs uniquement pour l'instant, cf
// GET /mesures côté API).
// Portée actuelle : les 4 domaines (circuit, batterie, capteur, reservoir)
// en lecture, écriture (consigne/mode auto) sur circuit uniquement.
//
// Suit également l'état de connexion (succès/échec de chaque requête) pour
// permettre à l'UI d'afficher un état "hors ligne" explicite plutôt que de
// laisser les listes silencieusement vides ou figées.
// laisser les listes silencieusement vides ou figées (cf synthèse, gestion
// du hors-ligne).
// ============================================================================
class DataProvider : public QObject
{
@@ -31,54 +27,48 @@ class DataProvider : public QObject
Q_PROPERTY(bool connecte READ connecte NOTIFY etatConnexionChange)
Q_PROPERTY(bool aDejaEteConnecte READ aDejaEteConnecte NOTIFY etatConnexionChange)
Q_PROPERTY(QDateTime derniereConnexionReussie READ derniereConnexionReussie NOTIFY etatConnexionChange)
// Live distant (synthèse §49) : vrai dès qu'une réponse 401 indique que la session Live a expiré ou que l'accès a
// été suspendu/révoqué depuis le portail - l'UI peut alors inviter à rouvrir le Live depuis le portail, plutôt
// que d'afficher un simple "hors ligne" (rien ne reviendra tout seul).
Q_PROPERTY(bool sessionExpiree READ sessionExpiree NOTIFY sessionExpireeChange)
public:
explicit DataProvider(QObject *parent = nullptr);
// TODO : rendre l'adresse de base configurable (IP locale du CM5 en cas 2,
// domaine VPS en cas 3 - cf QR code d'accès, §"Architecture cible"/Cas 2-3
// de la synthèse) - en dur sur le relais VPS pour cette étape.
// Adresse de l'API : "/api" sur l'origine même de la page (synthèse §49) - https://b<N>.ship2net.fr/api à
// distance (relais Traefik, session vérifiée), https://<IP du CM5>/api en local (nginx du CM5). Une seule
// version du wasm pour tous les cas, et plus d'adresse en dur.
Q_INVOKABLE void get_familles(const QString &type);
Q_INVOKABLE void get_elements(const QString &type, int famille);
// Écriture rapide sur un circuit (bouton d'accueil) - PATCH /elements/circuit/<id>.
Q_INVOKABLE void set_circuit(int id, int consigne);
// Historique/courbe pour la page de détail - GET /mesures.
// periode : 0=1h, 1=24h, 2=7j, 3=30j (même convention que le natif).
// variable : requis pour circuits/batteries (sélecteur de variable côté
// client), ignoré pour capteurs/reservoirs (variable fixe côté serveur).
Q_INVOKABLE void get_mesures(const QString &type, int id, int periode, const QString &variable = QString());
// Consommation d'énergie (Phase 4, batteries uniquement pour l'instant) -
// GET /energie. Endpoint séparé de get_mesures() : réponse de forme très
// différente (barres + comparatif par circuit, pas une simple courbe).
Q_INVOKABLE void get_energie(const QString &type, int id, int periode);
bool connecte() const { return m_connecte; }
bool aDejaEteConnecte() const { return m_derniereConnexionReussie.isValid(); }
QDateTime derniereConnexionReussie() const { return m_derniereConnexionReussie; }
bool sessionExpiree() const { return m_sessionExpiree; }
signals:
// Le type (et, pour les éléments, la famille) sont désormais inclus dans
// chaque signal - indispensable avec plusieurs domaines actifs en
// parallèle, pour que le QML sache à quelle réponse il a affaire.
void famillesRecues(const QString &type, const QJsonArray &familles);
void elementsRecus(const QString &type, int famille, const QJsonArray &elements);
void circuitModifie(int id, bool succes);
void mesuresRecues(const QString &type, int id, int periode, const QString &variable, const QJsonObject &donnees);
void energieRecue(const QString &type, int id, int periode, const QJsonObject &donnees);
void erreur(const QString &message);
void etatConnexionChange();
void sessionExpireeChange();
private:
void signalerSucces();
void signalerEchec();
bool reponseEnErreur(QNetworkReply *reply); // true si erreur (signalée), en distinguant la session expirée (401)
static QString adresseApi();
QNetworkAccessManager m_manager;
QString m_baseUrl = QStringLiteral("https://api.ship2net.fr");
QString m_baseUrl = adresseApi();
bool m_connecte = false;
QDateTime m_derniereConnexionReussie;
QNetworkReply *m_replyFamillesEnCours = nullptr;
QNetworkReply *m_replyElementsEnCours = nullptr;
QNetworkReply *m_replyMesuresEnCours = nullptr;
QNetworkReply *m_replyEnergieEnCours = nullptr;
bool m_sessionExpiree = false;
QDateTime m_derniereConnexionReussie; // invalide (isValid()==false) tant qu'aucun succès n'a jamais eu lieu
};
+46 -28
View File
@@ -1,12 +1,22 @@
// Service worker du client ShipHeart.
// Stratégie "cache d'abord" pour la coquille statique de l'app (mêmes fichiers
// que le domaine de la page elle-même), MAIS jamais pour les appels vers
// l'API (données live, cross-origine) - ceux-ci passent toujours par le
// réseau, sans jamais être mis en cache, sous peine de figer les données
// affichées (rencontré concrètement : rafraîchissement périodique et même
// rechargement de page ne montraient plus jamais de valeurs à jour).
// Service worker du client ShipHeart (synthèse §49).
//
// Coquille statique de l'app (index.html, wasm, JS, icônes) : "cache d'abord" - le wasm pèse ~25 Mo, il ne doit
// pas être retéléchargé à chaque ouverture (connexion mobile en mer). Mais avec une MISE À JOUR FIABLE :
// 1. CACHE_NAME porte l'horodatage du déploiement : toute nouvelle version modifie sw.js, que le navigateur
// revérifie à chaque ouverture -> installation d'une nouvelle version du service worker ;
// 2. à l'installation, les fichiers sont téléchargés en CONTOURNANT le cache HTTP du navigateur
// (cache: "reload") - sinon, la nouvelle version pouvait se remplir avec les anciens fichiers gardés par ce
// cache (cas rencontré : ancien wasm toujours servi après un déploiement) ;
// 3. à l'activation, les anciens caches sont supprimés et, s'il y en avait (= c'est une MISE À JOUR, pas une
// première installation), les pages ouvertes sont rechargées une fois : elles exécutent aussitôt la nouvelle
// version, sans action de l'utilisateur.
//
// Jamais de cache (le navigateur traite la requête normalement, sans passer par ce service worker) :
// - l'API (/api/..., même origine depuis la synthèse §49) : données live - un cache figerait les valeurs ;
// - l'ouverture de session Live (/_auth) ;
// - toute requête autre que GET, et toute requête vers une autre origine.
const CACHE_NAME = "shipheart-wasm-v1";
const CACHE_NAME = "shipheart-wasm-20261005_193211"; // Horodatage posé par le déploiement - ne pas modifier à la main
const APP_SHELL = [
"./index.html",
"./qtloader.js",
@@ -23,7 +33,8 @@ self.addEventListener("install", (event) => {
caches.open(CACHE_NAME).then((cache) =>
Promise.allSettled(
APP_SHELL.map((url) =>
cache.add(url).catch((err) => console.warn("Échec de mise en cache:", url, err))
cache.add(new Request(url, { cache: "reload" })) // jamais le cache HTTP du navigateur (point 2)
.catch((err) => console.warn("Échec de mise en cache:", url, err))
)
)
)
@@ -32,34 +43,41 @@ self.addEventListener("install", (event) => {
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
)
)
);
self.clients.claim();
event.waitUntil((async () => {
const anciens = (await caches.keys()).filter((key) => key !== CACHE_NAME);
await Promise.all(anciens.map((key) => caches.delete(key)));
await self.clients.claim();
// Mise à jour (et non première installation) : recharger les pages ouvertes, qui exécutent encore l'ancienne
// version (point 3). NB : rechargement lancé SANS l'attendre - le navigateur ne transmet les requêtes de la page
// rechargée à ce service worker qu'une fois l'activation terminée ; l'attendre ici bloquerait les deux
// indéfiniment (constaté en test).
if (anciens.length > 0) {
const fenetres = await self.clients.matchAll({ type: "window" });
for (const fenetre of fenetres) {
fenetre.navigate(fenetre.url).catch((err) => console.warn("Rechargement impossible:", fenetre.url, err));
}
}
})());
});
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
const requete = event.request;
const url = new URL(requete.url);
// Appels vers l'API (données live, autre origine) : jamais de cache,
// toujours le réseau.
if (url.origin !== self.location.origin) {
event.respondWith(fetch(event.request));
return;
}
// Hors coquille statique : pas de respondWith -> le navigateur traite la requête normalement (réseau).
if (requete.method !== "GET") return;
if (url.origin !== self.location.origin) return;
if (url.pathname.startsWith("/api/") || url.pathname === "/_auth") return;
// Coquille statique de l'app (même origine) : cache d'abord, comme avant.
// Coquille statique : cache d'abord, réseau sinon (et mise en cache de la réponse).
event.respondWith(
caches.match(event.request).then((cached) => {
caches.match(requete).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((response) => {
return fetch(requete).then((response) => {
if (response && response.ok) {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, responseClone));
caches.open(CACHE_NAME).then((cache) => cache.put(requete, responseClone));
}
return response;
});