Feat v0.5.2 chat surface (#32)
All checks were successful
All checks were successful
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #32.
This commit is contained in:
@@ -17,13 +17,20 @@ export function Menu({ items = [], anchor, open, direction = 'down-right', onSel
|
||||
const selectableItems = items.filter(i => !i.divider && !i.disabled);
|
||||
|
||||
// Position relative to anchor
|
||||
// When the surface uses CSS transform: scale(), getBoundingClientRect()
|
||||
// returns scaled coordinates but position:fixed uses viewport coordinates.
|
||||
// Dividing by the scale factor converts back to true viewport pixels.
|
||||
useEffect(() => {
|
||||
if (!open || !anchor) return;
|
||||
const rect = typeof anchor.getBoundingClientRect === 'function'
|
||||
const scale = window.sw?.shell?.getScale?.() || 1;
|
||||
const raw = typeof anchor.getBoundingClientRect === 'function'
|
||||
? anchor.getBoundingClientRect()
|
||||
: anchor;
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const rect = scale !== 1
|
||||
? { top: raw.top / scale, bottom: raw.bottom / scale, left: raw.left / scale, right: raw.right / scale }
|
||||
: raw;
|
||||
const vw = window.innerWidth / scale;
|
||||
const vh = window.innerHeight / scale;
|
||||
|
||||
let top, left;
|
||||
const [vDir, hDir] = direction.split('-');
|
||||
|
||||
@@ -209,13 +209,26 @@ export function createAuth() {
|
||||
// Suppress _on401Failure redirect during boot — boot handles 401 itself.
|
||||
_booting = true;
|
||||
|
||||
// Defensive timeout: boot MUST complete within 8s.
|
||||
// If stale tokens cause a network hang or unexpected deadlock,
|
||||
// clear tokens and let the login page render.
|
||||
let timedOut = false;
|
||||
const bootTimeout = setTimeout(() => {
|
||||
timedOut = true;
|
||||
console.warn('[sw.auth] Boot timed out — clearing stale tokens');
|
||||
_clearTokens();
|
||||
_booting = false;
|
||||
}, 8000);
|
||||
|
||||
// Fetch permissions (validates the access token)
|
||||
try {
|
||||
await _fetchPermissions();
|
||||
} catch (e) {
|
||||
if (timedOut) return;
|
||||
if (e.status === 401) {
|
||||
// Token expired — try refresh
|
||||
const ok = await auth.refresh();
|
||||
if (timedOut) return;
|
||||
if (ok) {
|
||||
try { await _fetchPermissions(); }
|
||||
catch (_) { _clearTokens(); }
|
||||
@@ -226,6 +239,7 @@ export function createAuth() {
|
||||
console.warn('[sw.auth] Boot: permissions fetch failed (keeping tokens):', e.message);
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(bootTimeout);
|
||||
_booting = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,12 @@ async function _buildError(resp, method, path) {
|
||||
*/
|
||||
export function createRestClient(getToken, onRefresh, on401Failure) {
|
||||
|
||||
// Auth endpoints must never trigger the 401→refresh→retry loop.
|
||||
// Doing so causes a deadlock: refresh() calls POST /auth/refresh via
|
||||
// _request, which 401s and calls onRefresh(), which returns the same
|
||||
// unresolved refresh promise — both await each other forever.
|
||||
const _authPaths = ['/api/v1/auth/login', '/api/v1/auth/refresh', '/api/v1/auth/register'];
|
||||
|
||||
async function _request(path, method, body, opts) {
|
||||
const _opts = opts || {};
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
@@ -62,8 +68,9 @@ export function createRestClient(getToken, onRefresh, on401Failure) {
|
||||
|
||||
let resp = await fetch(BASE + path, fetchOpts);
|
||||
|
||||
// 401 — try refresh once
|
||||
if (resp.status === 401) {
|
||||
// 401 — try refresh once (skip for auth endpoints to prevent deadlock)
|
||||
const isAuthPath = _authPaths.some(p => path === p || path.startsWith(p + '?'));
|
||||
if (resp.status === 401 && !isAuthPath && !_opts.skipRefresh) {
|
||||
const refreshed = await onRefresh();
|
||||
if (refreshed) {
|
||||
// Retry with new token
|
||||
|
||||
Reference in New Issue
Block a user