feat: T6 wave 5 – readiness checks and report, compliance and medicine UI, label UI
- Readiness: all 18 checks (medicine lots, treatment log gaps, labels, harvest withdrawal), medicine subjects masked without veterinary-medicines:read, report PDF (counts only), summary cached 60 s, 409 codes READINESS_*. - Frontend: /compliance page with acknowledge/revoke, dashboard line and nav; veterinary medicine catalogue, lots, movement book with inventory and disposal; label designer check panel, print card with gap list and proof print, food business master data, product legal name. - Service worker: cacheability is path-based (the backend sends no-store on every answer); compliance, medicine, restriction, notice and label check/print paths and restricted document kinds are never cached. - ARCHITECTURE.md §2, §12.10–12.11, Annahmen 86–90. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
b912d29166
commit
b4746a2b17
149 files changed
+11869
-320
No files matched your search
@@ -68,8 +68,30 @@
|
||||
/\/api\/v1\/colonies\/?$/i,
|
||||
/\/api\/v1\/colonies\/[^/]+\/inspections\/?$/i,
|
||||
/\/api\/v1\/apiaries\/[^/]+\/(colony-overview|offline-pack)\/?$/i,
|
||||
// Prüfbereitschaft and authority data: file numbers, addresses, notices,
|
||||
// restricted zones around an apiary, medicine packs with receipts and
|
||||
// suppliers, and the label check and print (they carry the food business address)
|
||||
/\/api\/v1\/compliance(\/|$)/i,
|
||||
/\/api\/v1\/authority-notices(\/|$)/i,
|
||||
/\/api\/v1\/apiary-restrictions(\/|$)/i,
|
||||
/\/api\/v1\/apiaries\/[^/]+\/restrictions\/?$/i,
|
||||
/\/api\/v1\/veterinary-medicines(\/|$)/i,
|
||||
/\/api\/v1\/veterinary-medicine-lots(\/|$)/i,
|
||||
/\/api\/v1\/label-templates\/check(\/|$)/i,
|
||||
/\/api\/v1\/label-templates\/[^/]+\/(print|check)(\/|$)/i,
|
||||
// One single document (metadata): its kind is not in the path, and it may
|
||||
// belong to a pack, a notice or a restriction. The lists (with the kind
|
||||
// in the query, see below) and the photo downloads use other routes.
|
||||
/\/api\/v1\/documents\/[^/]+\/?$/i,
|
||||
];
|
||||
|
||||
// The document list of these records is never kept either (attachments of a
|
||||
// pack, a notice or a restriction). The list is /documents?entityType=...,
|
||||
// so the query decides; documents of inspections (photos) stay available
|
||||
// offline. Downloads are blobs and never kept anyway.
|
||||
var DOCUMENTS_PATH = /\/api\/v1\/documents\/?$/i;
|
||||
var NEVER_CACHED_DOCUMENT_TYPES = ["MEDICINE_LOT", "AUTHORITY_NOTICE", "APIARY_RESTRICTION"];
|
||||
|
||||
// A root-relative build asset, in an attribute or in the inline RSC data
|
||||
// (where quotes are escaped). The leading character keeps absolute URLs of
|
||||
// other hosts out.
|
||||
@@ -174,9 +196,35 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function isNeverCachedDocumentList(url) {
|
||||
if (!DOCUMENTS_PATH.test(url.pathname)) {
|
||||
return false;
|
||||
}
|
||||
var entityType = (url.searchParams.get("entityType") || "").toUpperCase();
|
||||
return NEVER_CACHED_DOCUMENT_TYPES.indexOf(entityType) !== -1;
|
||||
}
|
||||
|
||||
/** The path rules plus the document lists, which only the query tells apart. */
|
||||
function isCacheableApiUrl(url) {
|
||||
return isCacheableApiPath(url.pathname) && !isNeverCachedDocumentList(url);
|
||||
}
|
||||
|
||||
/** Only answers of the backend named by the runtime config are kept. */
|
||||
function shouldCacheApi(url, apiOrigin) {
|
||||
return Boolean(apiOrigin) && url.origin === apiOrigin && isCacheableApiPath(url.pathname);
|
||||
return Boolean(apiOrigin) && url.origin === apiOrigin && isCacheableApiUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a backend answer may be stored: a successful JSON record. The
|
||||
* backend marks EVERY answer `Cache-Control: no-store`, so that header says
|
||||
* nothing about one record; what must not stay on the device is decided by
|
||||
* the path rules above. Never downloads, photos, PDFs.
|
||||
*/
|
||||
function isKeepableApiAnswer(answer) {
|
||||
return (
|
||||
Boolean(answer.ok) &&
|
||||
String(answer.contentType || "").toLowerCase().indexOf("application/json") !== -1
|
||||
);
|
||||
}
|
||||
|
||||
/** The backend's origin from /api/runtime-config ({ apiUrl }), or null. */
|
||||
@@ -242,7 +290,9 @@
|
||||
isShellRoute: isShellRoute,
|
||||
extractAssetUrls: extractAssetUrls,
|
||||
isCacheableApiPath: isCacheableApiPath,
|
||||
isCacheableApiUrl: isCacheableApiUrl,
|
||||
shouldCacheApi: shouldCacheApi,
|
||||
isKeepableApiAnswer: isKeepableApiAnswer,
|
||||
apiOriginFrom: apiOriginFrom,
|
||||
isRoutePayload: isRoutePayload,
|
||||
buildAssetsToPrune: buildAssetsToPrune,
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
* The routing rules live in sw-routes.js (unit-tested).
|
||||
*
|
||||
* What is never kept: auth answers (tokens, session state), apiaries
|
||||
* themselves (coordinates, address), Next.js route payloads, and request
|
||||
* themselves (coordinates, address), Prüfbereitschaft and authority data,
|
||||
* medicine packs, single document records, Next.js route
|
||||
* payloads, and request
|
||||
* headers - entries are stored under their bare URL, so the Authorization
|
||||
* header with the access token never lands in Cache Storage. The cached
|
||||
* HTML holds no user data: the (app) layout renders nothing until the client
|
||||
@@ -19,7 +21,7 @@
|
||||
// Content version of sw-routes.js (sha256 of its LF text, first 12 hex
|
||||
// characters; sw-routes.spec.ts checks it). A changed value changes this
|
||||
// file, so every browser - Safari too - installs the worker with new rules.
|
||||
const SW_ROUTES_VERSION = "6a6f9dcbc99f";
|
||||
const SW_ROUTES_VERSION = "0681ff4b095d";
|
||||
importScripts(`/sw-routes.js?v=${SW_ROUTES_VERSION}`);
|
||||
|
||||
const {
|
||||
@@ -28,7 +30,8 @@ const {
|
||||
offlineHomeFor,
|
||||
navigationPlanFor,
|
||||
extractAssetUrls,
|
||||
isCacheableApiPath,
|
||||
isCacheableApiUrl,
|
||||
isKeepableApiAnswer: isKeepableApiHeaders,
|
||||
apiOriginFrom,
|
||||
isRoutePayload,
|
||||
buildAssetsToPrune,
|
||||
@@ -125,7 +128,7 @@ self.addEventListener("fetch", (event) => {
|
||||
event.respondWith(respondWithRuntimeConfig(event));
|
||||
return;
|
||||
}
|
||||
if (isCacheableApiPath(url.pathname)) {
|
||||
if (isCacheableApiUrl(url)) {
|
||||
respondToApiCall(event, url);
|
||||
return;
|
||||
}
|
||||
@@ -202,7 +205,7 @@ function removeUnknownCaches() {
|
||||
async function pruneForbiddenEntries() {
|
||||
const data = await caches.open(DATA_CACHE);
|
||||
for (const request of await data.keys()) {
|
||||
if (!isCacheableApiPath(new URL(request.url).pathname)) {
|
||||
if (!isCacheableApiUrl(new URL(request.url))) {
|
||||
await data.delete(request);
|
||||
}
|
||||
}
|
||||
@@ -299,8 +302,10 @@ function isGoodAnswer(response) {
|
||||
* so it is readable on the backend's cross-origin answers.
|
||||
*/
|
||||
function isKeepableApiAnswer(response) {
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
return response.ok && contentType.toLowerCase().includes("application/json");
|
||||
return isKeepableApiHeaders({
|
||||
ok: response.ok,
|
||||
contentType: response.headers.get("content-type"),
|
||||
});
|
||||
}
|
||||
|
||||
function isGoodPage(response) {
|
||||
|
||||
Reference in new issue
Block a user