18 Commits
Author SHA1 Message Date
thePR0M3TH3ANandClaude Opus 4.8 e81e65ff40 feat(blossom): Phase 2 management — blob delete-on-orphan + kind-10063 server list (#30)
- deleteByUrl maps a Blossom URL back to its content-addressed blob (blobSha256FromUrl) and deletes it via BUD-12, defaulting to the URL's own origin. Wired into the edit-modal orphan cleanup so a superseded Blossom video/thumbnail is reclaimed (best-effort, never blocks the edit).

- BUD-03 kind-10063 server-list discovery: read (serversFromServerListEvent) + build (buildBlossomServerListEvent) + an extracted, testable import/publish helper (blossomServerList.js), plus 'Import from my server list' / 'Publish my server list' controls in the Storage pane's Blossom section.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-12 10:32:22 -04:00
thePR0M3TH3ANandClaude Opus 4.8 c22fa23353 fix(blossom): push the encrypted synced copy after saving Blossom servers [LOCAL] (#30)
The keyless Blossom save returned early without triggering storageSyncService.push
(the S3 path does it after saving), so changing Blossom servers never updated the
synced copy — it looked like the settings didn't sync. Blossom servers already
live in the account record's plaintext meta (which exportAccountRecord ships), so
the same push the S3 branch uses now runs after a keyless save: the record is
re-encrypted to self and published, and it roams like S3.

NOTE: sync export/import needs the account's encryptedMasterKey, which exists once
any secret-bearing (S3) connection has been unlocked/saved. A Blossom-ONLY account
(no S3 ever) has no master key envelope yet — encrypted sync for that case is a
follow-up.

Lint + build clean. Local only.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 22:00:21 -04:00
thePR0M3TH3ANandClaude Opus 4.8 ba62b35ada fix(storage): make Blossom actually usable in the upload modal [LOCAL] (#30)
Two root causes for "Blossom set as default but not showing / not usable":

1. SAVE required unlock. saveConnection throws "Storage is locked" without a
   master key — but Blossom has no secret. Added
   storageService.saveKeylessConnection() (writes plaintext meta, no encryption,
   no unlock) and the Blossom save branch now uses it. So a Blossom connection
   saves even if the user never unlocked storage.

2. UploadModal treated Blossom like a locked S3 connection: it only set
   activeCredentials when isStorageUnlocked, showed "Locked 🔒", blocked the
   file-pick behind an unlock prompt, and mislabeled it "S3 Compatible". Now:
   getProviderLabel → "Blossom"; loadFromStorage resolves servers from the
   plaintext meta (no unlock); the summary shows "N servers / Nostr-native"; the
   two upload gates and updateLockUi bypass unlock for Blossom
   (activeProviderIsBlossom). Uploads authorize with the Nostr signer instead.

Bumped UploadModal size baseline (+gating code). Lint + build clean. Local only.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 21:40:18 -04:00
thePR0M3TH3ANandClaude Opus 4.8 c5e8c7f9f1 feat(storage): Blossom Phase 1b — keyless multi-server config UI [LOCAL, flag ON] (#30)
Makes Blossom configurable + uploadable end-to-end (local testing).

- Storage pane: a "Blossom (Nostr-native)" provider option (revealed only when the
  flag is on) that swaps the S3 field group (#storageS3Fields) for a keyless
  #storageBlossomSection — a multi-server textarea (one URL per line). No
  access-key/secret/bucket/CORS.
- ProfileStorageController: parseBlossomServers(); save branch persists the server
  list in the PLAINTEXT connection meta ({servers}, no encrypted secret) via the
  per-provider slot; provider-change loads servers from meta WITHOUT an unlock;
  Test does a best-effort reachability ping; visibility branch hides S3 fields +
  CORS helper for Blossom.
- mediaUploader.resolveActiveConnection: Blossom connections resolve from plaintext
  meta with no unlock (uploads authorize via the Nostr signer, not a stored key),
  so credentials = { provider, servers, meta } flow straight into the Phase-1a
  engine.
- instance-config: FEATURE_BLOSSOM_STORAGE = true for LOCAL testing (reset to false
  before pushing).

Tests: blossom-service 7/7 (isAvailable now flag-relative). Lint + build clean.
Blossom UI present in dist.

VERIFY locally: Storage → Provider → Blossom → add a server → Save → upload a
video → it plays from <server>/<sha256>. Watch the UploadModal's own storage
gating (may need a tweak for the no-secret/no-PIN Blossom case).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-09 16:31:34 -04:00
thePR0M3TH3ANandClaude Opus 4.8 a59a74c976 [sync] Unify wallet + storage sync UI: one confirm, consistent notifications (step 2)
ProfileWalletController and ProfileStorageController each had their own copy of
the sync toggle/restore/overwrite-confirm logic, producing different notifications
and duplicate confirm dialogs. Extract js/ui/profileModal/syncSection.js — one
overwrite-confirm dialog, one status-class mapping, and shared runSyncToggle /
runSyncRestore handlers. Both controllers now delegate, so every syncable item
gets identical toasts + one confirm (conflict-only, via the shared syncConflict).

Intentional divergence preserved: the NWC spending-secret warning stays as the
wallet's preEnableConfirm (a wallet-connect URI can spend), since publishing it
even encrypted warrants a one-time heads-up.

Tests: tests/sync-section.test.mjs (8) — enable/conflict/nothing-to-sync/rollback/
preEnableConfirm/disable/restore, plus wallet-vs-storage produce the same message
shapes. wallet-sync + storage-sync + profile-modal suites green (34).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-01 16:04:33 -04:00
thePR0M3TH3ANandClaude Opus 4.8 ad83c9f8db [ui] route all native alert()/confirm() through the site notification system
Every browser alert()/confirm() now uses the app's UI instead of native dialogs
(no prompt() existed). Two new pieces:

- js/ui/confirmDialog.js — showConfirm(message, opts) -> Promise<boolean>, a styled
  `bv-modal modal-always-on-top` dialog (Confirm/Cancel/backdrop/Escape; danger variant
  focuses Cancel). Importable anywhere, so future confirmations use it too. It stacks
  above other modals and is typable thanks to the recent stacked-modal focus fix.
- js/ui/appNotify.js — notifyError/notifyStatus/notifySuccess routing to the app's
  NotificationController, for modules without a local showError; re-exports showConfirm.

Migrated: UploadModal (10 alert -> this.showError, 1 confirm), subscriptionHistory
(3 alert -> notify*, 1 confirm), MyVideosController (2), ProfileStorage/Wallet/Relay
controllers, profileModalController (2), settingsRestorePrompt (default confirm now
showConfirm), nostr/client deleteAllVersions. confirmSyncOverwrite helpers made async
(syncConflict already awaits confirmOverwrite).

Tests: tests/confirm-dialog.test.mjs (6 — confirm/cancel/backdrop/Escape resolution,
DOM cleanup, danger focus, no-DOM fallback). Build + lint clean; affected suites green.
(Note: build/lint don't parse JS — caught a multi-line-import split + a wrong import
path via module-import checks.)

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 16:49:42 -04:00
thePR0M3TH3ANandClaude Opus 4.8 6e85c2a32b [storage] fix B2 / generic-S3 uploads ("S3 endpoint is required") — prepareUpload contract
Uploading to a Backblaze B2 (or any generic-S3) bucket failed with "S3 endpoint is
required." even though the connection tested fine.

Root cause: the two upload services have different prepareUpload contracts —
r2Service.prepareUpload(npub, { credentials }) vs s3UploadService.prepareUpload(settings).
MediaUploader calls both the r2 way, so for S3/B2 the npub STRING became the settings →
validateS3Connection saw no endpoint. Compounding it, a storageService.getConnection()
result keeps bucket/endpoint/region under `.meta`, which validateS3Connection reads at
the top level. (R2 worked because its service already uses the (npub, {credentials})
contract.)

Fix:
- s3UploadService.prepareUpload now accepts (npub, { credentials }) AND a direct settings
  object (back-compat), flattening meta → top-level via flattenS3Settings.
- The storage Test now derives/passes the B2 public URL (else the prefix), so it stops
  falsely warning "Public Bucket URL is missing. Uploads will fail."

Tests: tests/services/s3-upload-prepare-contract.test.mjs (3 — meta flatten, npub ignored
when credentials given, direct-settings back-compat). Build + lint clean; upload/storage
suites 46/46.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 16:02:37 -04:00
thePR0M3TH3ANandClaude Opus 4.8 2e7e2016d3 [ui] B2 storage: use pipx for the CORS CLI install + rename secret field to "Application Key"
- CORS setup helper: the Backblaze B2 instructions now say `pipx install b2` instead of
  `pip install b2` (the recommended way to install a CLI tool).
- Storage form: for the Backblaze B2 provider the "Secret Access Key" field is now
  labelled "Application Key" to match B2's own terminology (the secret half of an
  Application Key). Other providers keep "Secret Access Key".

Test updated to assert the pipx instruction. Build + lint clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 15:19:22 -04:00
thePR0M3TH3ANandClaude Opus 4.8 f0beab2713 [ui] store storage credentials per provider so R2 / B2 / S3 don't clash
The Storage pane saved every provider into a single shared "default" connection id,
so configuring Backblaze B2 overwrote your Cloudflare R2 (and vice-versa). The service
layer, upload path, and sync already supported a MAP of connections keyed by id
(listConnections / defaultForUploads / exportAccountRecord) — only the UI collapsed
them into one slot.

- Save each provider under its own slot keyed by provider id (cloudflare_r2 /
  backblaze_b2 / generic_s3), preserving a single active defaultForUploads and lazily
  migrating the legacy "default" slot to its provider id.
- Switching the provider dropdown now loads that provider's saved credentials (or
  clears the fields for a fresh entry), so each provider keeps its own creds visibly.
- Sync is unchanged: it already exports the whole account record, so every provider's
  connection syncs together.

Logic extracted to js/ui/profileModal/storageConnections.js (saveProviderConnection +
pure helpers) so ProfileStorageController stays under the 1000-line cap. Tests:
tests/storage-connections.test.mjs (7 — pure default/dup logic + integration proving
R2+B2 coexist with one default, the re-default switch, legacy-"default" migration, and
that the synced record carries both). Storage + profile-modal suites 69/69; lint clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 10:58:30 -04:00
thePR0M3TH3ANandClaude Opus 4.8 6006813b7e [ui] #38: make the CORS setup helper provider-aware (R2 / B2 / Custom S3)
The first cut showed for Custom S3 too but always gave B2-only instructions (b2 CLI +
B2-native JSON), and was hidden for R2. Now the helper shows for all bucket providers
and tailors its content:

- Backblaze B2  -> B2-native rules + `b2 update-bucket --corsRules ...`
- Custom S3     -> standard S3 CORSConfiguration + `aws s3api put-bucket-cors
                   --endpoint-url <endpoint> ...`
- Cloudflare R2 -> standard S3 JSON + Cloudflare dashboard guidance (R2 -> bucket ->
                   Settings -> CORS Policy) and the AWS-CLI command vs the derived R2
                   endpoint.

Origins pre-filled from getCorsOrigins(); intro/command-label/notes set per provider;
copy buttons for both blocks. bitvid still auto-applies CORS on Save (R2/S3 via
ensureBucketCors) — this is the manual fallback when the key lacks CORS-write perms.

buildCorsHelpContent() switches on provider; modal text is now data-driven. Tests
extended to 10 (per-provider rules + command + a DOM provider-switch). Build + lint
clean; storage suites 40/40.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 10:45:50 -04:00
thePR0M3TH3ANandClaude Opus 4.8 d7ad151698 [ui] #38: add a CORS setup helper modal for B2 / S3-compatible buckets
Confirmed against the real B2 account: the web-console "Share everything..." presets
write DOWNLOAD-only CORS (s3_get/s3_head, b2_download_*) with NO s3_put/s3_post, so
bitvid's browser uploads CORS-fail. The web UI can't set custom rules — it points
users at the B2 CLI.

Adds a "CORS setup help" button (shown for B2 / Custom S3, hidden for R2) that opens a
modal with:
- the exact CORS rules JSON in B2-native shape (upload + ranged-playback ops; origins
  pre-filled from getCorsOrigins()), with a Copy button,
- the `b2 update-bucket --corsRules '<json>' <bucket> allPublic` command to apply it,
- step-by-step + the "use a key with writeBucketCors and bitvid sets it for you" tip.

CORS logic extracted to js/ui/profileModal/storageCorsHelp.js (pure builders +
StorageCorsHelp modal controller) so ProfileStorageController stays under the 1000-line
cap. Tests: tests/storage-cors-help.test.mjs (8 — asserts the rules include the upload
ops the presets omit, origin pre-fill, command shape, modal open/close/visibility).
Build + lint clean; profile-modal + storage suites 32/32.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 10:37:24 -04:00
thePR0M3TH3ANandClaude Opus 4.8 e79595b902 [storage] #38: add Backblaze B2 as a first-class storage provider
B2 doesn't map onto the generic-S3 "paste an endpoint" model: its S3-compatible
endpoint is region-scoped (s3.<region>.backblazeb2.com) and a public bucket is
addressed virtual-hosted-style, so the public download URL is
https://<bucket>.s3.<region>.backblazeb2.com/<key> — not derivable from a pasted
generic endpoint. So B2 silently failed under "Custom S3".

- s3-url: deriveB2Endpoint(region) -> https://s3.<region>.backblazeb2.com (empty for
  auto/blank/pasted-host so callers can require a real region); re-exported from
  s3Service.
- storageService: PROVIDERS.B2 + resolveB2Endpoint (explicit endpoint wins, else
  derive) + PROVIDER_TESTS[B2] (derived endpoint, forcePathStyle false).
- Public URL = S3-style virtual-hosted, auto-derived; an explicit Public Access URL
  (custom domain/CDN) overrides it. The save path now forwards the user's public URL
  to prepareS3Connection (also fixes generic-S3 ignoring a custom CDN URL when blank
  -> still derives, so no regression).
- UI: "Backblaze B2" provider option + helper; raw Endpoint hidden for B2 (region
  drives it), region shows a us-west-004 example, force-path-style toggle hidden;
  save/test derive endpoint from region with a clear "enter your region" error.
  UploadModal shows B2-specific labels.
- Tests: tests/storage-b2-provider.test.mjs (8). All s3/storage suites green (50);
  build + lint clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 10:05:53 -04:00
thePR0M3TH3ANandClaude Opus 4.8 35bb1bc576 [ui] #36: auto-open the unlock-saved-key flow from storage when an nsec session is locked
Follow-up to the locked-nsec detection: clicking "Unlock Storage" on a locked
persisted-nsec session (no in-memory signer after a page reload) now auto-opens the
login modal straight to its existing unlock-saved-key (passphrase) flow, so the user
re-unlocks in one step instead of hunting for the Login menu. Once unlocked, the
signer is active app-wide and re-opening the storage pane auto-unlocks.

- applicationBootstrap: add a side-effect-free openLoginModal service to
  profileModalServices (app.loginModalController.openModal — no add-account callback;
  idempotently ensures the modal is initialized first).
- ProfileStorageController: reportLockedNsecSession({ autoOpenLogin }) opens the modal
  only from the user-initiated unlock click, never from the passive pane render (no
  surprise popups).
- Tests extended to 8 (auto-open fires on click; stays closed on passive render).

Build + lint clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-30 09:35:40 -04:00
thePR0M3TH3ANandClaude Opus 4.8 a669040740 [ui] #36: clear, actionable storage-unlock error for a locked nsec session after reload
Root cause (user-confirmed repro): a persisted "remember this key" nsec session
restores the logged-in pubkey + UI on reload, but NOT the in-memory signer (the
private key is only passphrase-encrypted). Storage unlock then has no signer, and
the fallback (ensureActiveSignerForPubkey) can only rebuild an EXTENSION signer —
so an nsec user dead-ended on the misleading "No active signer found. Please login."
even though they still look logged in. The crypto layer itself is correct: an
empirical repro shows storageService.unlock round-trips master-key encrypt/decrypt
fine with real nsec cipher closures.

Fix: detect the locked persisted-nsec case (getLockedStoredNsecSession via
getStoredSessionActorMetadata, scoped to the account being unlocked) and surface an
actionable error/status ("re-enter your passphrase from the Login menu to unlock
your saved key, then unlock storage") in both the Unlock-button flow and the passive
Storage-pane status text, instead of the generic no-signer / no-decryptor messages.
New error code storage-unlock-locked-nsec-session.

Tests: tests/storage-unlock-locked-nsec.test.mjs (6 behavioral). Build + lint clean;
existing storage-service tests still 14/14.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-29 22:19:45 -04:00
thePR0M3TH3ANandClaude Opus 4.8 dec5cf8fe1 feat(sync): warn before overwriting a newer remote copy (conflict guard)
Saves push silently as before, but if the relay copy is NEWER than the one this
device last pushed (another device changed it), the user is now asked to confirm
before overwriting — preventing a silent multi-device clobber. No nag on normal
saves.

- js/services/settingsSyncFlags.js: track per-pubkey+kind last-pushed created_at
  (getSyncPushedAt/setSyncPushedAt).
- js/services/syncConflict.js: pushWithConflictCheck — peek remote created_at
  (list-only, no decrypt), compare to last push, call confirmOverwrite only on a
  real conflict, then push strictly-newer than remote so the replace wins.
- storage/walletSyncService push()/enable() route through it and accept a
  confirmOverwrite callback.
- ProfileStorage/WalletController supply confirmOverwrite (window.confirm) and
  surface a "kept the newer copy" status when the user declines.

tests/sync-conflict.test.mjs: no-remote→silent, newer→prompt(+decline blocks
push), confirm→strictly-newer push, older→no prompt. Mutation-verified
(ignoring the decline fails). Service-test twins gained exists().

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 21:21:54 -04:00
thePR0M3TH3ANandClaude Opus 4.8 7376ff91e0 feat(sync): Storage pane UI for encrypted settings sync (opt-in toggle + restore)
Wires the storage-credential sync (6f7f4196) into the profile Storage pane.

UI (components/profile-modal.html): a "Cross-device sync" card shown only when
storage is unlocked — an opt-in toggle ("Sync these settings to my Nostr account
(encrypted)"), a "Restore from my Nostr account" button, the public-relay /
key-compromise warning copy, and a status line.

Controller (ProfileStorageController):
- toggle on -> storageSyncService.enable() (encrypt + publish), with rollback +
  message if nothing is stored yet; toggle off -> disable() (wipe the note)
- restore -> pull() + re-render the pane so the restored connection shows
- after a successful Save, re-push the synced copy when sync is enabled

Also routes the core's relay READ through the subscription manager (injected
listEvents) instead of pool.list, satisfying lint:pool-access for the new file;
the facade supplies it via nostrClient.getSubscriptionManager().list.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 17:17:35 -04:00
google-labs-jules[bot]andPR0M3TH3AN cf4e6eb1d4 fix(ci): Resolve runtime crash in ProfileStorageController and race in UploadModal
- Fixes `TypeError: Cannot set properties of null (setting 'value')` in `ProfileStorageController` by adding guards for DOM elements during background login (fixes `feed-pagination-stress` and likely `video-upload-flow`).
- Fixes `UploadModal` visibility race condition by awaiting `load()` in `open()` (from previous attempt).
- Adds timeout to `notification-system.spec.ts` to prevent flakiness (from previous attempt).
- Updates visual baselines (from previous attempt).
- Moves `initializeUserInterface` earlier in `app.js` startup (from previous attempt).

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-23 01:00:08 +00:00
google-labs-jules[bot]andPR0M3TH3AN 48d33f45eb [decompose] Extract Wallet and Storage controllers from ProfileModalController
Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-11 03:30:40 +00:00