Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-22 16:29:05 -07:00
commit 7f88d4f0bc
4 changed files with 23 additions and 8 deletions

View File

@ -161,6 +161,7 @@ export class CommunitySync extends EventTarget {
#offlineStore: OfflineStore | null = null;
#saveDebounceTimer: ReturnType<typeof setTimeout> | null = null;
#syncedDebounceTimer: ReturnType<typeof setTimeout> | null = null;
#initialSyncFired = false;
#wsUrl: string | null = null;
// ── Undo/Redo state ──
@ -292,6 +293,7 @@ export class CommunitySync extends EventTarget {
this.#ws.onclose = () => {
console.log(`[CommunitySync] Disconnected from ${this.#communitySlug}`);
this.#initialSyncFired = false;
this.dispatchEvent(new CustomEvent("disconnected"));
if (!this.#disconnectedIntentionally) {
@ -844,10 +846,12 @@ export class CommunitySync extends EventTarget {
// Debounce the synced event — during initial sync negotiation, #applyDocToDOM()
// is called for every Automerge sync message (100+ round-trips). Debounce to
// fire once after the burst settles.
// fire once after the burst settles. Only fires once per connection cycle.
if (this.#initialSyncFired) return;
if (this.#syncedDebounceTimer) clearTimeout(this.#syncedDebounceTimer);
this.#syncedDebounceTimer = setTimeout(() => {
this.#syncedDebounceTimer = null;
this.#initialSyncFired = true;
this.dispatchEvent(new CustomEvent("synced", { detail: { shapes } }));
}, 300);
}

View File

@ -711,7 +711,7 @@ export class RStackIdentity extends HTMLElement {
let openSection: string | null = null;
// Account completion status
let acctStatus: { email: boolean; multiDevice: boolean; socialRecovery: boolean; guardianCount: number; credentialCount: number } | null = null;
let acctStatus: { email: boolean; emailAddress?: string | null; multiDevice: boolean; socialRecovery: boolean; guardianCount: number; credentialCount: number } | null = null;
// Lazy-loaded data
let guardians: { id: string; name: string; email?: string; status: string }[] = [];
@ -819,10 +819,13 @@ export class RStackIdentity extends HTMLElement {
</div>`;
}
}
const emailDisplay = !isOpen && done && acctStatus?.emailAddress
? `<span style="color:var(--rs-text-secondary);font-size:0.85rem;margin-left:8px">${acctStatus.emailAddress}</span>`
: "";
return `
<div class="account-section${isOpen ? " open" : ""}${done === false ? " section--warning" : ""}">
<div class="account-section-header" data-section="email">
<span>${statusDot(done)} Email</span>
<span>${statusDot(done)} Email${emailDisplay}</span>
<span class="section-arrow">${isOpen ? "▾" : "▸"}</span>
</div>
${body}
@ -1087,7 +1090,7 @@ export class RStackIdentity extends HTMLElement {
body: JSON.stringify({ email: emailAddr, code }),
});
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "Verification failed");
if (acctStatus) acctStatus.email = true;
if (acctStatus) { acctStatus.email = true; acctStatus.emailAddress = emailAddr; }
openSection = null; render();
this.dispatchEvent(new CustomEvent("identity-action", { bubbles: true, composed: true, detail: { action: "email-added", email: emailAddr } }));
} catch (e: any) {

View File

@ -224,7 +224,7 @@ export async function cleanExpiredChallenges(): Promise<number> {
// ============================================================================
export async function setUserEmail(userId: string, email: string): Promise<void> {
await sql`UPDATE users SET email = ${email} WHERE id = ${userId}`;
await sql`UPDATE users SET email = ${email}, profile_email = ${email}, updated_at = NOW() WHERE id = ${userId}`;
}
export async function getUserByEmail(email: string) {

View File

@ -3088,10 +3088,18 @@
let _tripCache = null; // { trips: [], detail: null, fetchedAt: 0 }
const TRIP_CACHE_TTL = 60000; // 1 minute
function _authHeaders() {
try {
const s = JSON.parse(localStorage.getItem('encryptid_session') || '{}');
if (s?.accessToken) return { 'Authorization': 'Bearer ' + s.accessToken };
} catch {}
return {};
}
async function fetchTripData() {
if (_tripCache && Date.now() - _tripCache.fetchedAt < TRIP_CACHE_TTL) return _tripCache;
try {
const res = await fetch(`/${communitySlug}/rtrips/api/trips`);
const res = await fetch(`/${communitySlug}/rtrips/api/trips`, { headers: _authHeaders() });
if (!res.ok) return { trips: [], detail: null, fetchedAt: Date.now() };
const trips = await res.json();
_tripCache = { trips, detail: null, fetchedAt: Date.now() };
@ -3101,7 +3109,7 @@
async function fetchTripDetail(tripId) {
try {
const res = await fetch(`/${communitySlug}/rtrips/api/trips/${tripId}`);
const res = await fetch(`/${communitySlug}/rtrips/api/trips/${tripId}`, { headers: _authHeaders() });
if (!res.ok) return null;
return await res.json();
} catch { return null; }
@ -3145,7 +3153,7 @@
async function fetchNotesData() {
if (_notesCache && Date.now() - _notesCache.fetchedAt < TRIP_CACHE_TTL) return _notesCache;
try {
const res = await fetch(`/${communitySlug}/rnotes/api/notes?limit=50`);
const res = await fetch(`/${communitySlug}/rnotes/api/notes?limit=50`, { headers: _authHeaders() });
if (!res.ok) return { notes: [], fetchedAt: Date.now() };
const data = await res.json();
_notesCache = { notes: data.notes || [], fetchedAt: Date.now() };