File size: 5,565 Bytes
aff7a19
 
 
5657d91
aff7a19
f4791ec
0d2f09e
5657d91
 
 
 
 
 
 
 
 
 
 
0d2f09e
4d25967
 
5657d91
 
 
 
 
4d25967
 
 
 
 
 
5657d91
 
 
4d25967
 
 
 
 
5f08495
aff7a19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
beba5b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aff7a19
 
 
 
4d25967
aff7a19
5657d91
 
 
 
 
 
 
 
aff7a19
4d25967
aff7a19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4d25967
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// BharatGraph Service Worker v5
// BUG-28 FIX: API responses were cached indefinitely
// NEW-A5 FIX: host === "hf.space" never matches -- HF uses *.hf.space subdomains
// SW-01 FIX: use relative paths so caching works on GitHub Pages

const CACHE = "bharatgraph-v6";
const STATIC = [
  "./",
  "./index.html",
  "./css/design-system.css",
  "./css/components.css",
  "./js/router.js",
  "./js/api.js",
  "./js/components.js",
  "./js/graph.js",
  "./js/timeline.js",
  "./js/evidence_panel.js",
  "./js/app.js"
];

self.addEventListener("install", e => {
  e.waitUntil(
    caches.open(CACHE)
      .then(c => c.addAll(STATIC))
      .catch(() => {})   // never block install on cache failure
  );
  self.skipWaiting();
});

self.addEventListener("activate", e => {
  e.waitUntil(
    caches.keys()
      .then(keys => Promise.all(
        keys.filter(k => k !== CACHE).map(k => caches.delete(k))
      ))
      .then(() => self.clients.claim())
  );
});

self.addEventListener("fetch", e => {
  const reqUrl = new URL(e.request.url);
  const host   = reqUrl.hostname;
  const path   = reqUrl.pathname;

  // NEW-A5 FIX: HuggingFace Spaces run on *.hf.space subdomains.
  // The previous check `host === "hf.space"` never matched because the actual
  // hostname is e.g. "abinazebinoly-bharatgraph.hf.space" -- use endsWith().
  // BUG-28 FIX: all API paths must be network-first, never cached.
  const isHfSpace    = host.endsWith(".hf.space") || host === "hf.space";
  const isApiPath    = path.startsWith("/search") ||
                       path.startsWith("/profile") ||
                       path.startsWith("/risk") ||
                       path.startsWith("/graph") ||
                       path.startsWith("/biography") ||
                       path.startsWith("/investigate") ||
                       path.startsWith("/admin") ||
                       path.startsWith("/stats") ||
                       path.startsWith("/health") ||
                       path.startsWith("/ws/") ||
                       path.startsWith("/runtime") ||
                       path.startsWith("/multilingual") ||
                       path.startsWith("/export") ||
                       // Prefixes below verified against every @router.get/post/put/delete
                       // decorator in api/routes/*.py -- 14 of these were named in the audit
                       // report; adversarial, affidavit, benami, conflict, debate, linguistic,
                       // policy, procurement, sources, timeline, forensics, self-learning,
                       // case-memory, resolve.
                       path.startsWith("/timeline") ||
                       path.startsWith("/forensics") ||
                       path.startsWith("/benami") ||
                       path.startsWith("/procurement") ||
                       path.startsWith("/conflict") ||
                       path.startsWith("/linguistic") ||
                       path.startsWith("/policy") ||
                       path.startsWith("/self-learning") ||
                       path.startsWith("/case-memory") ||
                       path.startsWith("/affidavit") ||
                       path.startsWith("/adversarial") ||
                       path.startsWith("/debate") ||
                       path.startsWith("/resolve") ||
                       path.startsWith("/sources") ||
                       // Not named in the audit report -- found by extracting every real
                       // route path and diffing against the allowlist directly, rather than
                       // relying on the report's own count. None of these are nested under
                       // any prefix above: export.py's /verify/{hash} and
                       // investigation.py's /connection-map and /node-evidence/{id} were
                       // never nested under /export or /investigate despite living in those
                       // files; multilingual.py's /languages, /translate, /ui-labels are
                       // separate top-level paths that don't literally start with
                       // "/multilingual" at all (only /search/multilingual and
                       // /risk/multilingual/{id} do, and those are already covered by the
                       // /search and /risk entries above).
                       path.startsWith("/verify") ||
                       path.startsWith("/connection-map") ||
                       path.startsWith("/node-evidence") ||
                       path.startsWith("/languages") ||
                       path.startsWith("/translate") ||
                       path.startsWith("/ui-labels");
  const isLocalApi   = host === "localhost" || host === "127.0.0.1";

  const isApi = isHfSpace || (isLocalApi && isApiPath);

  if (isApi) {
    // Network-first: always go to network, fall back to error JSON
    e.respondWith(
      fetch(e.request).catch(() =>
        new Response(
          JSON.stringify({ error: "API temporarily unavailable" }),
          { status: 503, headers: { "Content-Type": "application/json" } }
        )
      )
    );
    return;
  }

  // Static assets: cache-first
  e.respondWith(
    caches.match(e.request).then(cached => {
      if (cached) return cached;
      return fetch(e.request).then(response => {
        if (!response || response.status !== 200 || response.type !== "basic") {
          return response;
        }
        const clone = response.clone();
        caches.open(CACHE).then(c => c.put(e.request, clone));
        return response;
      });
    })
  );
});