itsgoin/frontend/index.html
Scott Reimers 481e1c8435 Network-wide announcements signed by the bootstrap anchor posting id
New primitive: `VisibilityIntent::Announcement`, a public post whose
author MUST be the hardcoded bootstrap anchor posting identity
(`DEFAULT_ANCHOR_POSTING_ID`) and whose content carries an ed25519
signature by that key. Forged announcements (any other author, or
bad signature) are rejected by `control::receive_post` before storage
— they never enter the DB and never propagate via neighbor-manifest
diffs. Only the real anchor can publish announcements, and it does so
sparingly as part of the release deploy flow.

Uses release announcements to drive an in-app upgrade banner:
- Anchor publishes a signed `{category:release, version, channel,
  download_url, ...}` post during every deploy.
- Clients receive it via the normal CDN; `apply_announcement_if_applicable`
  stores the latest-per-category/channel in the settings kv, keyed
  e.g. `announcement:release:stable`.
- Welcome screen checks storage on startup; if the stored release
  version > CARGO_PKG_VERSION on the user's selected channel, a banner
  appears with a Download button that opens the system browser.
- Settings gets "Updates" section with Stable / Beta radio + Check-now
  button + current status line.

Core:
- `DEFAULT_ANCHOR_POSTING_ID: NodeId` constant (32 bytes, the anchor's
  current posting id — `17af141956ae...`).
- New `VisibilityIntent::Announcement` variant; feed filters in all 6
  `get_feed*` / `list_posts*` query sites updated to also exclude the
  new intent AND the pre-existing `GroupKeyDistribute` intent.
- `types::AnnouncementContent` + `ReleaseAnnouncement` structs.
- `crypto::{sign,verify}_announcement` — length-prefixed field digest
  with a "has release" 1-byte flag.
- New `announcement` module with `verify_announcement_post`,
  `apply_announcement_if_applicable`, `latest_release`,
  `build_announcement_post`, and a `StoredAnnouncement` envelope saved
  to settings so the UI can render without a full post scan.
- `Node::publish_announcement` refuses to run unless the default posting
  id equals the anchor constant — accidental use on client installs
  fails loud.

Wire / receive:
- `control::receive_post` verifies announcement signatures upfront
  alongside Control and Profile. Same pattern; same guarantees.

CLI one-shots (no daemon):
- `itsgoin <data_dir> --print-identity` — prints network_id +
  default_posting_id, exits.
- `itsgoin <data_dir> --announce --ann-category release
  --ann-channel stable --ann-version X --ann-title ... --ann-body ...
  --ann-url https://itsgoin.com/download.html` — builds + stores +
  propagates the signed post, exits.

deploy.sh:
- Now runs the announce one-shot inside the anchor-restart window
  (after binary swap, before start). The DB is free during that gap,
  so the one-shot can write without conflicting with the running
  daemon. The restarted daemon loads all storage on boot and serves
  the new announcement to pulling peers.

Tauri IPC:
- `check_release_announcement(channel)` → Option<ReleaseAnnouncementDto>
  — returns None when up-to-date.
- `get_update_channel` / `set_update_channel(channel)` — persists in
  settings kv key `ui_update_channel`; defaults to stable.
- `open_url_external(url)` — desktop-only (xdg-open / open / cmd start);
  refuses non-http(s) URLs. Android needs the opener plugin — TODO.

Frontend:
- Upgrade banner on the welcome screen, populated by
  `loadUpgradeBanner()`. Hidden when no newer release is known.
- Settings → Updates section with Stable/Beta radio + Check-now button
  + current status line.

Tests: announcement signature roundtrip; non-anchor author rejection;
non-announcement intent is a no-op. 124 / 124 core tests pass.
2026-04-23 01:50:12 -04:00

291 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>ItsGoin</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- First-run: set display name (optional — can be left blank) -->
<div id="setup-overlay" class="overlay hidden">
<div class="overlay-box">
<h2>Welcome to ItsGoin</h2>
<p>Pick a display name if you want one &mdash; or leave blank to stay anonymous.</p>
<input id="setup-name" type="text" placeholder="Display name (optional)" maxlength="50" autofocus />
<button id="setup-btn" class="btn btn-primary">Continue</button>
</div>
</div>
<header>
<div id="header-row">
<h1>ItsGoin</h1>
<div id="status-ticker"></div>
<div id="net-indicator">
<span id="net-dot"></span>
<span id="net-labels"></span>
</div>
</div>
<nav id="tabs">
<button class="tab" data-tab="feed"><span class="tab-icon">&#x1f4f0;</span><span class="tab-label">Feed</span></button>
<button class="tab" data-tab="myposts"><span class="tab-icon">&#x270d;</span><span class="tab-label">My Posts</span></button>
<button class="tab" data-tab="people"><span class="tab-icon">&#x1f465;</span><span class="tab-label">People</span></button>
<button class="tab" data-tab="messages"><span class="tab-icon">&#x1f4ac;</span><span class="tab-label">Messages</span></button>
<button class="tab" data-tab="settings"><span class="tab-icon">&#x2699;</span><span class="tab-label">Settings</span></button>
</nav>
</header>
<main>
<!-- Welcome (shown on startup) -->
<section id="view-welcome" class="view active">
<div style="text-align:center;padding:2rem 1rem">
<h2 style="color:#7fdbca;margin-bottom:0.25rem">Welcome back!</h2>
<p style="color:#e0e0e0;font-size:1.1rem;margin-bottom:0.5rem">How's it goin?</p>
<!-- Release upgrade banner: populated by loadUpgradeBanner() when a
newer version is announced on the user's selected update channel. -->
<div id="upgrade-banner" class="hidden" style="max-width:360px;margin:0 auto 1rem;padding:0.8rem 1rem;border:1px solid #7fdbca;border-radius:8px;background:#0f2a26;text-align:left;font-size:0.9rem">
<div id="upgrade-banner-title" style="color:#7fdbca;font-weight:600;margin-bottom:0.3rem"></div>
<div id="upgrade-banner-body" style="color:#c0d0c0;font-size:0.82rem;margin-bottom:0.6rem"></div>
<button id="upgrade-banner-btn" style="padding:0.5rem 1rem;border:none;border-radius:6px;background:#7fdbca;color:#0a1a18;font-weight:600;cursor:pointer;width:100%">Download upgrade</button>
</div>
<p style="color:#666;font-size:0.8rem;margin-bottom:1.5rem">Connecting and getting updates usually takes a couple minutes.<br>New things we've found so far:</p>
<div id="welcome-counts" style="display:flex;flex-wrap:wrap;gap:1rem;justify-content:center;color:#888;font-size:0.85rem">
<div><span id="welcome-connections" style="font-size:1.4rem;font-weight:700;color:#5b8def;display:block">-</span>Connections</div>
<div><span id="welcome-posts" style="font-size:1.4rem;font-weight:700;color:#5b8def;display:block">-</span>New Posts</div>
<div><span id="welcome-messages" style="font-size:1.4rem;font-weight:700;color:#5b8def;display:block">-</span>Messages</div>
<div><span id="welcome-reacts" style="font-size:1.4rem;font-weight:700;color:#5b8def;display:block">-</span>Reacts</div>
<div><span id="welcome-comments" style="font-size:1.4rem;font-weight:700;color:#5b8def;display:block">-</span>Comments</div>
</div>
<div style="margin-top:2rem;max-width:280px;margin-left:auto;margin-right:auto">
<div style="background:#1a1a2e;border-radius:6px;height:6px;overflow:hidden;margin-bottom:0.75rem">
<div id="welcome-ready-bar" style="height:100%;background:#7fdbca;width:0%;transition:width 0.5s ease;border-radius:6px"></div>
</div>
<button id="welcome-ready-btn" disabled style="width:100%;padding:0.75rem 1.5rem;border:1px solid #333;border-radius:8px;background:#1a1a2e;color:#666;font-size:0.95rem;cursor:not-allowed;opacity:0.5;transition:all 0.3s ease">Loading...</button>
</div>
</div>
</section>
<!-- Feed tab -->
<section id="view-feed" class="view">
<div id="persona-filter-row" class="hidden" style="display:flex;gap:0.4rem;flex-wrap:wrap;padding:0.3rem 0.5rem;border-bottom:1px solid #222;overflow-x:auto"></div>
<div id="feed-list"></div>
</section>
<!-- My Posts tab -->
<section id="view-myposts" class="view">
<div class="section-card" style="text-align:center;margin-bottom:0.5rem">
<button id="profile-lightbox-btn" class="btn btn-ghost btn-sm">Profiles</button>
<button id="circles-toggle" class="btn btn-ghost btn-sm section-toggle">Manage Circles</button>
<button id="redundancy-lightbox-btn" class="btn btn-ghost btn-sm">Redundancy</button>
<div id="circles-body" class="hidden">
<div class="input-row" style="margin-top:0.5rem">
<input id="circle-name-input" placeholder="New circle name" />
<button id="create-circle-btn" class="btn btn-primary">Create</button>
</div>
<div id="circles-list"></div>
</div>
</div>
<div id="compose">
<textarea id="post-content" placeholder="How's it goin?"></textarea>
<div id="attachment-preview"></div>
<input type="file" id="file-input" multiple class="hidden" />
<div style="text-align:center;margin:0.3rem 0"><button id="attach-btn" class="btn btn-ghost btn-sm" title="Attach images or videos">Attach</button></div>
<div id="compose-footer">
<div class="compose-left">
<span class="hint">Ctrl+Enter to post</span>
<div id="visibility-row">
<select id="persona-select" title="Post as" class="hidden"></select>
<select id="visibility-select">
<option value="public">Public</option>
<option value="friends">Friends</option>
<option value="circle">Circle</option>
</select>
<select id="circle-select" class="hidden"></select>
<select id="comment-perm-select" title="Comment permission">
<option value="public">Comments: All</option>
<option value="followers_only">Comments: Followers</option>
<option value="none">Comments: Off</option>
</select>
<select id="react-perm-select" title="React permission">
<option value="both">Reacts: All</option>
<option value="public">Reacts: Public</option>
<option value="private">Reacts: Private</option>
<option value="none">Reacts: Off</option>
</select>
</div>
</div>
<div class="compose-right">
<span id="char-count">0/500</span>
<button id="post-btn" class="btn btn-primary">Post</button>
</div>
</div>
</div>
<div id="my-posts-list"></div>
</section>
<!-- People tab -->
<section id="view-people" class="view">
<div class="section-card">
<h3>Following</h3>
<div id="follows-list"></div>
</div>
<div class="section-card">
<button id="discover-toggle" class="btn btn-ghost btn-sm section-toggle">Discover People</button>
<div id="discover-body" class="hidden">
<p class="empty-hint" style="margin-bottom:0.5rem">People on the network with profiles you can follow.</p>
<div id="discover-list"></div>
</div>
</div>
<div class="section-card" style="display:flex;gap:0.5rem;flex-wrap:wrap">
<button id="share-details-btn" class="btn btn-ghost btn-sm">Share my details</button>
<button id="connect-toggle" class="btn btn-ghost btn-sm">Add peer manually</button>
<div id="connect-body" class="hidden">
<div class="input-row" style="margin-top:0.5rem">
<input id="connect-input" placeholder="paste connect string: nodeid@ip:port" />
<button id="connect-btn" class="btn btn-primary">Connect</button>
</div>
<div id="connect-status"></div>
</div>
</div>
</section>
<!-- Messages tab -->
<section id="view-messages" class="view">
<div id="dm-compose" class="section-card">
<h3>New Conversation</h3>
<div class="dm-compose-row">
<select id="dm-recipient-select">
<option value="">(select recipient)</option>
</select>
</div>
<textarea id="dm-content" placeholder="Write a private message..." rows="2"></textarea>
<div class="dm-compose-footer">
<span class="hint">Encrypted end-to-end</span>
<button id="dm-send-btn" class="btn btn-primary">Send</button>
</div>
</div>
<div id="conversations-list"></div>
<div class="section-card" id="message-requests-section">
<h3>Message Requests</h3>
<div id="message-requests-list"></div>
</div>
</section>
<!-- Settings tab -->
<section id="view-settings" class="view">
<!-- Hidden profile fields (used by JS, edited via Profiles lightbox) -->
<div class="hidden">
<input id="profile-name" type="text" maxlength="50" />
<textarea id="profile-bio" maxlength="200"></textarea>
<input type="checkbox" id="public-visible-check" checked />
<button id="save-profile-btn"></button>
<button id="circle-profiles-toggle"></button>
<div id="circle-profiles-body" class="hidden"><div id="circle-profiles-list"></div></div>
</div>
<div class="section-card" style="text-align:center">
<h3 style="margin-bottom:0.25rem">Updates</h3>
<p class="empty-hint" style="margin-bottom:0.5rem">Network-wide release announcements are signed by the bootstrap anchor and arrive via the CDN. Choose which channel to follow.</p>
<div id="update-channel-row" style="display:flex;gap:1rem;justify-content:center;margin-bottom:0.5rem">
<label style="cursor:pointer"><input type="radio" name="update-channel" value="stable" id="channel-stable" checked> Stable</label>
<label style="cursor:pointer"><input type="radio" name="update-channel" value="beta" id="channel-beta"> Beta</label>
</div>
<div id="update-status" class="empty-hint" style="font-size:0.8rem"></div>
<button id="check-updates-btn" class="btn btn-ghost btn-sm" style="margin-top:0.5rem">Check now</button>
</div>
<div class="section-card" style="text-align:center">
<h3 style="margin-bottom:0.5rem">Identities</h3>
<div id="identities-list" style="margin-bottom:0.5rem"></div>
<div style="display:flex;gap:0.5rem;justify-content:center;flex-wrap:wrap">
<button id="create-identity-btn" class="btn btn-ghost btn-sm">New Identity</button>
<button id="import-identity-btn" class="btn btn-ghost btn-sm">Import Key</button>
</div>
</div>
<div class="section-card" style="text-align:center">
<h3 style="margin-bottom:0.25rem">Personas</h3>
<p class="empty-hint" style="margin-bottom:0.5rem">Separate posting identities on this device. Peers see each persona as a distinct author.</p>
<div id="personas-list" style="margin-bottom:0.5rem;text-align:left"></div>
<button id="create-persona-btn" class="btn btn-ghost btn-sm">New Persona</button>
</div>
<div class="section-card" style="text-align:center">
<div style="display:flex;gap:0.5rem;justify-content:center;flex-wrap:wrap">
<button id="export-btn" class="btn btn-ghost btn-sm">Export</button>
<button id="import-btn" class="btn btn-ghost btn-sm">Import</button>
</div>
</div>
<div class="section-card">
<button id="notifications-btn" class="btn btn-ghost btn-full">Notifications</button>
</div>
<!-- Hidden: node-info, anchors, diagnostics btn moved elsewhere -->
<div class="hidden">
<button id="anchors-toggle"></button>
<div id="anchors-body"><div id="known-anchors-list"></div><div id="anchors-list"></div></div>
<button id="diagnostics-btn"></button>
<div id="node-info"><span id="node-id"></span></div>
</div>
<div class="section-card">
<h3>Text Size</h3>
<div id="text-size-btns" style="display:flex;gap:0.4rem;margin-top:0.3rem">
<button class="notif-opt text-size-opt" data-size="xsmall">XS</button>
<button class="notif-opt text-size-opt" data-size="small">S</button>
<button class="notif-opt text-size-opt active" data-size="normal">M</button>
<button class="notif-opt text-size-opt" data-size="large">L</button>
<button class="notif-opt text-size-opt" data-size="xlarge">XL</button>
</div>
</div>
<div class="section-card">
<h3>Cache Storage</h3>
<div id="cache-stats-display" class="empty-hint">Loading...</div>
<label for="cache-size-select" style="margin-top:0.5rem;display:block">Cache Size Limit</label>
<select id="cache-size-select" style="margin-top:0.25rem;width:100%">
<option value="268435456">256 MB</option>
<option value="536870912">512 MB</option>
<option value="1073741824" selected>1 GB (default)</option>
<option value="2147483648">2 GB</option>
<option value="5368709120">5 GB</option>
<option value="10737418240">10 GB</option>
<option value="0">Unlimited</option>
</select>
</div>
<!-- Redundancy moved to My Posts lightbox -->
<div class="hidden"><div id="redundancy-panel"></div></div>
<!-- Sync moved to diagnostics popover -->
<div class="hidden"><button id="sync-btn"></button></div>
<div class="section-card">
<h3>Danger Zone</h3>
<p class="empty-hint">Delete all local data. Identity key preserved.</p>
<button id="reset-data-btn" class="btn btn-danger btn-full">Reset All Data</button>
</div>
</section>
</main>
<!-- Generic popover -->
<div id="popover-overlay" class="overlay hidden">
<div class="overlay-box overlay-wide">
<div class="overlay-header">
<h3 id="popover-title"></h3>
<button class="overlay-close" id="popover-close-btn">&times;</button>
</div>
<div class="overlay-body" id="popover-body"></div>
</div>
</div>
<div id="toast" class="toast hidden"></div>
<script src="app.js"></script>
</body>
</html>