feat(ui): chat frontend, model picker, streaming, admin, a11y

Preview/UI work: chat renderer, streaming/think-trace, model picker
(instant-open/refresh-backoff), admin panel, a11y, gallery landing.

Source: vendor/photocraft (ad1cff44) static files, 3-way merged onto
upstream/dev (a8c147b2). Resolution policy in pr_work/D_conflicts.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pewdiepie-archdaemon
2026-10-13 13:42:33 +00:00
co-authored by Claude Opus 5.5
parent a8c147b238
commit 8fda203d28
26 changed files with 571 additions and 144 deletions
+38 -3
View File
@@ -1302,8 +1302,11 @@ let _libSyncStatus = {
source: '',
warming: false,
loading: false,
stale: false,
};
let _libSyncTicker = null;
let _libStaleRefetchTimer = null;
let _libStaleRefetchCount = 0;
function _libSyncDateFrom(value) {
if (!value) return null;
@@ -1358,9 +1361,37 @@ function _setEmailSyncStatus(next = {}) {
if (Object.prototype.hasOwnProperty.call(next, 'loading')) {
_libSyncStatus.loading = Boolean(next.loading);
}
if (Object.prototype.hasOwnProperty.call(next, 'stale')) {
_libSyncStatus.stale = Boolean(next.stale);
}
_renderEmailSyncStatus();
}
// A server-side stale (sync.stale) response means the SWR snapshot was served
// while a background IMAP refresh is still running. Re-fetch in place after a
// short delay (bounded) so the fresh rows land with no skeleton and no scroll
// jump. We only keep retrying while the response is still marked stale.
function _clearStaleEmailRefetch() {
if (_libStaleRefetchTimer) {
clearTimeout(_libStaleRefetchTimer);
_libStaleRefetchTimer = null;
}
_libStaleRefetchCount = 0;
}
function _scheduleStaleEmailRefetch() {
if (_libStaleRefetchTimer || _libStaleRefetchCount >= 3) return;
const folderAtSchedule = state._libFolder;
const accountAtSchedule = state._libAccountId || '';
_libStaleRefetchCount++;
_libStaleRefetchTimer = setTimeout(() => {
_libStaleRefetchTimer = null;
if (document.hidden) return;
if (folderAtSchedule !== state._libFolder || accountAtSchedule !== (state._libAccountId || '')) return;
_loadEmails({ force: false, useCache: false, skipFastPath: true });
}, 2500);
}
function _libCacheKeyFor(accountId, folder, filter, hasAttachments) {
return [
accountId || '',
@@ -2211,7 +2242,7 @@ export function openEmailLibrary(opts = {}) {
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5"/><path d="M12 19l-7-7 7-7"/></svg>
<span>Back</span>
</button>
<button class="close-btn" id="email-lib-close">\u2716</button>
<button class="close-btn" id="email-lib-close" aria-label="Close">\u2716</button>
</div>
</div>
<div class="modal-body" style="display:flex;flex-direction:column;gap:10px;overflow:hidden;">
@@ -4794,7 +4825,7 @@ export async function _refreshUnreadBadge({ unreadCountOverride = null, preserve
} catch (_) { _syncUnreadTabBadge(0); }
}
export async function _loadEmails({ force = false, useCache = true } = {}) {
export async function _loadEmails({ force = false, useCache = true, skipFastPath = false } = {}) {
const seq = ++_libLoadSeq;
state._libLoading = true;
const accountAtStart = state._libAccountId || '';
@@ -4848,11 +4879,15 @@ export async function _loadEmails({ force = false, useCache = true } = {}) {
}
const stats = document.getElementById('email-lib-stats');
if (stats) stats.textContent = `${state._libTotal} emails`;
const _isStale = Boolean(sync.stale);
_setEmailSyncStatus({
updatedAt: sync.updated_at || '',
source: sync.source || (cacheSource ? 'client_cache' : ''),
loading: false,
stale: _isStale,
});
if (_isStale) _scheduleStaleEmailRefetch();
else _clearStaleEmailRefetch();
if (cacheable && !cacheSource) {
_libCachePut(ck, { emails: state._libEmails.slice(), total: state._libTotal, sync });
}
@@ -4888,7 +4923,7 @@ export async function _loadEmails({ force = false, useCache = true } = {}) {
const accountQS = accountAtStart ? `&account_id=${encodeURIComponent(accountAtStart)}` : '';
const attQS = hasAttachmentsAtStart ? '&has_attachments=1' : '';
const dateQS = `${dateFromAtStart ? `&date_from=${encodeURIComponent(dateFromAtStart)}` : ''}${dateToAtStart ? `&date_to=${encodeURIComponent(dateToAtStart)}` : ''}`;
if (!cached && cacheable && !force) {
if (!cached && cacheable && !force && !skipFastPath) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 450);
try {