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:
+51
-113
@@ -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
@@ -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
@@ -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;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user