openvibers/openvibe.network Commits

The mark's wait instead of spinner icons and bare Loading text (#791)

OP OpenVibers committed 3 days ago · 8ad3464cfe · 6 files, +20 −18

The account page's history, consent, sessions, apps and blocks loaders, the admin storage, AI and bucket panels, and the button work states now show the shared mark busy (ov-mark.js) rather than a rotating icon or Loading… on its own. The page's skeleton rows and the wallet's dimmed buttons are unchanged.

Changes

ddiff --git a/public/admin.html b/public/admin.html
iindex 60df09cf..4896810d 100644
---- a/public/admin.html
++++ b/public/admin.html
@@@ -218,7 +218,7 @@
<!-- Storage -->
<div class="section admin-clarity" id="sec-storage">
<p class="admin-intro">This shows how much space recordings and other files use. Review it when storage runs low; deleting recordings is permanent and requires a separate backup to recover.</p>
-- <div id="storage-content"><div class="loading"><i class="fa-solid fa-spinner fa-spin"></i> Analyzing storage...</div></div>
++ <div id="storage-content"><div class="loading"><span class="ov-mark" data-state="busy" data-size="16" data-tone="ink" aria-hidden="true"></span> Analyzing storage...</div></div>
</div>
<!-- Notifications (Broadcast) -->
@@@ -672,6 +672,7 @@
<!-- Shared scripts -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
++<script src="/shared/ov-mark.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/js/account-safety.js" defer></script>
<script src="/shared/notification-ui.js" defer></script>
ddiff --git a/public/js/admin/ai.js b/public/js/admin/ai.js
iindex a2b0d6bf..eb35ee64 100644
---- a/public/js/admin/ai.js
++++ b/public/js/admin/ai.js
@@@ -402,7 +402,7 @@ function renderCookieVerdict(data) {
async function checkStreamerMediaCookies() {
const el = document.getElementById('streamer-media-cookie-verdict');
-- if (el) el.innerHTML = '<div class="muted" style="margin-top:10px"><i class="fa-solid fa-spinner fa-spin"></i> Validating and running a live extraction…</div>';
++ if (el) el.innerHTML = '<div class="muted" style="margin-top:10px"><span class="ov-mark" data-state="busy" data-size="16" data-tone="ink" aria-hidden="true"></span> Validating and running a live extraction…</div>';
try {
const data = await api('/api/admin/streamer/media-tools/cookies/check', { method: 'POST', body: {} });
renderCookieVerdict(data);
@@@ -416,7 +416,7 @@ async function saveStreamerMediaCookies() {
const cookies = ta?.value?.trim() || '';
if (!cookies) return toast('Paste cookies.txt content first', 'error');
const el = document.getElementById('streamer-media-cookie-verdict');
-- if (el) el.innerHTML = '<div class="muted" style="margin-top:10px"><i class="fa-solid fa-spinner fa-spin"></i> Validating and running a live extraction…</div>';
++ if (el) el.innerHTML = '<div class="muted" style="margin-top:10px"><span class="ov-mark" data-state="busy" data-size="16" data-tone="ink" aria-hidden="true"></span> Validating and running a live extraction…</div>';
try {
const data = await api('/api/admin/streamer/media-tools/cookies', { method: 'PUT', body: { cookies } });
toast(data?.test?.ok ? 'Cookies saved — live extraction passed' : 'Cookies saved', 'success');
@@@ -442,7 +442,7 @@ async function testStreamerMediaExtraction() {
const url = document.getElementById('streamer-media-test-url')?.value?.trim() || '';
const results = document.getElementById('streamer-media-test-results');
if (!url) return toast('Enter a URL to test', 'error');
-- results.innerHTML = '<p class="muted"><i class="fa-solid fa-spinner fa-spin"></i> Testing live OpenVibe.Live extraction…</p>';
++ results.innerHTML = '<p class="muted"><span class="ov-mark" data-state="busy" data-size="16" data-tone="ink" aria-hidden="true"></span> Testing live OpenVibe.Live extraction…</p>';
try {
const data = await api('/api/admin/streamer/media-tools/test', { method: 'POST', body: { url } });
results.innerHTML = `<div style="display:grid;gap:8px;font-size:0.9rem">${(data.steps || []).map(step => {
ddiff --git a/public/js/admin/content.js b/public/js/admin/content.js
iindex cceb42c5..c7b39c94 100644
---- a/public/js/admin/content.js
++++ b/public/js/admin/content.js
@@@ -64,7 +64,7 @@ async function deleteAllPasteForks() {
// ═══════════════════════════════════════════════════════════════
async function loadStorage() {
const c = document.getElementById('storage-content');
-- c.innerHTML = '<div class="loading"><i class="fa-solid fa-spinner fa-spin"></i> Analyzing storage...</div>';
++ c.innerHTML = '<div class="loading"><span class="ov-mark" data-state="busy" data-size="16" data-tone="ink" aria-hidden="true"></span> Analyzing storage...</div>';
try {
const [data, tierData] = await Promise.all([
api('/api/admin/streamer/storage'),
@@@ -180,7 +180,7 @@ function buildStreamerTierHtml(tierData) {
+ '<thead><tr style="text-align:left;color:var(--text-muted);border-bottom:1px solid var(--border)">'
+ '<th style="padding:6px 10px">Tier</th><th style="padding:6px 10px;text-align:right">VODs</th><th style="padding:6px 10px;text-align:right">VOD Size</th><th style="padding:6px 10px;text-align:right">Share</th><th style="padding:6px 10px;text-align:right">Clips</th><th style="padding:6px 10px">Provider</th></tr></thead>'
+ '<tbody>' + rowHtml
-- + '<tr style="border-top:2px solid var(--border);font-weight:700"><td style="padding:8px 10px">Total</td><td style="padding:8px 10px;text-align:right">' + totalVods.toLocaleString() + '</td><td style="padding:8px 10px;text-align:right">' + fmtBytes(totalBytes) + '</td><td style="padding:8px 10px;text-align:right">100%</td><td style="padding:8px 10px;text-align:right">' + totalClips.toLocaleString() + '</td><td style="padding:8px 10px">' + (tierData.sweepRunning ? '<span style="color:#f59e0b"><i class="fa-solid fa-spinner fa-spin"></i> sweeping</span>' : '<span style="color:var(--text-muted)">idle</span>') + '</td></tr>'
++ + '<tr style="border-top:2px solid var(--border);font-weight:700"><td style="padding:8px 10px">Total</td><td style="padding:8px 10px;text-align:right">' + totalVods.toLocaleString() + '</td><td style="padding:8px 10px;text-align:right">' + fmtBytes(totalBytes) + '</td><td style="padding:8px 10px;text-align:right">100%</td><td style="padding:8px 10px;text-align:right">' + totalClips.toLocaleString() + '</td><td style="padding:8px 10px">' + (tierData.sweepRunning ? '<span style="color:#f59e0b"><span class="ov-mark" data-state="busy" data-size="16" data-tone="ink" aria-hidden="true"></span> sweeping</span>' : '<span style="color:var(--text-muted)">idle</span>') + '</td></tr>'
+ '</tbody></table></div>'
+ '<div style="display:flex;flex-wrap:wrap;gap:8px;font-size:12px;color:var(--text-muted);margin-top:12px">'
+ '<span><i class="fa-solid ' + (ts.enabled ? 'fa-circle-check' : 'fa-circle-xmark') + '" style="color:' + (ts.enabled ? '#22c55e' : '#ef4444') + '"></i> Auto-offload: ' + (ts.enabled ? 'ON' : 'OFF') + '</span>'
@@@ -202,7 +202,7 @@ async function loadStreamerCloudStorage(force) {
const header = '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">'
+ '<h3 style="margin:0"><i class="fa-solid fa-cloud"></i> Cloud storage usage and cost</h3>'
+ '<p class="admin-intro">This estimates storage in Backblaze B2 and Cloudflare R2, in bytes and US dollars per month. Rescan when checking capacity; higher usage means more stored data and may increase charges.</p><button class="btn btn-sm btn-outline" onclick="loadStreamerCloudStorage(true)"><i class="fa-solid fa-rotate"></i> Rescan now</button></div>';
-- el.innerHTML = header + '<div class="loading"><i class="fa-solid fa-spinner fa-spin"></i> Scanning cloud buckets…</div>';
++ el.innerHTML = header + '<div class="loading"><span class="ov-mark" data-state="busy" data-size="16" data-tone="ink" aria-hidden="true"></span> Scanning cloud buckets…</div>';
try {
const data = await api('/api/admin/streamer/storage/buckets' + (force ? '?force=1' : ''));
const usage = data.usage || {};
ddiff --git a/public/js/my/consent.js b/public/js/my/consent.js
iindex 21b09d01..b8093cdb 100644
---- a/public/js/my/consent.js
++++ b/public/js/my/consent.js
@@@ -103,7 +103,7 @@ function consentRow(purpose) {
function renderConsent() {
const box = document.getElementById('consent-choices');
if (!box) return;
-- if (!consentDoc) { box.innerHTML = '<div class="loading-spinner">Loading your choices…</div>'; return; }
++ if (!consentDoc) { box.innerHTML = '<div class="loading-spinner"><span class="ov-mark" data-state="busy" data-size="32" aria-hidden="true"></span></div>'; return; }
// A re-render (a save, or the apps arriving) keeps the folds the person had open.
const open = new Set([...box.querySelectorAll('details[open][data-consent-row]')].map((d) => d.getAttribute('data-consent-row')));
box.innerHTML = (consentDoc.purposes || []).map(consentRow).join('');
@@@ -188,7 +188,7 @@ function renderConsentApps() {
const box = document.getElementById('consent-apps');
if (!box) return;
box.replaceChildren();
-- if (!consentAppsDoc) { box.append(consentNode('div', 'loading-spinner', 'Loading your apps…')); return; }
++ if (!consentAppsDoc) { const wait = consentNode('div', 'loading-spinner', null); wait.innerHTML = '<span class="ov-mark" data-state="busy" data-size="32" aria-hidden="true"></span>'; box.append(wait); return; }
const apps = consentAppsDoc.apps || [];
if (!apps.length) {
box.append(consentNode('p', 'ovk-muted consent-empty', 'No app has asked you for anything yet. An app asks when you sign in to it, and what you allow shows here.'));
ddiff --git a/public/js/my/security.js b/public/js/my/security.js
iindex 447378dc..34346edf 100644
---- a/public/js/my/security.js
++++ b/public/js/my/security.js
@@@ -26,7 +26,7 @@ async function loadEmailStatus() {
} catch (e) { card.style.display = 'none'; }
}
async function resendVerification(btn) {
-- if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Sending…'; }
++ if (btn) { btn.disabled = true; btn.innerHTML = '<span class="ov-mark" data-state="busy" data-size="16" data-tone="ink" aria-hidden="true"></span> Sending…'; }
try { const r = await apiFetch('/api/auth/email/send-verification', { method: 'POST' }); toast(r.message || 'Sent', 'success'); }
catch (e) { toast(e.message, 'error'); }
loadEmailStatus();
@@@ -184,7 +184,7 @@ async function loadHistory(more) {
if (isAnonSession()) { document.getElementById('hist-list').innerHTML = '<div class="hist-empty">History is only kept for signed-in accounts.</div>'; return; }
if (!more) _histOffset = 0;
const listEl = document.getElementById('hist-list');
-- if (!more) listEl.innerHTML = '<div class="loading-spinner">Loading history…</div>';
++ if (!more) listEl.innerHTML = '<div class="loading-spinner"><span class="ov-mark" data-state="busy" data-size="32" aria-hidden="true"></span></div>';
try {
const q = new URLSearchParams({ limit: '40', offset: String(_histOffset) });
if (_histService) q.set('service', _histService);
ddiff --git a/public/my.html b/public/my.html
iindex 1b210580..50c86bed 100644
---- a/public/my.html
++++ b/public/my.html
@@@ -383,7 +383,7 @@
<button class="btn btn-small btn-danger" onclick="clearHistory()" title="Delete the whole timeline"><i class="fa-solid fa-trash"></i> Clear</button>
</div>
<p id="hist-summary" role="status" hidden></p>
-- <div id="hist-list"><div class="loading-spinner">Loading history…</div></div>
++ <div id="hist-list"><div class="loading-spinner"><span class="ov-mark" data-state="busy" data-size="32" aria-hidden="true"></span></div></div>
<div class="btn-row" id="hist-more-row" style="display:none;margin-top:10px"><button class="btn" onclick="loadHistory(true)">Load more</button></div>
</div>
</div>
@@@ -396,12 +396,12 @@
<div class="card" id="consent-card">
<h3><span class="icon"><i class="fa-solid fa-circle-check"></i></span> Your data choices</h3>
<p>AI training, the open dataset, face recognition and apps that use your content are <b>off until you turn them on</b>. Each choice is yours, and you can turn it off again at any time. Nothing here is on by default. We never sell your data.</p>
-- <div id="consent-choices"><div class="loading-spinner">Loading your choices…</div></div>
++ <div id="consent-choices"><div class="loading-spinner"><span class="ov-mark" data-state="busy" data-size="32" aria-hidden="true"></span></div></div>
<!-- Apps you allowed (C4, 2026-11-07): what each app asked for when you signed in to it (consent:<purpose>)
and was given, one Turn off per purpose, and what you said no to in the last 30 days
(GET /api/v1/me/consent/apps). -->
<h4 class="consent-apps-title">Apps you allowed</h4>
-- <div id="consent-apps"><div class="loading-spinner">Loading your apps…</div></div>
++ <div id="consent-apps"><div class="loading-spinner"><span class="ov-mark" data-state="busy" data-size="32" aria-hidden="true"></span></div></div>
<p class="muted consent-status" id="consent-status" role="status"></p>
</div>
<!-- What you see (L1, 2026-11-02): one row per content label, three equal choices, saved at once. The web default
@@@ -426,7 +426,7 @@
<div class="card">
<h3><span class="icon"><i class="fa-solid fa-database"></i></span> Your data on OpenVibe</h3>
<p style="margin-bottom:12px">Settings and summaries OpenVibe sites keep with your account: your chat settings, favourite tools, streaming and community summaries and more. The sites themselves keep the full records. You can download all of this, or delete any of it. A site that writes a summary may write it again the next time you use that site.</p>
-- <div id="modules-list"><div class="loading-spinner">Loading…</div></div>
++ <div id="modules-list"><div class="loading-spinner"><span class="ov-mark" data-state="busy" data-size="32" aria-hidden="true"></span></div></div>
<div class="btn-row"><button class="btn" type="button" onclick="exportModules()"><i class="fa-solid fa-download"></i> Download as JSON</button></div>
</div>
</div>
@@@ -531,7 +531,7 @@
</div>
<div class="card">
<h3><span class="icon"><i class="fa-solid fa-mobile-screen"></i></span> Active Sessions</h3>
-- <div id="sessions-list"><div class="loading-spinner">Loading sessions...</div></div>
++ <div id="sessions-list"><div class="loading-spinner"><span class="ov-mark" data-state="busy" data-size="32" aria-hidden="true"></span></div></div>
<p style="margin-top:12px;color:var(--text-secondary);font-size:13px">Lost a phone or used a shared computer? Signing out ends every sign-in on every OpenVibe site at once, and open chats and games disconnect.</p>
<div class="btn-row" style="margin-top:10px">
<button class="btn btn-danger" onclick="revokeAllSessions()"><i class="fa-solid fa-mobile-screen-button"></i> Sign out other devices</button>
@@@ -541,7 +541,7 @@
<div class="card">
<h3><span class="icon"><i class="fa-solid fa-plug"></i></span> Signed-in apps</h3>
<p style="margin-bottom:12px;color:var(--text-secondary);font-size:13px">Extensions and apps that hold their own sign-in. Revoking one ends its refresh token; the next request asks you to sign in again.</p>
-- <div id="apps-list"><div class="loading-spinner">Loading apps...</div></div>
++ <div id="apps-list"><div class="loading-spinner"><span class="ov-mark" data-state="busy" data-size="32" aria-hidden="true"></span></div></div>
</div>
<div class="card" id="blocks-card">
<h3><span class="icon"><i class="fa-solid fa-user-slash"></i></span> Blocked people</h3>
@@@ -550,7 +550,7 @@
<div class="form-field"><label for="block-username">Block someone</label><input id="block-username" name="username" maxlength="64" autocomplete="off" placeholder="username"></div>
<div class="btn-row"><button class="btn btn-danger" type="submit">Block</button></div>
</form>
-- <div id="blocks-list"><div class="loading-spinner">Loading…</div></div>
++ <div id="blocks-list"><div class="loading-spinner"><span class="ov-mark" data-state="busy" data-size="32" aria-hidden="true"></span></div></div>
<p class="data-status" id="blocks-status" role="status"></p>
</div>
</div>
@@@ -626,6 +626,7 @@
<!-- Shared scripts -->
<script src="/shared/theme-loader.js" defer></script>
++<script src="/shared/ov-mark.js" defer></script>
<script src="/shared/navbar.js" defer></script>
<script src="/js/account-safety.js" defer></script>
<script src="/shared/notification-ui.js" defer></script>