diff --git a/Cargo.lock b/Cargo.lock index c7229e8..0368c06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2746,7 +2746,7 @@ dependencies = [ [[package]] name = "itsgoin-desktop" -version = "0.4.2" +version = "0.4.4" dependencies = [ "anyhow", "base64 0.22.1", diff --git a/crates/tauri-app/Cargo.toml b/crates/tauri-app/Cargo.toml index 567c027..eb4fe09 100644 --- a/crates/tauri-app/Cargo.toml +++ b/crates/tauri-app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "itsgoin-desktop" -version = "0.4.3" +version = "0.4.4" edition = "2021" [lib] diff --git a/crates/tauri-app/tauri.conf.json b/crates/tauri-app/tauri.conf.json index 101faf6..444d033 100644 --- a/crates/tauri-app/tauri.conf.json +++ b/crates/tauri-app/tauri.conf.json @@ -1,6 +1,6 @@ { "productName": "itsgoin", - "version": "0.4.3", + "version": "0.4.4", "identifier": "com.itsgoin.app", "build": { "frontendDist": "../../frontend", diff --git a/frontend/app.js b/frontend/app.js index 9937253..37005e3 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -39,8 +39,8 @@ const dmRecipientSelect = $('#dm-recipient-select'); const dmContent = $('#dm-content'); const dmSendBtn = $('#dm-send-btn'); const anchorsList = $('#anchors-list'); -const anchorAddSelect = $('#anchor-add-select'); -const anchorAddBtn = $('#anchor-add-btn'); +const anchorAddSelect = null; // removed — anchors are read-only +const anchorAddBtn = null; const attachBtn = $('#attach-btn'); const fileInput = $('#file-input'); const attachmentPreview = $('#attachment-preview'); @@ -1818,7 +1818,7 @@ async function doRemoveAnchor(nid) { } } -anchorAddBtn.addEventListener('click', doAddAnchor); +if (anchorAddBtn) anchorAddBtn.addEventListener('click', doAddAnchor); async function loadKnownAnchors() { const container = $('#known-anchors-list'); @@ -2819,7 +2819,8 @@ document.querySelectorAll('.tab').forEach(tab => { tab.addEventListener('click', () => { if (tab.dataset.tab === currentTab) return; // Close any open lightboxes/overlays/popovers - document.querySelectorAll('.image-lightbox, .download-prompt-overlay, .offline-lightbox, .overlay').forEach(el => el.remove()); + document.querySelectorAll('.image-lightbox, .download-prompt-overlay, .offline-lightbox').forEach(el => el.remove()); + closePopover(); // hide the persistent popover overlay (don't remove it) document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); const oldView = document.querySelector('.view.active'); if (oldView) { @@ -2862,6 +2863,94 @@ document.querySelectorAll('.tab').forEach(tab => { }); // --- Collapsible section toggles --- +$('#profile-lightbox-btn').addEventListener('click', () => { + const overlay = document.createElement('div'); + overlay.className = 'image-lightbox'; + overlay.style.cursor = 'default'; + // Pre-fill from the settings fields + const currentName = profileNameInput.value || ''; + const currentBio = profileBioInput.value || ''; + const currentVisible = $('#public-visible-check')?.checked ?? true; + overlay.innerHTML = ` +
+

Profiles

+

Default Profile

+ + + + + +
+ +
+
+

Circle Profiles

+

Set a different name/bio for each circle you manage.

+
+
+ +
+
`; + document.body.appendChild(overlay); + overlay.querySelector('#lb-profile-save').addEventListener('click', async () => { + const name = overlay.querySelector('#lb-profile-name').value.trim(); + const bio = overlay.querySelector('#lb-profile-bio').value.trim(); + if (!name) { toast('Display name is required'); return; } + try { + await invoke('set_profile', { name, bio }); + const visible = overlay.querySelector('#lb-public-visible').checked; + await invoke('set_public_visible', { visible }); + // Sync back to settings fields + profileNameInput.value = name; + profileBioInput.value = bio; + if ($('#public-visible-check')) $('#public-visible-check').checked = visible; + toast('Profile saved!'); + loadNodeInfo(); + overlay.remove(); + } catch (e) { toast('Error: ' + e); } + }); + overlay.querySelector('#lb-profile-close').addEventListener('click', () => overlay.remove()); + overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); + // Populate circle profiles list + const srcList = $('#circle-profiles-list'); + const lbList = overlay.querySelector('#lb-circle-profiles-list'); + if (srcList && lbList) lbList.innerHTML = srcList.innerHTML; +}); + +$('#redundancy-lightbox-btn').addEventListener('click', async () => { + const overlay = document.createElement('div'); + overlay.className = 'image-lightbox'; + overlay.style.cursor = 'default'; + overlay.innerHTML = ` +
+

Redundancy

+

Loading...

+
+ +
+
`; + document.body.appendChild(overlay); + // Load redundancy data + try { + const r = await invoke('get_redundancy_info'); + const zeroClass = r.zeroReplicas > 0 ? 'warn' : 'ok'; + const oneClass = r.oneReplica > 0 ? '' : 'ok'; + overlay.querySelector('#lb-redundancy-panel').innerHTML = `
+
${r.zeroReplicas}
Unreplicated
+
${r.oneReplica}
1 replica
+
${r.twoPlusReplicas}
2+ replicas
+
${r.total}
Total posts
+
`; + } catch (_) { + overlay.querySelector('#lb-redundancy-panel').innerHTML = '

Could not load redundancy info

'; + } + overlay.querySelector('#lb-redundancy-close').addEventListener('click', () => overlay.remove()); + overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); +}); + $('#circles-toggle').addEventListener('click', () => { const body = $('#circles-body'); body.classList.toggle('hidden'); @@ -2885,20 +2974,30 @@ $('#anchors-toggle').addEventListener('click', () => { } }); -$('#diagnostics-btn').addEventListener('click', () => { +function openDiagnostics() { const diagHtml = `
-
+
+ +
+
-
- +
+ + +
+

Activity Log

`; openPopover('Network Diagnostics', diagHtml, { @@ -2920,6 +3019,28 @@ $('#diagnostics-btn').addEventListener('click', () => { btn.textContent = 'Show Connections'; } }); + // Wire anchors toggle + $('#show-anchors-btn').addEventListener('click', async () => { + const section = $('#anchors-section'); + const btn = $('#show-anchors-btn'); + if (section.classList.contains('hidden')) { + section.classList.remove('hidden'); + btn.textContent = 'Hide Anchors'; + try { + const anchors = await invoke('list_known_anchors'); + const list = $('#diag-known-anchors-list'); + if (list) list.innerHTML = anchors.length ? anchors.map(a => { + const icon = generateIdenticon(a.nodeId, 18); + const label = escapeHtml(peerLabel(a.nodeId, a.displayName)); + const addr = a.addresses.length > 0 ? `${escapeHtml(a.addresses[0])}` : ''; + return `
${icon} ${label}${addr}
`; + }).join('') : '

No discovered anchors

'; + } catch (_) {} + } else { + section.classList.add('hidden'); + btn.textContent = 'Stored Anchors'; + } + }); // Wire action buttons $('#diag-refresh-btn').addEventListener('click', async () => { const btn = $('#diag-refresh-btn'); @@ -2928,6 +3049,19 @@ $('#diagnostics-btn').addEventListener('click', () => { catch (e) { toast('Error: ' + e); } finally { btn.disabled = false; btn.textContent = 'Refresh'; } }); + $('#diag-sync-btn').addEventListener('click', async () => { + const btn = $('#diag-sync-btn'); + btn.disabled = true; btn.textContent = 'Syncing...'; + try { + const result = await invoke('sync_all'); + toast(result); + loadFeed(true); + if (currentTab === 'myposts') loadMyPosts(true); + if (currentTab === 'people') loadFollows(); + if (currentTab === 'messages') loadMessages(true); + } catch (e) { toast('Sync error: ' + e); } + finally { btn.disabled = false; btn.textContent = 'Sync All'; } + }); $('#rebalance-btn').addEventListener('click', async () => { const btn = $('#rebalance-btn'); btn.disabled = true; btn.textContent = 'Rebalancing...'; @@ -2953,7 +3087,9 @@ $('#diagnostics-btn').addEventListener('click', () => { activityInterval = setInterval(loadActivityLog, 3000); } }); -}); +} +$('#diagnostics-btn').addEventListener('click', openDiagnostics); +$('#net-indicator').addEventListener('click', openDiagnostics); // --- Event handlers --- postBtn.addEventListener('click', doPost); diff --git a/frontend/index.html b/frontend/index.html index 9cf898b..46ccff2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,7 @@ - + ItsGoin @@ -26,9 +26,6 @@
- - -
+ +
@@ -60,13 +59,25 @@
+
+ + + + +
+
-
@@ -172,58 +172,26 @@
-
-

Profile

-
- - - - - - -
+ +
-
- - -
- -
- -
- -
- - -
- -
- + +
@@ -252,16 +220,11 @@
-
-

Redundancy

-
-

Loading...

-
-
+ + -
- -
+ +

Danger Zone

diff --git a/frontend/style.css b/frontend/style.css index 8b2a718..d282bb2 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -1,14 +1,17 @@ * { box-sizing: border-box; margin: 0; padding: 0; } html { font-size: clamp(14px, 1.5vw, 24px); } select option { color: #000 !important; } -body { font-family: system-ui, sans-serif; max-width: clamp(640px, 80vw, 1600px); margin: 0 auto; padding: clamp(0.5rem, 2vw, 2rem); background: #1a1a2e; color: #e0e0e0; color-scheme: dark; } -header { border-bottom: 1px solid #333; padding-bottom: 0.5rem; margin-bottom: 1rem; } -#header-row { display: flex; justify-content: space-between; align-items: center; } +body { font-family: system-ui, sans-serif; margin: 0; padding: 0; background: #0a0a1a; color: #e0e0e0; color-scheme: dark; } +header { position: sticky; top: 0; z-index: 800; background: #0a0a1a; border-bottom: none; padding: 0.5rem clamp(0.5rem, 2vw, 2rem) 0; margin-bottom: 0; } +main { max-width: clamp(640px, 80vw, 1600px); margin: 0 auto; padding: 0 clamp(0.5rem, 2vw, 2rem); } +header::after { content: ''; position: absolute; bottom: -15px; left: 0; right: 0; height: 15px; background: linear-gradient(to bottom, #0a0a1a, transparent); pointer-events: none; z-index: 799; } +#header-row { display: flex; justify-content: space-between; align-items: center; max-width: clamp(640px, 80vw, 1600px); margin: 0 auto; } +#tabs { max-width: clamp(640px, 80vw, 1600px); margin: 0 auto; } header h1 { font-size: clamp(1.4rem, 2.5vw, 2rem); color: #7fdbca; margin: 0; } #status-ticker { flex: 1; text-align: center; font-size: 0.7rem; color: #888; line-height: 1.3; max-height: 2.4em; overflow: hidden; transition: opacity 0.3s; } #status-ticker.faded { opacity: 0; } -#net-indicator { display: flex; align-items: center; gap: 0.4rem; } -#net-dot { width: 10px; height: 10px; border-radius: 50%; background: #222; border: 1px solid #444; } +#net-indicator { display: flex; align-items: center; gap: 0.4rem; cursor: pointer; } +#net-dot { width: 10px; height: 15px; border-radius: 50%; background: #222; border: 1px solid #444; } #net-dot.net-black { background: #222; } #net-dot.net-red { background: #e74c3c; } #net-dot.net-yellow { background: #f1c40f; } @@ -82,21 +85,23 @@ header h1 { font-size: clamp(1.4rem, 2.5vw, 2rem); color: #7fdbca; margin: 0; } .compose-left { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; } /* Tabs — desktop (top bar) */ -#tabs { display: flex; gap: 0; margin-bottom: 1rem; border-bottom: 1px solid #333; } -.tab { background: none; border: none; color: #99a; padding: 0.5rem 0.6rem; cursor: pointer; border-bottom: 2px solid transparent; font-size: 0.82rem; transition: color 0.15s, border-color 0.15s; position: relative; flex: 1; text-align: center; white-space: nowrap; } +#tabs { display: flex; gap: 0; margin-bottom: 1rem; border-bottom: none; } +.tab { background: none; border: none; color: #99a; padding: 0.5rem 0.6rem; cursor: pointer; border-bottom: 2px solid #0a0a1a; font-size: 0.82rem; transition: color 0.15s, border-color 0.15s; position: relative; flex: 1; text-align: center; white-space: nowrap; } .tab:hover { color: #ccd; } .tab.active { color: #7fdbca; border-bottom-color: #7fdbca; } -.tab-icon { display: none; } +.tab-icon { display: inline; margin-right: 0.3rem; } .tab-badge { display: inline-flex; align-items: center; justify-content: center; background: #0f3460; color: #7fdbca; font-size: 0.6rem; min-width: 1.1rem; height: 1.1rem; border-radius: 0.55rem; padding: 0 0.3rem; margin-left: 0.25rem; font-family: system-ui, sans-serif; vertical-align: middle; } /* Tabs — mobile/tablet (bottom nav bar) */ @media (max-width: 768px) { - #tabs { position: fixed; bottom: 0; left: 0; right: 0; z-index: 900; background: #0a0a1a; border-bottom: none; border-top: 1px solid #333; margin-bottom: 0; padding: 0; padding-bottom: env(safe-area-inset-bottom, 0); } + #tabs { position: fixed; bottom: 0; top: auto; left: 0; right: 0; z-index: 900; background: #0a0a1a; border-bottom: none; border-top: none; margin-bottom: 0; padding: 0; padding-bottom: env(safe-area-inset-bottom, 0); } + #tabs::before { content: ''; position: absolute; top: -15px; left: 0; right: 0; height: 15px; background: linear-gradient(to top, #0a0a1a, transparent); pointer-events: none; } .tab { flex-direction: column; align-items: center; padding: 0.4rem 0.2rem 0.3rem; border-bottom: none; border-top: 2px solid transparent; font-size: 0.6rem; gap: 0.15rem; display: flex; } .tab.active { border-bottom: none; border-top-color: #7fdbca; } .tab-icon { display: block; font-size: 1.2rem; line-height: 1; } .tab-badge { position: absolute; top: 0.1rem; right: 0.2rem; margin-left: 0; font-size: 0.5rem; min-width: 0.9rem; height: 0.9rem; border-radius: 0.45rem; } - main { padding-bottom: 4rem; } + header { position: fixed; top: 0; left: 0; right: 0; z-index: 800; padding: 0.4rem clamp(0.5rem, 2vw, 2rem); padding-top: calc(0.4rem + env(safe-area-inset-top, 0px)); } + main { padding-top: calc(3rem + env(safe-area-inset-top, 0px)); padding-bottom: 4rem; } .toast { bottom: 4.5rem; } } @@ -118,7 +123,7 @@ header h1 { font-size: clamp(1.4rem, 2.5vw, 2rem); color: #7fdbca; margin: 0; } .subsection-title { font-size: 0.85rem; color: #aab; margin: 0.5rem 0 0.35rem; font-weight: 600; } /* Collapsible section toggle buttons */ -.section-toggle { width: 100%; text-align: left; } +.section-toggle { width: auto; text-align: center; } /* Input rows */ .input-row { display: flex; gap: 0.4rem; } diff --git a/pic2.png b/pic2.png new file mode 100644 index 0000000..7f07615 Binary files /dev/null and b/pic2.png differ diff --git a/website/design.html b/website/design.html index ab98e3f..edfa8a1 100644 --- a/website/design.html +++ b/website/design.html @@ -44,7 +44,8 @@

This is the canonical technical reference for ItsGoin. It describes the vision, the architecture, and the current state of every subsystem — with full implementation detail. This document is versioned; each update records what changed.

Changelog -

v0.4.3 (2026-03-22): Lock contention overhaul — all conn_mgr lock holds during network I/O eliminated. PostFetch, TcpPunch, PullFromPeer, FetchEngagement, ResolveAddress, AnchorProbe, WormLookup, ContentSearch now use brief locks for data gathering only. Bi-stream handlers (BlobRequest, WormQuery, RelayIntroduce, PostFetchRequest, ManifestRefresh) fully lock-free for I/O. ConnectionActor hoists shared Arcs (storage, blob_store, endpoint) for lock-free access. ResolveAddress adds 5s per-query timeout (was unbounded). Worm cascade uses connection snapshots. Initial exchange failure now aborts mesh upgrade (was silently continuing). connect_to_peer/connect_to_anchor use 15s timeout. StoragePool — 8 concurrent SQLite connections in WAL mode replace single Mutex<Storage>. Reads run fully parallel; writes serialize only at SQLite level. Bottom nav bar for mobile/tablet (≤768px) with icon tabs. Text sizes: XS 75%, S 100%, M 125% (default), L 150%, XL 200%. Text size persisted to localStorage for instant restore. Fix: blocking_lock panic inside async runtime (prevented app startup). StoragePool reduced to 4 connections for Android compatibility. Keepalive fix — tokio::time::sleep inside select! was resetting every loop iteration, keepalives never fired; switched to tokio::time::interval. Auto-reconnect on unexpected disconnect — 3s delay then direct reconnect to last known address; falls back to growth loop. notify_growth on disconnect — immediately signals growth loop to fill empty slot instead of waiting 10min rebalance. Tab badge fix — updateTabBadge was using textContent which destroyed icon+label spans; now updates only the label and manages badge span separately. Feed re-render skip during media playback — prevents video echo from DOM destruction.

+

v0.4.4 (2026-03-23): UI overhaul — sticky header with tabs as one floating block on desktop, fixed header+bottom nav on mobile. Full-width dark header (#0a0a1a) edge-to-edge with 15px fade gradient into content. Tab icons visible on desktop (inline) and mobile (stacked). Safe area inset support for phone notches/camera cutouts. Lightbox close on tab switch. Profiles lightbox (name, bio, visibility, circle profiles) moved from settings to My Posts. Redundancy lightbox moved from settings to My Posts. Sync All and Stored Anchors moved into Network Diagnostics popover. Network indicator click opens diagnostics. Diagnostics buttons centered in rows. Settings streamlined — removed profile editor, diagnostics button, sync button, redundancy panel, anchor management. Attach button centered in compose. Manage Circles released from full-width constraint.

+

v0.4.3 (2026-03-22): Lock contention overhaul — all conn_mgr lock holds during network I/O eliminated. PostFetch, TcpPunch, PullFromPeer, FetchEngagement, ResolveAddress, AnchorProbe, WormLookup, ContentSearch now use brief locks for data gathering only. Bi-stream handlers (BlobRequest, WormQuery, RelayIntroduce, PostFetchRequest, ManifestRefresh) fully lock-free for I/O. ConnectionActor hoists shared Arcs (storage, blob_store, endpoint) for lock-free access. ResolveAddress adds 5s per-query timeout (was unbounded). Worm cascade uses connection snapshots. Initial exchange failure now aborts mesh upgrade (was silently continuing). connect_to_peer/connect_to_anchor use 15s timeout. StoragePool — 8 concurrent SQLite connections in WAL mode replace single Mutex<Storage>. Reads run fully parallel; writes serialize only at SQLite level. Bottom nav bar for mobile/tablet (≤768px) with icon tabs. Text sizes: XS 75%, S 100%, M 125% (default), L 150%, XL 200%. Text size persisted to localStorage for instant restore. Fix: blocking_lock panic inside async runtime (prevented app startup). StoragePool reduced to 4 connections for Android compatibility. Keepalive fix — tokio::time::sleep inside select! was resetting every loop iteration, keepalives never fired; switched to tokio::time::interval. Auto-reconnect on unexpected disconnect — 3s delay then direct reconnect to last known address; falls back to growth loop. notify_growth on disconnect — immediately signals growth loop to fill empty slot instead of waiting 10min rebalance. Tab badge fix — updateTabBadge was using textContent which destroyed icon+label spans; now updates only the label and manages badge span separately. Feed re-render skip during media playback — prevents video echo from DOM destruction.

v0.4.2 (2026-03-22): Welcome screen — startup shows “How’s it goin?” with staggered counters (connections, posts, messages, reacts, comments) while backend bootstraps. Status ticker — header ticker for new posts, messages, reactions, comments, connection changes. Notification improvements — Tauri plugin → Web Notification → notify-rust fallback chain, Linux native notifications. Responsive text scaling — Small/Normal/Large (100%/150%/200%), persisted via settings. Diagnostics popover — diagnostics moved from inline section to overlay, connections on-demand, timers removed. Share details lightbox with QR code. Connect string prefers external address (UPnP/public IPv6/observed). Stale N1 fix — disconnected social routes excluded from N1 share. Replication handler fix — actively fetches posts + blobs from requester after accepting replication. Hole punch fix — target-side registers publicly routable remote address for relay introduction. Replication semaphore (3 concurrent max). Peer labels show truncated node ID.

v0.4.1 (2026-03-21): Security hardening — reaction signatures (ed25519), comment signature verification on receipt, reaction removal authorization, BlobHeader author verification. Lock contention fixes — ManifestPush discovery (cm lock released during I/O), pull request handler (filter without lock), pull sender (split into brief locks), engagement checker (batch writes per chunk). Data cleanup — post deletion cleans downstream/upstream/seen tables.

v0.4.0 (2026-03-21): Protocol v4 — header-driven sync. ManifestPush as primary post notification. Slim PullSyncRequest (per-author timestamps, not full post ID list). Tiered engagement checks (5min/1hr/4hr/24hr by content age). Multi-upstream (3 max) with fallback chain. Auto-prefetch followed authors <90d. Self Last Encounter per-author tracking. Encrypted-but-not-for-us CDN caching. Serial engagement polling. ~90% bandwidth reduction for established nodes.

diff --git a/website/download.html b/website/download.html index 1a279ca..7c629e9 100644 --- a/website/download.html +++ b/website/download.html @@ -25,16 +25,16 @@

Download ItsGoin

Available for Android and Linux. Free and open source.

-

Version 0.4.3 — March 22, 2026

+

Version 0.4.4 — March 23, 2026

@@ -46,7 +46,7 @@

Android

  1. Download the APK — Tap the button above. Your browser may warn that this type of file can be harmful — tap Download anyway.
  2. -
  3. Open the file — When the download finishes, tap the notification or find itsgoin-0.4.3.apk in your Downloads folder and tap it.
  4. +
  5. Open the file — When the download finishes, tap the notification or find itsgoin-0.4.4.apk in your Downloads folder and tap it.
  6. Allow installation — Android will ask you to allow installs from this source. Tap Settings, toggle "Allow from this source", then go back and tap Install.
  7. Launch the app — Once installed, tap Open or find ItsGoin in your app drawer.
@@ -59,8 +59,8 @@

Linux (AppImage)

  1. Download the AppImage — Click the button above to download.
  2. -
  3. Make it executable — Open a terminal and run:
    chmod +x itsgoin_0.4.3_amd64.AppImage
  4. -
  5. Run it — Double-click the file, or from the terminal:
    ./itsgoin_0.4.3_amd64.AppImage
  6. +
  7. Make it executable — Open a terminal and run:
    chmod +x itsgoin_0.4.4_amd64.AppImage
  8. +
  9. Run it — Double-click the file, or from the terminal:
    ./itsgoin_0.4.4_amd64.AppImage
Note: If it doesn't launch, you may need to install FUSE:
sudo apt install libfuse2 (Debian/Ubuntu) or sudo dnf install fuse (Fedora). @@ -71,6 +71,18 @@

Changelog

+
v0.4.4 — March 23, 2026
+
    +
  • UI overhaul — Sticky header with tabs as one floating block on desktop. Fixed header + bottom nav on mobile. Full-width dark header with 15px fade gradient into content.
  • +
  • Tab icons — Icons visible on both desktop (inline with text) and mobile (stacked above text).
  • +
  • Safe area insets — Header pads below phone notch/camera cutout. Bottom nav pads above home indicator.
  • +
  • Profiles lightbox — Name, bio, visibility, and circle profiles now in a lightbox from My Posts instead of settings.
  • +
  • Redundancy lightbox — Post replication stats in a lightbox from My Posts.
  • +
  • Diagnostics reorganized — Sync All and Stored Anchors moved into Network Diagnostics popover. Opened by clicking the network indicator. Buttons centered in rows.
  • +
  • Settings streamlined — Removed profile editor, diagnostics button, sync button, redundancy panel, and anchor management from settings.
  • +
  • Lightbox close on tab switch — Any open lightbox/overlay closes when switching tabs.
  • +
+
v0.4.3 — March 22, 2026
  • Lock contention overhaul — All conn_mgr lock holds during network I/O eliminated across 14 handlers. Brief locks for data gathering only; all network operations run lock-free.