| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
feat(web): readable, modern meeting notes on the share page and web app (#18592) * feat(web): make meeting notes easy to read and scan Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(frontend): keep share checks runnable without installs Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> | 6 天前 | |
feat(web): meeting screenshots on the public share page; refresh expiring URLs in the web app (#19882) * feat(web): show "What was on screen" on the public share page The shared-conversation page (h.omi.me/conversations/<id>) now renders the approved meeting screenshots from the public GET /v1/conversations/{id}/shared/screenshots route in the Notes tab, after the notes and action items: the banner frame first, then the strip. - Fetched server-side per request with cache: 'no-store' in parallel with the note (the page is already dynamic via headers()), so a cached page never serves signed URLs past their 60-minute expiry. The client refetches through the same server action shortly before url_expires_at, when a hidden tab returns, and when an image fails to load, with a 30s floor between refetches. - Accessible lightbox on Radix Dialog: focus trap, Esc, left/right arrows, focus returns to the tile last shown. Sized to the frame's natural aspect ratio and fit to the viewport without upscaling; an actual-size toggle when the frame is larger than the fit. - Plain lazy <img> with explicit width/height rather than next/image, so expiring signed URLs are not proxied or cached by the image optimizer. - Hidden entirely for an empty set or a failed fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): refresh screenshot signed URLs before they expire useScreenFrames loaded the frame set once and never honoured url_expires_at, so a conversation panel left open past 60 minutes showed broken banner, strip and lightbox images. The hook now silently refetches (bypassing the 1-minute fetch cache) a margin before the earliest expiry, with a floor between refetches so a fast client clock cannot loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): fence URL refreshes against intervening frame mutations A background signed-URL refresh still in flight when deleteFrame/deleteAll/ setSharingEnabled completed could land afterwards and overwrite the mutation's authoritative set, resurrecting deleted tiles (the conversation id still matched, and clearing the timer does not cancel a running request). Every load and mutation now bumps a generation counter; a refresh applies its response only if the generation is unchanged. Regression test reproduces the race with a deferred refresh response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): defer, don't drop, share-page refetches during the cooldown An image onError within 30s of a renewal was discarded, leaving a broken image until the next expiry timer about an hour later. Refetches now go through createRefreshGate, which defers a cooldown request to the end of the cooldown (coalescing repeats), absorbs requests during a run, and is created per effect so StrictMode cannot leave a disposed gate in use. Unit tests drive the gate with an injected clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): close the remaining screenshot refresh races Two ways a stale GET could still bring back a deleted screenshot: - A renewal GET that started after a delete started, but before it committed, captured the already-bumped generation. If the DELETE resolved first, the older GET then applied. Mutations now bump the generation again when they commit, and the initial load is fenced the same way. Tests cover both orderings. - fetchWithCache wrote a GET that resolved after a delete into the cache the delete had just invalidated, so reopening within the 1-minute TTL served the deleted frames. Screenshot-set fetches are no longer cached; the signed URLs expire anyway and the set is small. The screenFrames cache key and its invalidations are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): rearm signed-URL renewal after a fenced-out or failed attempt If a renewal was in flight when a delete started and then failed, the generation fence discarded the renewal and nothing replaced frameSet, so the one-shot timer never rearmed and the signed URLs expired in place. The same happened after a renewal request failed. Both now count as a renewal miss, which rearms the timer with bounded exponential backoff (30s doubling, capped at 5 min); an adopted set resets it. Reproducing tests cover the fenced-out case and the failed-request case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): tag screenshot state with its conversation A delete in conversation A that committed after switching to B bumped the generation unconditionally, discarding B's pending load and leaving A's screenshots displayed under B. A stale A callback started after the switch could do the same at its start. State (set, error, loading) now carries the conversation id it belongs to, and the returned values are derived as empty unless it matches the current id, so a switch clears the previous set in the same render and nothing for A can land under B. Generation bumps (mutation start and commit) happen only while their conversation is current; the current-id ref updates in a layout effect so no promise can resolve between a switch and the ref catching up; only the most recent load may end the loading state. Mutations share one code path. Tests reproduce all three cross-conversation cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): keep the screenshot strip through transient refetch failures A network error or 5xx during a URL renewal returned null and cleared the set, which tore down the renewal timer and listeners, so the strip never came back without a reload. - getSharedScreenshots now returns { ok: true, set } | { ok: false }; only a successful response (including a successful empty one) replaces the set. - A failed refetch rejects inside createRefreshGate, which retries on its own with exponential backoff (30s doubling, capped at 5 min). - Only images that failed are hidden (the tile keeps its gradient ground). - Images are keyed by a per-successful-refetch version: measured live, the backend can return the identical signed URL after a failure, and an <img> whose src does not change never reloads after an error. - ScreenMoments is keyed by conversation id, and responses landing after unmount are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): track screenshot load failures per image URL, not per frame markFailed keyed on frame id, so a failed thumbnail also hid the healthy full-size image of the same frame in the lightbox (and vice versa). Failures are now tracked per image URL (thumbnail and content separately) via withFailedAsset; each <img> checks its own URL, a renewed URL starts clean, and a successful refetch still resets the set. Verified live with every thumbnail blocked: tiles hid their images, the lightbox image loaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): retry a failed server-side screenshot fetch from the client The page converted a failed initial fetch to null, indistinguishable from a successful empty set, so the client never retried and a transient backend error hid the strip for the life of the page. The page now passes the { ok } result through (type moved to memory.types). initialFrameState seeds ScreenMoments: a failure starts empty and enqueues a client retry through the refresh gate; a successful response, including an empty one, is final and never polled. The gate now also stops retrying on its own after 8 consecutive failures (explicit requests still pass), so the retry is bounded in count as well as backoff. Verified live behind a proxy that 503s the first screenshot request: server HTML had no strip, the client retry rendered all 7 tiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): recover on tab return after a failed load; keep lightbox focus sane - After a failed server-side fetch and exhausted automatic retries, the set was empty and the listener effect bailed out, so returning to the tab never retried. recoveryListenersMode now keeps visibilitychange/pageshow listeners registered while awaiting a first successful fetch ('recover'), cleared on the first success; a successful empty set stays unpolled. - When a refresh shrinks the set with the lightbox open, clampOpenIndex clamps the index and shownIndex follows it, so Esc refocuses the frame actually shown. When the set empties (the strip unmounts), focus falls back to the selected tab instead of being lost to <body>. Verified live via a proxy that returns an empty set mid-view: dialog closed, focus on "Notes". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 1 天前 | |
feat(web): meeting screenshots on the public share page; refresh expiring URLs in the web app (#19882) * feat(web): show "What was on screen" on the public share page The shared-conversation page (h.omi.me/conversations/<id>) now renders the approved meeting screenshots from the public GET /v1/conversations/{id}/shared/screenshots route in the Notes tab, after the notes and action items: the banner frame first, then the strip. - Fetched server-side per request with cache: 'no-store' in parallel with the note (the page is already dynamic via headers()), so a cached page never serves signed URLs past their 60-minute expiry. The client refetches through the same server action shortly before url_expires_at, when a hidden tab returns, and when an image fails to load, with a 30s floor between refetches. - Accessible lightbox on Radix Dialog: focus trap, Esc, left/right arrows, focus returns to the tile last shown. Sized to the frame's natural aspect ratio and fit to the viewport without upscaling; an actual-size toggle when the frame is larger than the fit. - Plain lazy <img> with explicit width/height rather than next/image, so expiring signed URLs are not proxied or cached by the image optimizer. - Hidden entirely for an empty set or a failed fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): refresh screenshot signed URLs before they expire useScreenFrames loaded the frame set once and never honoured url_expires_at, so a conversation panel left open past 60 minutes showed broken banner, strip and lightbox images. The hook now silently refetches (bypassing the 1-minute fetch cache) a margin before the earliest expiry, with a floor between refetches so a fast client clock cannot loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): fence URL refreshes against intervening frame mutations A background signed-URL refresh still in flight when deleteFrame/deleteAll/ setSharingEnabled completed could land afterwards and overwrite the mutation's authoritative set, resurrecting deleted tiles (the conversation id still matched, and clearing the timer does not cancel a running request). Every load and mutation now bumps a generation counter; a refresh applies its response only if the generation is unchanged. Regression test reproduces the race with a deferred refresh response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): defer, don't drop, share-page refetches during the cooldown An image onError within 30s of a renewal was discarded, leaving a broken image until the next expiry timer about an hour later. Refetches now go through createRefreshGate, which defers a cooldown request to the end of the cooldown (coalescing repeats), absorbs requests during a run, and is created per effect so StrictMode cannot leave a disposed gate in use. Unit tests drive the gate with an injected clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): close the remaining screenshot refresh races Two ways a stale GET could still bring back a deleted screenshot: - A renewal GET that started after a delete started, but before it committed, captured the already-bumped generation. If the DELETE resolved first, the older GET then applied. Mutations now bump the generation again when they commit, and the initial load is fenced the same way. Tests cover both orderings. - fetchWithCache wrote a GET that resolved after a delete into the cache the delete had just invalidated, so reopening within the 1-minute TTL served the deleted frames. Screenshot-set fetches are no longer cached; the signed URLs expire anyway and the set is small. The screenFrames cache key and its invalidations are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): rearm signed-URL renewal after a fenced-out or failed attempt If a renewal was in flight when a delete started and then failed, the generation fence discarded the renewal and nothing replaced frameSet, so the one-shot timer never rearmed and the signed URLs expired in place. The same happened after a renewal request failed. Both now count as a renewal miss, which rearms the timer with bounded exponential backoff (30s doubling, capped at 5 min); an adopted set resets it. Reproducing tests cover the fenced-out case and the failed-request case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): tag screenshot state with its conversation A delete in conversation A that committed after switching to B bumped the generation unconditionally, discarding B's pending load and leaving A's screenshots displayed under B. A stale A callback started after the switch could do the same at its start. State (set, error, loading) now carries the conversation id it belongs to, and the returned values are derived as empty unless it matches the current id, so a switch clears the previous set in the same render and nothing for A can land under B. Generation bumps (mutation start and commit) happen only while their conversation is current; the current-id ref updates in a layout effect so no promise can resolve between a switch and the ref catching up; only the most recent load may end the loading state. Mutations share one code path. Tests reproduce all three cross-conversation cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): keep the screenshot strip through transient refetch failures A network error or 5xx during a URL renewal returned null and cleared the set, which tore down the renewal timer and listeners, so the strip never came back without a reload. - getSharedScreenshots now returns { ok: true, set } | { ok: false }; only a successful response (including a successful empty one) replaces the set. - A failed refetch rejects inside createRefreshGate, which retries on its own with exponential backoff (30s doubling, capped at 5 min). - Only images that failed are hidden (the tile keeps its gradient ground). - Images are keyed by a per-successful-refetch version: measured live, the backend can return the identical signed URL after a failure, and an <img> whose src does not change never reloads after an error. - ScreenMoments is keyed by conversation id, and responses landing after unmount are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): track screenshot load failures per image URL, not per frame markFailed keyed on frame id, so a failed thumbnail also hid the healthy full-size image of the same frame in the lightbox (and vice versa). Failures are now tracked per image URL (thumbnail and content separately) via withFailedAsset; each <img> checks its own URL, a renewed URL starts clean, and a successful refetch still resets the set. Verified live with every thumbnail blocked: tiles hid their images, the lightbox image loaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): retry a failed server-side screenshot fetch from the client The page converted a failed initial fetch to null, indistinguishable from a successful empty set, so the client never retried and a transient backend error hid the strip for the life of the page. The page now passes the { ok } result through (type moved to memory.types). initialFrameState seeds ScreenMoments: a failure starts empty and enqueues a client retry through the refresh gate; a successful response, including an empty one, is final and never polled. The gate now also stops retrying on its own after 8 consecutive failures (explicit requests still pass), so the retry is bounded in count as well as backoff. Verified live behind a proxy that 503s the first screenshot request: server HTML had no strip, the client retry rendered all 7 tiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): recover on tab return after a failed load; keep lightbox focus sane - After a failed server-side fetch and exhausted automatic retries, the set was empty and the listener effect bailed out, so returning to the tab never retried. recoveryListenersMode now keeps visibilitychange/pageshow listeners registered while awaiting a first successful fetch ('recover'), cleared on the first success; a successful empty set stays unpolled. - When a refresh shrinks the set with the lightbox open, clampOpenIndex clamps the index and shownIndex follows it, so Esc refocuses the frame actually shown. When the set empties (the strip unmounts), focus falls back to the selected tab instead of being lost to <body>. Verified live via a proxy that returns an empty set mid-view: dialog closed, focus on "Notes". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 1 天前 | |
feat(web): meeting screenshots on the public share page; refresh expiring URLs in the web app (#19882) * feat(web): show "What was on screen" on the public share page The shared-conversation page (h.omi.me/conversations/<id>) now renders the approved meeting screenshots from the public GET /v1/conversations/{id}/shared/screenshots route in the Notes tab, after the notes and action items: the banner frame first, then the strip. - Fetched server-side per request with cache: 'no-store' in parallel with the note (the page is already dynamic via headers()), so a cached page never serves signed URLs past their 60-minute expiry. The client refetches through the same server action shortly before url_expires_at, when a hidden tab returns, and when an image fails to load, with a 30s floor between refetches. - Accessible lightbox on Radix Dialog: focus trap, Esc, left/right arrows, focus returns to the tile last shown. Sized to the frame's natural aspect ratio and fit to the viewport without upscaling; an actual-size toggle when the frame is larger than the fit. - Plain lazy <img> with explicit width/height rather than next/image, so expiring signed URLs are not proxied or cached by the image optimizer. - Hidden entirely for an empty set or a failed fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): refresh screenshot signed URLs before they expire useScreenFrames loaded the frame set once and never honoured url_expires_at, so a conversation panel left open past 60 minutes showed broken banner, strip and lightbox images. The hook now silently refetches (bypassing the 1-minute fetch cache) a margin before the earliest expiry, with a floor between refetches so a fast client clock cannot loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): fence URL refreshes against intervening frame mutations A background signed-URL refresh still in flight when deleteFrame/deleteAll/ setSharingEnabled completed could land afterwards and overwrite the mutation's authoritative set, resurrecting deleted tiles (the conversation id still matched, and clearing the timer does not cancel a running request). Every load and mutation now bumps a generation counter; a refresh applies its response only if the generation is unchanged. Regression test reproduces the race with a deferred refresh response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): defer, don't drop, share-page refetches during the cooldown An image onError within 30s of a renewal was discarded, leaving a broken image until the next expiry timer about an hour later. Refetches now go through createRefreshGate, which defers a cooldown request to the end of the cooldown (coalescing repeats), absorbs requests during a run, and is created per effect so StrictMode cannot leave a disposed gate in use. Unit tests drive the gate with an injected clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): close the remaining screenshot refresh races Two ways a stale GET could still bring back a deleted screenshot: - A renewal GET that started after a delete started, but before it committed, captured the already-bumped generation. If the DELETE resolved first, the older GET then applied. Mutations now bump the generation again when they commit, and the initial load is fenced the same way. Tests cover both orderings. - fetchWithCache wrote a GET that resolved after a delete into the cache the delete had just invalidated, so reopening within the 1-minute TTL served the deleted frames. Screenshot-set fetches are no longer cached; the signed URLs expire anyway and the set is small. The screenFrames cache key and its invalidations are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): rearm signed-URL renewal after a fenced-out or failed attempt If a renewal was in flight when a delete started and then failed, the generation fence discarded the renewal and nothing replaced frameSet, so the one-shot timer never rearmed and the signed URLs expired in place. The same happened after a renewal request failed. Both now count as a renewal miss, which rearms the timer with bounded exponential backoff (30s doubling, capped at 5 min); an adopted set resets it. Reproducing tests cover the fenced-out case and the failed-request case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): tag screenshot state with its conversation A delete in conversation A that committed after switching to B bumped the generation unconditionally, discarding B's pending load and leaving A's screenshots displayed under B. A stale A callback started after the switch could do the same at its start. State (set, error, loading) now carries the conversation id it belongs to, and the returned values are derived as empty unless it matches the current id, so a switch clears the previous set in the same render and nothing for A can land under B. Generation bumps (mutation start and commit) happen only while their conversation is current; the current-id ref updates in a layout effect so no promise can resolve between a switch and the ref catching up; only the most recent load may end the loading state. Mutations share one code path. Tests reproduce all three cross-conversation cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): keep the screenshot strip through transient refetch failures A network error or 5xx during a URL renewal returned null and cleared the set, which tore down the renewal timer and listeners, so the strip never came back without a reload. - getSharedScreenshots now returns { ok: true, set } | { ok: false }; only a successful response (including a successful empty one) replaces the set. - A failed refetch rejects inside createRefreshGate, which retries on its own with exponential backoff (30s doubling, capped at 5 min). - Only images that failed are hidden (the tile keeps its gradient ground). - Images are keyed by a per-successful-refetch version: measured live, the backend can return the identical signed URL after a failure, and an <img> whose src does not change never reloads after an error. - ScreenMoments is keyed by conversation id, and responses landing after unmount are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): track screenshot load failures per image URL, not per frame markFailed keyed on frame id, so a failed thumbnail also hid the healthy full-size image of the same frame in the lightbox (and vice versa). Failures are now tracked per image URL (thumbnail and content separately) via withFailedAsset; each <img> checks its own URL, a renewed URL starts clean, and a successful refetch still resets the set. Verified live with every thumbnail blocked: tiles hid their images, the lightbox image loaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): retry a failed server-side screenshot fetch from the client The page converted a failed initial fetch to null, indistinguishable from a successful empty set, so the client never retried and a transient backend error hid the strip for the life of the page. The page now passes the { ok } result through (type moved to memory.types). initialFrameState seeds ScreenMoments: a failure starts empty and enqueues a client retry through the refresh gate; a successful response, including an empty one, is final and never polled. The gate now also stops retrying on its own after 8 consecutive failures (explicit requests still pass), so the retry is bounded in count as well as backoff. Verified live behind a proxy that 503s the first screenshot request: server HTML had no strip, the client retry rendered all 7 tiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): recover on tab return after a failed load; keep lightbox focus sane - After a failed server-side fetch and exhausted automatic retries, the set was empty and the listener effect bailed out, so returning to the tab never retried. recoveryListenersMode now keeps visibilitychange/pageshow listeners registered while awaiting a first successful fetch ('recover'), cleared on the first success; a successful empty set stays unpolled. - When a refresh shrinks the set with the lightbox open, clampOpenIndex clamps the index and shownIndex follows it, so Esc refocuses the frame actually shown. When the set empties (the strip unmounts), focus falls back to the selected tab instead of being lost to <body>. Verified live via a proxy that returns an empty set mid-view: dialog closed, focus on "Notes". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 1 天前 | |
feat(web): meeting screenshots on the public share page; refresh expiring URLs in the web app (#19882) * feat(web): show "What was on screen" on the public share page The shared-conversation page (h.omi.me/conversations/<id>) now renders the approved meeting screenshots from the public GET /v1/conversations/{id}/shared/screenshots route in the Notes tab, after the notes and action items: the banner frame first, then the strip. - Fetched server-side per request with cache: 'no-store' in parallel with the note (the page is already dynamic via headers()), so a cached page never serves signed URLs past their 60-minute expiry. The client refetches through the same server action shortly before url_expires_at, when a hidden tab returns, and when an image fails to load, with a 30s floor between refetches. - Accessible lightbox on Radix Dialog: focus trap, Esc, left/right arrows, focus returns to the tile last shown. Sized to the frame's natural aspect ratio and fit to the viewport without upscaling; an actual-size toggle when the frame is larger than the fit. - Plain lazy <img> with explicit width/height rather than next/image, so expiring signed URLs are not proxied or cached by the image optimizer. - Hidden entirely for an empty set or a failed fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): refresh screenshot signed URLs before they expire useScreenFrames loaded the frame set once and never honoured url_expires_at, so a conversation panel left open past 60 minutes showed broken banner, strip and lightbox images. The hook now silently refetches (bypassing the 1-minute fetch cache) a margin before the earliest expiry, with a floor between refetches so a fast client clock cannot loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): fence URL refreshes against intervening frame mutations A background signed-URL refresh still in flight when deleteFrame/deleteAll/ setSharingEnabled completed could land afterwards and overwrite the mutation's authoritative set, resurrecting deleted tiles (the conversation id still matched, and clearing the timer does not cancel a running request). Every load and mutation now bumps a generation counter; a refresh applies its response only if the generation is unchanged. Regression test reproduces the race with a deferred refresh response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): defer, don't drop, share-page refetches during the cooldown An image onError within 30s of a renewal was discarded, leaving a broken image until the next expiry timer about an hour later. Refetches now go through createRefreshGate, which defers a cooldown request to the end of the cooldown (coalescing repeats), absorbs requests during a run, and is created per effect so StrictMode cannot leave a disposed gate in use. Unit tests drive the gate with an injected clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): close the remaining screenshot refresh races Two ways a stale GET could still bring back a deleted screenshot: - A renewal GET that started after a delete started, but before it committed, captured the already-bumped generation. If the DELETE resolved first, the older GET then applied. Mutations now bump the generation again when they commit, and the initial load is fenced the same way. Tests cover both orderings. - fetchWithCache wrote a GET that resolved after a delete into the cache the delete had just invalidated, so reopening within the 1-minute TTL served the deleted frames. Screenshot-set fetches are no longer cached; the signed URLs expire anyway and the set is small. The screenFrames cache key and its invalidations are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): rearm signed-URL renewal after a fenced-out or failed attempt If a renewal was in flight when a delete started and then failed, the generation fence discarded the renewal and nothing replaced frameSet, so the one-shot timer never rearmed and the signed URLs expired in place. The same happened after a renewal request failed. Both now count as a renewal miss, which rearms the timer with bounded exponential backoff (30s doubling, capped at 5 min); an adopted set resets it. Reproducing tests cover the fenced-out case and the failed-request case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): tag screenshot state with its conversation A delete in conversation A that committed after switching to B bumped the generation unconditionally, discarding B's pending load and leaving A's screenshots displayed under B. A stale A callback started after the switch could do the same at its start. State (set, error, loading) now carries the conversation id it belongs to, and the returned values are derived as empty unless it matches the current id, so a switch clears the previous set in the same render and nothing for A can land under B. Generation bumps (mutation start and commit) happen only while their conversation is current; the current-id ref updates in a layout effect so no promise can resolve between a switch and the ref catching up; only the most recent load may end the loading state. Mutations share one code path. Tests reproduce all three cross-conversation cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): keep the screenshot strip through transient refetch failures A network error or 5xx during a URL renewal returned null and cleared the set, which tore down the renewal timer and listeners, so the strip never came back without a reload. - getSharedScreenshots now returns { ok: true, set } | { ok: false }; only a successful response (including a successful empty one) replaces the set. - A failed refetch rejects inside createRefreshGate, which retries on its own with exponential backoff (30s doubling, capped at 5 min). - Only images that failed are hidden (the tile keeps its gradient ground). - Images are keyed by a per-successful-refetch version: measured live, the backend can return the identical signed URL after a failure, and an <img> whose src does not change never reloads after an error. - ScreenMoments is keyed by conversation id, and responses landing after unmount are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): track screenshot load failures per image URL, not per frame markFailed keyed on frame id, so a failed thumbnail also hid the healthy full-size image of the same frame in the lightbox (and vice versa). Failures are now tracked per image URL (thumbnail and content separately) via withFailedAsset; each <img> checks its own URL, a renewed URL starts clean, and a successful refetch still resets the set. Verified live with every thumbnail blocked: tiles hid their images, the lightbox image loaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): retry a failed server-side screenshot fetch from the client The page converted a failed initial fetch to null, indistinguishable from a successful empty set, so the client never retried and a transient backend error hid the strip for the life of the page. The page now passes the { ok } result through (type moved to memory.types). initialFrameState seeds ScreenMoments: a failure starts empty and enqueues a client retry through the refresh gate; a successful response, including an empty one, is final and never polled. The gate now also stops retrying on its own after 8 consecutive failures (explicit requests still pass), so the retry is bounded in count as well as backoff. Verified live behind a proxy that 503s the first screenshot request: server HTML had no strip, the client retry rendered all 7 tiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): recover on tab return after a failed load; keep lightbox focus sane - After a failed server-side fetch and exhausted automatic retries, the set was empty and the listener effect bailed out, so returning to the tab never retried. recoveryListenersMode now keeps visibilitychange/pageshow listeners registered while awaiting a first successful fetch ('recover'), cleared on the first success; a successful empty set stays unpolled. - When a refresh shrinks the set with the lightbox open, clampOpenIndex clamps the index and shownIndex follows it, so Esc refocuses the frame actually shown. When the set empties (the strip unmounts), focus falls back to the selected tab instead of being lost to <body>. Verified live via a proxy that returns an empty set mid-view: dialog closed, focus on "Notes". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 1 天前 | |
chore: feature-flag registry as source of truth + graduate/kill dead flags (#18722) * chore: make flag authority auditable without inert gates Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: preserve mobile changelog contract for dead switch removal Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: keep legacy cohort decoder coverage lint-clean Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: remove orphaned Google OAuth credential store Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: keep memory proofs on the canonical intake switch Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: regenerate feature-flag registry doc after main merge Upstream SCA-542 declared CONVERSATION_RELEVANCE_JEV_ENABLED and MEMORY_OWNER_JEV_FLIP_ENABLED in deploy manifests, so the rendered registry moved both out of the undeclared section. * chore: keep backend AGENTS.md within the size ratchet The flag-registry line grew the guide to 39011 bytes against its 39000 budget, failing the Hygiene agents-md-lean check. Same facts, tighter wording: 38986 bytes. * chore: restore main's desktop changelog release state after merge * chore: dart-format touched provider files to satisfy the pre-push gate * chore: apply resolved-formatter indentation to developer_mode_provider * chore(ci): re-trigger pull_request checks (scheduler wedge left suites queued) * chore: regenerate rendered feature-flag registry after main merge --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: David Zhang <9387252+Git-on-my-level@users.noreply.github.com> | 6 天前 | |
web fix images hosts, fix web frontend lints (#2688) * Add vecteezy.com to image host * Fix lints on web | 1 年前 | |
feat(web): meeting screenshots on the public share page; refresh expiring URLs in the web app (#19882) * feat(web): show "What was on screen" on the public share page The shared-conversation page (h.omi.me/conversations/<id>) now renders the approved meeting screenshots from the public GET /v1/conversations/{id}/shared/screenshots route in the Notes tab, after the notes and action items: the banner frame first, then the strip. - Fetched server-side per request with cache: 'no-store' in parallel with the note (the page is already dynamic via headers()), so a cached page never serves signed URLs past their 60-minute expiry. The client refetches through the same server action shortly before url_expires_at, when a hidden tab returns, and when an image fails to load, with a 30s floor between refetches. - Accessible lightbox on Radix Dialog: focus trap, Esc, left/right arrows, focus returns to the tile last shown. Sized to the frame's natural aspect ratio and fit to the viewport without upscaling; an actual-size toggle when the frame is larger than the fit. - Plain lazy <img> with explicit width/height rather than next/image, so expiring signed URLs are not proxied or cached by the image optimizer. - Hidden entirely for an empty set or a failed fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): refresh screenshot signed URLs before they expire useScreenFrames loaded the frame set once and never honoured url_expires_at, so a conversation panel left open past 60 minutes showed broken banner, strip and lightbox images. The hook now silently refetches (bypassing the 1-minute fetch cache) a margin before the earliest expiry, with a floor between refetches so a fast client clock cannot loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): fence URL refreshes against intervening frame mutations A background signed-URL refresh still in flight when deleteFrame/deleteAll/ setSharingEnabled completed could land afterwards and overwrite the mutation's authoritative set, resurrecting deleted tiles (the conversation id still matched, and clearing the timer does not cancel a running request). Every load and mutation now bumps a generation counter; a refresh applies its response only if the generation is unchanged. Regression test reproduces the race with a deferred refresh response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): defer, don't drop, share-page refetches during the cooldown An image onError within 30s of a renewal was discarded, leaving a broken image until the next expiry timer about an hour later. Refetches now go through createRefreshGate, which defers a cooldown request to the end of the cooldown (coalescing repeats), absorbs requests during a run, and is created per effect so StrictMode cannot leave a disposed gate in use. Unit tests drive the gate with an injected clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): close the remaining screenshot refresh races Two ways a stale GET could still bring back a deleted screenshot: - A renewal GET that started after a delete started, but before it committed, captured the already-bumped generation. If the DELETE resolved first, the older GET then applied. Mutations now bump the generation again when they commit, and the initial load is fenced the same way. Tests cover both orderings. - fetchWithCache wrote a GET that resolved after a delete into the cache the delete had just invalidated, so reopening within the 1-minute TTL served the deleted frames. Screenshot-set fetches are no longer cached; the signed URLs expire anyway and the set is small. The screenFrames cache key and its invalidations are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): rearm signed-URL renewal after a fenced-out or failed attempt If a renewal was in flight when a delete started and then failed, the generation fence discarded the renewal and nothing replaced frameSet, so the one-shot timer never rearmed and the signed URLs expired in place. The same happened after a renewal request failed. Both now count as a renewal miss, which rearms the timer with bounded exponential backoff (30s doubling, capped at 5 min); an adopted set resets it. Reproducing tests cover the fenced-out case and the failed-request case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): tag screenshot state with its conversation A delete in conversation A that committed after switching to B bumped the generation unconditionally, discarding B's pending load and leaving A's screenshots displayed under B. A stale A callback started after the switch could do the same at its start. State (set, error, loading) now carries the conversation id it belongs to, and the returned values are derived as empty unless it matches the current id, so a switch clears the previous set in the same render and nothing for A can land under B. Generation bumps (mutation start and commit) happen only while their conversation is current; the current-id ref updates in a layout effect so no promise can resolve between a switch and the ref catching up; only the most recent load may end the loading state. Mutations share one code path. Tests reproduce all three cross-conversation cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): keep the screenshot strip through transient refetch failures A network error or 5xx during a URL renewal returned null and cleared the set, which tore down the renewal timer and listeners, so the strip never came back without a reload. - getSharedScreenshots now returns { ok: true, set } | { ok: false }; only a successful response (including a successful empty one) replaces the set. - A failed refetch rejects inside createRefreshGate, which retries on its own with exponential backoff (30s doubling, capped at 5 min). - Only images that failed are hidden (the tile keeps its gradient ground). - Images are keyed by a per-successful-refetch version: measured live, the backend can return the identical signed URL after a failure, and an <img> whose src does not change never reloads after an error. - ScreenMoments is keyed by conversation id, and responses landing after unmount are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): track screenshot load failures per image URL, not per frame markFailed keyed on frame id, so a failed thumbnail also hid the healthy full-size image of the same frame in the lightbox (and vice versa). Failures are now tracked per image URL (thumbnail and content separately) via withFailedAsset; each <img> checks its own URL, a renewed URL starts clean, and a successful refetch still resets the set. Verified live with every thumbnail blocked: tiles hid their images, the lightbox image loaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): retry a failed server-side screenshot fetch from the client The page converted a failed initial fetch to null, indistinguishable from a successful empty set, so the client never retried and a transient backend error hid the strip for the life of the page. The page now passes the { ok } result through (type moved to memory.types). initialFrameState seeds ScreenMoments: a failure starts empty and enqueues a client retry through the refresh gate; a successful response, including an empty one, is final and never polled. The gate now also stops retrying on its own after 8 consecutive failures (explicit requests still pass), so the retry is bounded in count as well as backoff. Verified live behind a proxy that 503s the first screenshot request: server HTML had no strip, the client retry rendered all 7 tiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): recover on tab return after a failed load; keep lightbox focus sane - After a failed server-side fetch and exhausted automatic retries, the set was empty and the listener effect bailed out, so returning to the tab never retried. recoveryListenersMode now keeps visibilitychange/pageshow listeners registered while awaiting a first successful fetch ('recover'), cleared on the first success; a successful empty set stays unpolled. - When a refresh shrinks the set with the lightbox open, clampOpenIndex clamps the index and shownIndex follows it, so Esc refocuses the frame actually shown. When the set empties (the strip unmounts), focus falls back to the selected tab instead of being lost to <body>. Verified live via a proxy that returns an empty set mid-view: dialog closed, focus on "Notes". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 1 天前 | |
feat(web): meeting screenshots on the public share page; refresh expiring URLs in the web app (#19882) * feat(web): show "What was on screen" on the public share page The shared-conversation page (h.omi.me/conversations/<id>) now renders the approved meeting screenshots from the public GET /v1/conversations/{id}/shared/screenshots route in the Notes tab, after the notes and action items: the banner frame first, then the strip. - Fetched server-side per request with cache: 'no-store' in parallel with the note (the page is already dynamic via headers()), so a cached page never serves signed URLs past their 60-minute expiry. The client refetches through the same server action shortly before url_expires_at, when a hidden tab returns, and when an image fails to load, with a 30s floor between refetches. - Accessible lightbox on Radix Dialog: focus trap, Esc, left/right arrows, focus returns to the tile last shown. Sized to the frame's natural aspect ratio and fit to the viewport without upscaling; an actual-size toggle when the frame is larger than the fit. - Plain lazy <img> with explicit width/height rather than next/image, so expiring signed URLs are not proxied or cached by the image optimizer. - Hidden entirely for an empty set or a failed fetch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): refresh screenshot signed URLs before they expire useScreenFrames loaded the frame set once and never honoured url_expires_at, so a conversation panel left open past 60 minutes showed broken banner, strip and lightbox images. The hook now silently refetches (bypassing the 1-minute fetch cache) a margin before the earliest expiry, with a floor between refetches so a fast client clock cannot loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): fence URL refreshes against intervening frame mutations A background signed-URL refresh still in flight when deleteFrame/deleteAll/ setSharingEnabled completed could land afterwards and overwrite the mutation's authoritative set, resurrecting deleted tiles (the conversation id still matched, and clearing the timer does not cancel a running request). Every load and mutation now bumps a generation counter; a refresh applies its response only if the generation is unchanged. Regression test reproduces the race with a deferred refresh response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): defer, don't drop, share-page refetches during the cooldown An image onError within 30s of a renewal was discarded, leaving a broken image until the next expiry timer about an hour later. Refetches now go through createRefreshGate, which defers a cooldown request to the end of the cooldown (coalescing repeats), absorbs requests during a run, and is created per effect so StrictMode cannot leave a disposed gate in use. Unit tests drive the gate with an injected clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): close the remaining screenshot refresh races Two ways a stale GET could still bring back a deleted screenshot: - A renewal GET that started after a delete started, but before it committed, captured the already-bumped generation. If the DELETE resolved first, the older GET then applied. Mutations now bump the generation again when they commit, and the initial load is fenced the same way. Tests cover both orderings. - fetchWithCache wrote a GET that resolved after a delete into the cache the delete had just invalidated, so reopening within the 1-minute TTL served the deleted frames. Screenshot-set fetches are no longer cached; the signed URLs expire anyway and the set is small. The screenFrames cache key and its invalidations are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): rearm signed-URL renewal after a fenced-out or failed attempt If a renewal was in flight when a delete started and then failed, the generation fence discarded the renewal and nothing replaced frameSet, so the one-shot timer never rearmed and the signed URLs expired in place. The same happened after a renewal request failed. Both now count as a renewal miss, which rearms the timer with bounded exponential backoff (30s doubling, capped at 5 min); an adopted set resets it. Reproducing tests cover the fenced-out case and the failed-request case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web-app): tag screenshot state with its conversation A delete in conversation A that committed after switching to B bumped the generation unconditionally, discarding B's pending load and leaving A's screenshots displayed under B. A stale A callback started after the switch could do the same at its start. State (set, error, loading) now carries the conversation id it belongs to, and the returned values are derived as empty unless it matches the current id, so a switch clears the previous set in the same render and nothing for A can land under B. Generation bumps (mutation start and commit) happen only while their conversation is current; the current-id ref updates in a layout effect so no promise can resolve between a switch and the ref catching up; only the most recent load may end the loading state. Mutations share one code path. Tests reproduce all three cross-conversation cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): keep the screenshot strip through transient refetch failures A network error or 5xx during a URL renewal returned null and cleared the set, which tore down the renewal timer and listeners, so the strip never came back without a reload. - getSharedScreenshots now returns { ok: true, set } | { ok: false }; only a successful response (including a successful empty one) replaces the set. - A failed refetch rejects inside createRefreshGate, which retries on its own with exponential backoff (30s doubling, capped at 5 min). - Only images that failed are hidden (the tile keeps its gradient ground). - Images are keyed by a per-successful-refetch version: measured live, the backend can return the identical signed URL after a failure, and an <img> whose src does not change never reloads after an error. - ScreenMoments is keyed by conversation id, and responses landing after unmount are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): track screenshot load failures per image URL, not per frame markFailed keyed on frame id, so a failed thumbnail also hid the healthy full-size image of the same frame in the lightbox (and vice versa). Failures are now tracked per image URL (thumbnail and content separately) via withFailedAsset; each <img> checks its own URL, a renewed URL starts clean, and a successful refetch still resets the set. Verified live with every thumbnail blocked: tiles hid their images, the lightbox image loaded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): retry a failed server-side screenshot fetch from the client The page converted a failed initial fetch to null, indistinguishable from a successful empty set, so the client never retried and a transient backend error hid the strip for the life of the page. The page now passes the { ok } result through (type moved to memory.types). initialFrameState seeds ScreenMoments: a failure starts empty and enqueues a client retry through the refresh gate; a successful response, including an empty one, is final and never polled. The gate now also stops retrying on its own after 8 consecutive failures (explicit requests still pass), so the retry is bounded in count as well as backoff. Verified live behind a proxy that 503s the first screenshot request: server HTML had no strip, the client retry rendered all 7 tiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): recover on tab return after a failed load; keep lightbox focus sane - After a failed server-side fetch and exhausted automatic retries, the set was empty and the listener effect bailed out, so returning to the tab never retried. recoveryListenersMode now keeps visibilitychange/pageshow listeners registered while awaiting a first successful fetch ('recover'), cleared on the first success; a successful empty set stays unpolled. - When a refresh shrinks the set with the lightbox open, clampOpenIndex clamps the index and shownIndex follows it, so Esc refocuses the frame actually shown. When the set empties (the strip unmounts), focus falls back to the selected tab instead of being lost to <body>. Verified live via a proxy that returns an empty set mid-view: dialog closed, focus on "Notes". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 1 天前 | |
frontend folder in web | 1 年前 |