Files
thandClaude Opus 4.7 f986a41ccc v2.0.4 — re-auth on 401, robust bucket-switch test
- auth: when a background poll or save returns 401 (session expired
  while the UI is still open), the api() helpers now prompt for the
  password instead of letting the save fail silently. Concurrent 401s
  collapse onto a single dialog via a shared reauthPromise, and each
  caller retries its original request once after login. /api/auth/* is
  excluded from the retry path. Extracts promptLogin() in app.js and
  exposes it as window._reauth so both the core util.js api() and the
  plugin-SDK hyprcrd.js api() can share the flow.
- tests: the "open-switches-bucket" detail test assumed buckets[0] was
  a plain top-level bucket, but the bar renders super-bucket parents
  with data-bucket-super and hides child buckets entirely, so the
  .bucket-tab.active[data-bucket] selector found nothing on setups
  with nested layouts. Test now picks two plain top-level buckets or
  skips.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:50:44 +02:00

76 lines
2.4 KiB
JavaScript

// ── Utilities ──
export function esc(s) {
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
export function escRe(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
let csrfToken = '';
export function setCsrf(token) { csrfToken = token; }
export function getCsrf() { return csrfToken; }
export async function api(path, opts = {}) {
const doFetch = () => {
const headers = { 'Content-Type': 'application/json' };
if (csrfToken) headers['X-CSRF-Token'] = csrfToken;
return fetch(path, {
headers,
...opts,
body: opts.body ? JSON.stringify(opts.body) : undefined,
});
};
let res = await doFetch();
// Session expired mid-use: prompt for re-login, then retry once. Avoid
// recursion on auth endpoints themselves — promptLogin handles its own
// fetches directly.
if (res.status === 401 && !path.startsWith('api/auth/') && typeof window._reauth === 'function') {
await window._reauth();
res = await doFetch();
}
return res.json();
}
export function $(id) { return document.getElementById(id); }
export function fuzzyMatch(needle, haystack) {
if (needle.length < 3) return null;
let ni = 0, hi = 0, score = 0, streak = 0;
while (ni < needle.length && hi < haystack.length) {
if (needle[ni] === haystack[hi]) {
streak++; score += streak * 2;
if (hi === 0 || /[\s\-_./]/.test(haystack[hi - 1])) score += 10;
if (hi < 5) score += 5 - hi;
ni++;
} else { streak = 0; }
hi++;
}
if (ni < needle.length) return null;
return score - (haystack.length - needle.length) * 0.3;
}
export function countSubstring(str, sub) {
let c = 0, p = 0;
while ((p = str.indexOf(sub, p)) !== -1) { c++; p += sub.length; }
return c;
}
export function highlight(html, words) {
let result = html;
for (const w of words) result = result.replace(new RegExp('(' + escRe(w) + ')', 'gi'), '<mark>$1</mark>');
if (!result.includes('<mark>')) {
const full = words.join('');
let fi = 0, out = '';
for (let i = 0; i < html.length && fi < full.length; i++) {
if (html[i].toLowerCase() === full[fi]) { out += '<mark>' + html[i] + '</mark>'; fi++; } else { out += html[i]; }
}
out += html.substring(out.replace(/<\/?mark>/g, '').length);
result = out;
}
return result;
}