Skip to content

Promote dev to main (upstream sync 2026-09-26: preview comments, connected clients, platform fixes) - #66

Merged
JOY (JOY) merged 45 commits into
mainfrom
dev
Sep 28, 2026
Merged

JOY (JOY) merged 45 commits into
mainfrom
dev

Conversation

@JOY

@JOY JOY (JOY) commented Sep 28, 2026 •

Copy link
Copy Markdown

Promotion for prod deploy: #65 (upstream sync 42 commits). PROD DB prerequisite already done: the Comments migration landed in the DOS-Me ledger and is verified live on the shared prod Supabase (6 new columns, nullable userId, indexes + FK, resolvedAt as timestamp(3) per Prisma model). Same reviewed diff, no new commits.


📌 TL;DR

This PR introduces a robust inline commenting system for post previews, allowing both authenticated and anonymous users to leave anchored comments with optional reCAPTCHA protection. It also refactors the preview UI to use a new design system, adds provider migration/visibility controls, and improves error handling for Stripe and Chrome extension interactions.

🎯 Type of Change

  • 🚀 New feature
  • 🐛 Bugfix
  • 🧹 Refactor
  • ⚙️ CI / Configuration

🔍 Changes Walkthrough

File Summary of Changes
.env.example Added configuration variables for reCAPTCHA (site/secret keys) and provider visibility/migration settings (HIDDEN_PROVIDERS, MIGRATE_PROVIDERS).
.zcodeignore New file defining ignore rules for the ZCode tool, syncing with .gitignore and adding specific exclusions for build artifacts and secrets.
apps/backend/src/api/routes/posts.controller.ts Updated createComment to accept a structured DTO and IP address for public comments. Added a new resolveComment endpoint to mark comments as resolved.
apps/backend/src/api/routes/public.controller.ts Added a public endpoint POST /posts/:id/comments to allow anonymous users to submit comments, passing the real IP for security checks.
apps/frontend/src/app/(app)/(preview)/p/[id]/layout.tsx Updated the preview layout to use new design system color tokens (bg-newBgColor, text-newTextColor).
apps/frontend/src/app/(app)/(preview)/p/[id]/page.tsx Major refactor of the preview page. Wrapped content in PreviewCommentsProvider. Replaced static HTML rendering with PostContentClient to support text selection for comments. Updated UI structure to use new design tokens and sticky comment sidebar.
apps/frontend/src/app/(app)/layout.tsx Passed recaptchaSiteKey from environment variables to the frontend context.
apps/frontend/src/app/global.scss Added a custom CSS class .preview-comment-cursor with a custom SVG cursor to indicate text selection for commenting.
apps/frontend/src/components/admin/admin-stats.component.tsx Added display for "Connected clients" statistics in the admin dashboard.
apps/frontend/src/components/auth/after.activate.tsx Improved auth activation flow to check for an onboarding header and skip further processing if present.
apps/frontend/src/components/billing/first.billing.component.tsx Added error handling for Stripe loading failures, displaying a user-friendly message if the payment form fails to load (e.g., due to ad blockers).
apps/frontend/src/components/launches/add.provider.component.tsx Fixed a potential crash when chrome object is undefined in non-extension environments.
apps/frontend/src/components/launches/calendar.tsx Added a check to show a "Post not found" warning if fetching posts by group returns an empty list.
apps/frontend/src/components/launches/continue.integration.tsx Improved Chrome extension messaging to handle lastError gracefully.
apps/frontend/src/components/launches/menu/menu.tsx Improved Chrome extension messaging to handle lastError gracefully.
apps/frontend/src/components/media/new.uploader.tsx Fixed Uppy uploader state management by using cancelAll instead of clear on error and safely accessing file state.
apps/frontend/src/components/preview/comments.components.tsx Core Feature: Replaced the simple comment list with a full-featured inline commenting system. Includes:
1. PreviewCommentsProvider context integration.
2. Anonymous user support with name capture and reCAPTCHA v2 invisible integration.
3. Text anchoring (start/end/quote) for inline comments.
4. Threaded replies and "Resolve" functionality.
5. New UI components for composer, thread cards, and reviewer name modal.

📊 Architectural Flow

sequenceDiagram
    participant User as User (Browser)
    participant Frontend as Preview Page (React)
    participant Context as PreviewCommentsContext
    participant Backend as Backend API
    participant Recaptcha as Google reCAPTCHA

    User->>Frontend: Selects text in post content
    Frontend->>Context: setPending({start, end, quote})
    Frontend->>User: Shows Comment Composer with anchor
    
    User->>Frontend: Submits Comment
    alt User is Authenticated
        Frontend->>Backend: POST /posts/:id/comments (with auth)
    else User is Anonymous
        Frontend->>Frontend: Prompt for Name (if not saved)
        Frontend->>Recaptcha: Execute Invisible reCAPTCHA
        Recaptcha-->>Frontend: Token
        Frontend->>Backend: POST /public/posts/:id/comments (with IP, Name, Token)
    end
    
    Backend->>Backend: Validate & Save Comment
    Backend-->>Frontend: Success Response
    Frontend->>Context: mutate() (Refetch comments)
    Context-->>Frontend: Updated Comment List
    Frontend->>User: Renders new comment thread
Loading

…ting

Anchored text comments with highlights, one-level replies, org-scoped
resolve/reopen, named anonymous comments behind optional invisible
reCAPTCHA v2 and the IP throttler, and a dashboard restyle of /p/:id.
…omments

# Conflicts:
#	apps/frontend/src/app/(app)/layout.tsx
#	libraries/nestjs-libraries/src/database/prisma/posts/posts.service.ts
…comments

feat(preview): inline comments, replies, resolve and anonymous commenting
…e silent-video audio hint

The container status poll is HTTP 200 with the failure in the body, so it never went through handleErrors and Meta's raw status text reached the tooltip.
…audio-tooltip-main

fix(instagram): curate container processing errors, map 2207082 to the silent-video audio hint
…allbacks

The STORE_REFRESH_TOKEN and REMOVE_REFRESH_TOKEN calls to the Postiz
browser extension passed a no-op callback. When the extension is not
installed, disabled, or the origin is not allowed, Chrome logs
"Unchecked runtime.lastError: Could not establish connection. Receiving
end does not exist." to the user's console because nothing read
lastError. Reading it in the callback, as add.provider.component.tsx
already does, marks the error as checked and silences the log. The
calls stay fire-and-forget with no behavior change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rrors

A Telegram 400 (caption too long, media Telegram could not download) is a
permanent rejection, but it surfaced as a plain error the workflow retried
3 times and then reported as "Could not publish after several attempts"
with a misleading couldn't-confirm email. Map it to BadBody so it fails
once with the real reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyDRJpL27E7WCK5UGsUope
finalizeEvent requires the secret key as a Uint8Array, but post/comment
passed the stored hex string, so every Nostr publish crashed with
"expected Uint8Array, got type=string". Reuse the same conversion
authenticate already does via a shared helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PyDRJpL27E7WCK5UGsUope
…ipt errors

MetaMask's injected inpage.js throws "Failed to connect to MetaMask"
(linked: "MetaMask extension not found") as Error instances, so the
existing plain-object wallet filter does not catch them and beforeSend
opens the user-report dialog. Both messages are added to ignorePatterns.
Sentry CLOUD-C: 860 events / 166 users in the last 30 days, 2270 events
/ 305 users lifetime, no Postiz frames in the stack.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y-main

fix(telegram): map Telegram 400 rejections to non-retryable BadBody errors
…tamask

fix(frontend): stop the Sentry report dialog on MetaMask injected-script errors
A page listed without an access_token made reConnect return an undefined token, and the integration upsert then threw a 500.
…-main

fix(nostr): convert the hex private key to bytes before signing posts
…ked-last-error

fix(frontend): silence Unchecked runtime.lastError from extension messaging callbacks
…ct-page-token

fix(facebook): fail reconnect cleanly when the page has no page token
…ed file

The Uppy upload-success handler read getState().files[file.id].progress
unconditionally. When the file had already been removed from Uppy's
state (validation or error paths call removeFile) the lookup is
undefined and the handler threw "Cannot read properties of undefined
(reading 'progress')", which also pops the Sentry report dialog.

Read the file state once and return when it is gone; setFileState is
otherwise unchanged.

Sentry CLOUD-20X + CLOUD-211 (Safari) + CLOUD-20Y: 1375 events / 62
users in the last 30 days, first seen 2026-09-16.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
getPostsByGroup crashed on posts[0].integrationId when the group had no
posts left (deleted from another tab, by a teammate, or on sub cancel),
so the API returned a 500 body. The calendar's editPost then indexed
data.posts[0] on that error body and threw an unhandled TypeError, which
also pops the Sentry report dialog.

The service now throws NotFoundException like the other post lookups,
and editPost returns early with the existing "Post not found" toast and
a calendar mutate so the stale tile disappears.

Sentry CLOUD-QF (Chrome) + CLOUD-QK (Safari): 833 events / 309 users in
the last 30 days, 2807 / 635 lifetime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ess error

The @uppy/aws-s3 4.3.2 progress callback reads uppy.getFile(id).progress
from the S3 part PUT load handler without a null check, so when the file
has already left uppy's state it throws "undefined is not an object
(evaluating 'r.progress')" (Sentry CLOUD-211). beforeSend shows the report
dialog for every captured exception, so the throw surfaced as a
"Something broke" popup for users on Safari. The upload itself is done or
cancelled by the time this fires, so the error is ignored like the
posthog recorder one.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…py-progress

fix(frontend): stop the Sentry report dialog on the uppy aws-s3 progress error
…-file-progress

fix(media): skip the upload-success state update for an already removed file
…-group-guard

fix(calendar): handle a deleted post group when opening a calendar tile
…heck

add.provider.component.tsx tested chrome?.runtime?.sendMessage without
first checking typeof chrome, so on Safari and iOS Chrome, where the
chrome global does not exist, the bare identifier threw
"ReferenceError: Can't find variable: chrome" instead of showing the
existing "Extension Not Found" modal. The other two extension call sites
already guard with typeof chrome !== 'undefined'.

Sentry CLOUD-14W: 8 events / 6 users in the last 30 days, 22 / 11
lifetime, mostly on the LinkedIn connect flow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…y state

The Uppy error handler called uppy.clear() while the failed upload was
still registered in currentUploads. Uppy core throws
"The installed uploader plugin does not allow removing files during an
upload." from clear() in that state, so every Transloadit assembly_error
produced a second, unhandled exception on top of the real failure and
popped the Sentry report dialog.

cancelAll() removes every file first, which drops the upload record
before that check, then resets the upload state; the rest of the handler
is unchanged.

Sentry CLOUD-S1: 53 events / 29 users in the last 30 days, 264 / 75
lifetime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…crashing

loadStripe() rejects when js.stripe.com is blocked (ad blockers, privacy
extensions, corporate proxies). The promise was stored in state with no
handler, so the rejection surfaced as an unhandled
"Error: Failed to load Stripe.js", popped the Sentry report dialog, and
the billing page stayed on the loading spinner forever.

The promise now has a catch that flips a stripeFailed state, and the page
renders an explanation in the same bordered box used for the
"another account already subscribed" case. New translation key
billing_stripe_load_failed added to all locales via lingo.dev.

Sentry CLOUD-SJ: 185 events / 27 users in the last 30 days, 338 / 75
lifetime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-undefined-guard

fix(frontend): guard the chrome global before the extension connect check
…on-error

fix(media): cancel in-flight uploads on error instead of clearing Uppy state
…ror-main

fix: friendly message when Threads reports an UNKNOWN container error
…redirect started

On a successful activation the backend answers with the onboarding
header. The shared afterRequest handler sees it, sets window.location to
the onboarding page and hands the response back; the activation page
then called .json() on it while the page was already navigating away.
Firefox rejects that body read with "AbortError: The operation was
aborted.", which is unhandled and pops the Sentry report dialog.

The page now returns early when the response carries the onboarding
header and only reads the body otherwise, so the "already activated"
path is unchanged. afterRequest itself is left alone because the
register flow relies on getting the onboarding response back to fire its
conversion events.

Sentry CLOUD-11: 65 events / 65 users in the last 30 days, 311 / 132
lifetime, all Firefox on /auth/activate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…apping-main

fix(instagram): map container error 2207085 to a curated video-format message
…oad-failure

fix(billing): show a message when Stripe.js fails to load instead of crashing
…dy-on-redirect

fix(auth): don't read the activate response body once the onboarding redirect started
Preview comments (inline, replies, resolve, anonymous + optional
reCAPTCHA), connected clients, schedule post preview url, plus fixes:
Instagram/Threads container error mapping, Telegram 400 -> BadBody,
media upload cancel-on-error, calendar deleted post group, Stripe.js
load failure message, extension chrome guards, nostr hex key signing.

Conflicts resolved:
- .env.example: union (Crove telemetry block + upstream reCAPTCHA and
  provider visibility docs; REDDIT_* stays removed)
- first.billing.component: keep the DOS checkout branch (shared billing)
  and add upstream's stripeFailed branch; Stripe only loads outside
  shared dos billing
- variable.context: keep brandConfig, add recaptchaSiteKey

Schema: Comments gains threading/anchor/resolved columns and a nullable
userId (anonymous). Beta DB is altered at deploy; the shared prod DB
goes through the DOS-Me migration ledger before the prod deploy.
merge: upstream main (42 commits - preview comments, connected clients, platform fixes)
return;
}
ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
setFlash(true);
Comment on lines +354 to +367
const toggleResolved = useCallback(async () => {
const response = await fetch(`/posts/comments/${comment.id}/resolve`, {
method: 'PUT',
body: JSON.stringify({ resolved: !resolved }),
});
if (!response.ok) {
toast.show(
t('preview_comment_failed', 'Could not post the comment'),
'warning'
);
return;
}
mutate();
}, [comment.id, resolved]);
Comment on lines +354 to +367
const toggleResolved = useCallback(async () => {
const response = await fetch(`/posts/comments/${comment.id}/resolve`, {
method: 'PUT',
body: JSON.stringify({ resolved: !resolved }),
});
if (!response.ok) {
toast.show(
t('preview_comment_failed', 'Could not post the comment'),
'warning'
);
return;
}
mutate();
}, [comment.id, resolved]);
Comment on lines +354 to +367
const toggleResolved = useCallback(async () => {
const response = await fetch(`/posts/comments/${comment.id}/resolve`, {
method: 'PUT',
body: JSON.stringify({ resolved: !resolved }),
});
if (!response.ok) {
toast.show(
t('preview_comment_failed', 'Could not post the comment'),
'warning'
);
return;
}
mutate();
}, [comment.id, resolved]);
Comment on lines +354 to +367
const toggleResolved = useCallback(async () => {
const response = await fetch(`/posts/comments/${comment.id}/resolve`, {
method: 'PUT',
body: JSON.stringify({ resolved: !resolved }),
});
if (!response.ok) {
toast.show(
t('preview_comment_failed', 'Could not post the comment'),
'warning'
);
return;
}
mutate();
}, [comment.id, resolved]);
Comment on lines +44 to +46
const load = useCallback(async () => {
return (await fetch(`/public/posts/${previewId}/comments`)).json();
}, [previewId]);
),
});
}),
[t]
setLoading(false);
}
},
[user?.id, recaptchaSiteKey, previewId, parentId, anchor, askForName]
return;
}
mutate();
}, [comment.id, resolved]);
const fetch = useFetch();
const load = useCallback(async () => {
return (await fetch(`/public/posts/${previewId}/comments`)).json();
}, [previewId]);

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a robust inline and public commenting system for post previews, complete with text-anchored comments, replies, resolution states, and reCAPTCHA protection. It also adds connected client statistics to the admin dashboard, improves error handling across various social media providers (Facebook, Instagram, Nostr, Telegram, Threads), and updates internationalization files. The review feedback highlights two important backend improvements in posts.service.ts: wrapping the external reCAPTCHA verification network request in a try-catch block to prevent unhandled 500 errors, and batching database queries to avoid an N+1 performance bottleneck when detaching stale anchors inside a loop.

Comment on lines +1476 to +1490
const result = await (
await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
secret: process.env.RECAPTCHA_SECRET_KEY,
response: token,
remoteip: ip,
}),
})
).json();

if (!result?.success) {
throw new BadRequestException('Captcha verification failed');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fetch call to Google's reCAPTCHA API is not wrapped in a try-catch block. If the external API is down or a network/DNS error occurs, fetch will throw a TypeError, resulting in an unhandled 500 Internal Server Error. Wrapping the network request in a try-catch block and throwing a BadRequestException ensures robust error handling.

    try {
      const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
          secret: process.env.RECAPTCHA_SECRET_KEY,
          response: token,
          remoteip: ip,
        }),
      });
      const result = await response.json();
      if (!result?.success) {
        throw new BadRequestException('Captcha verification failed');
      }
    } catch (error) {
      if (error instanceof BadRequestException) {
        throw error;
      }
      throw new BadRequestException('Captcha verification failed due to a network error');
    }

Comment on lines +1511 to +1514
for (const post of posts) {
const anchored = await this._postRepository.getAnchoredCommentsForPost(
post.id
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Executing database queries inside a loop (for (const post of posts)) results in N+1 queries, which can become a performance bottleneck as the number of posts grows. Consider batching the retrieval of anchored comments for all post IDs in a single query.

@JOY
JOY (JOY) merged commit 07dfeb4 into main Sep 28, 2026
215 of 218 checks passed
@JOY

Copy link
Copy Markdown
Author

ESLint code-scanning failure: same alert set accepted on #65 (react-compiler style rules + no-img-element in upstream's new preview-comments/auth files; the repo's own eslint.yml gate passes; merge-introduced files lint clean). Accepted per the documented precedent - this promotion carries exactly the #65 diff to prod.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants