Repository navigation
merge: upstream v2.25.1 (135 commits - billing redesign, settings tabs, creation modal, email queues) - #69
Conversation
Adds a pencil edit affordance on every row of the tags dropdown in the creation modal (spaced away from the delete x to avoid mis-clicks), wired to the existing ShowModal edit mode (PUT /posts/tags/:id). After saving, the tag list revalidates and any selected instance of the edited tag is remapped so chips and the post value reflect the new name/color without a reload. Uses onClose to resolve the modal promise so dismissing without saving restores the click-outside behavior. Adds a PencilIcon to the shared ui/icons (none existed) and the edit_tag translation key with lingo.dev translations for all locales. Tested e2e on a local run: edited a selected tag's name and color from the dropdown - the PUT persisted, the dropdown row and the selected footer chip updated immediately without a reload, re-saving the post kept it attached to the renamed tag, and dismissing the modal without saving restored the dropdown's click-outside close. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the mix-blend-difference treatment on the calendar tag bar (label and hover action icons) with the white-text + text-shadow-tags outline already used in the tags dropdown, so the bar stays readable for light, gray, and saturated tag colors alike. Posts without a tag color keep the bg-btnPrimary fallback unchanged. Tested e2e on a local run: reproduced the unreadable label on main with a mid-gray tag (white-on-gray difference blend is near-invisible for the label and the hover icons), then verified on this branch that the label and all hover action icons are crisply readable on mid-gray, pastel, saturated red, and purple tags, with before/after screenshots of the same post and color. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wrap the async bodies of editTag and deleteTag in try/finally so setAllowClose(true) runs even if mutate or the delete request rejects; otherwise a failed request left the dropdown's click-outside close permanently disabled until a page reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nnel Reconnecting a channel resolved the auth result with refreshToken: body.refresh - a field that is empty on the OAuth callback - so every reconnect overwrote the stored refresh token with an empty string and set a ~31 year token expiration. From that moment the channel worked only while the fresh access token lived (~1 hour for YouTube/GMB): the next refresh attempt failed before even reaching the provider, the channel was flagged for reconnection, and the next reconnect wiped the token again - a permanent loop that no amount of re-authorizing could escape. - store auth.refreshToken / auth.expiresIn from the new authentication in the reconnect branch (reConnect stays identity-verification only) - guard the repository update so an empty refresh token can never overwrite a stored one - log the real error when a refresh or post-refresh reConnect fails instead of swallowing it, and pass it into the user notification - add an optional refreshErrorMessage provider hook; YouTube maps Google's invalid_rapt (Workspace session policy) to a dedicated message telling the user how to exempt Postiz - YouTube reConnect no longer goes through pages(), which swallowed the real Google error and reported any failure as 'Channel not found' - drop the duplicated disconnect notification (refreshProcess notified once directly and again via disconnectChannel) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds GET /public/v1/media?page=&search= and a read-only mediaListTool so agents can reuse already-uploaded media paths as attachments instead of re-uploading. getMedia select now also returns type, fileSize, createdAt. Tested: unauthenticated request returns 401; with an API key pagination (pages count, page 2), filename search and the new fields verified; mediaListTool appears in the MCP tools list, returns the same rows, and a draft scheduled through the MCP with a listed path stores that path as the post image. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A negative page produced a negative Prisma skip and a 500. GetMediaDto (same shape as GetNotificationsDto) now rejects non-numeric or < 1 values with 400 and defaults to 1. Tested: page=abc / 0 / -3 return 400 with validation messages, page=1.5 parses to 1, no page / page=2 / search unchanged, unauthenticated still 401. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Onboarding checkout now refuses to mint a new subscription when the org's Stripe customer already has a live one, the created webhook cancels a duplicate, the deleted webhook keeps the org on a surviving subscription, and in-app cancel acts on all live subscriptions.
…g for the webhook POST /billing/cancel toggles Stripe's cancel_at_period_end, but only the customer.subscription.updated webhook wrote cancelAt to the Subscription row. When that webhook was lost, delayed or dropped by the handler, the billing page showed "Cancel subscription" again after a reload, and the next click toggled the subscription back to active, so the customer was billed at renewal after cancelling. - setToCancel writes cancelAt right after the Stripe update succeeds (the cancel date on cancel, null on reactivate) through a new updateCancelAt service/repository method. The webhook still runs and writes the same value. - The "Subscription Cancelled" email is sent after the call and only when the result carries a cancel date, so reactivations no longer send it. - Both billing page toasts are chosen from the response. When the click did the opposite of the button, the toast says so and tells the user to click again. The endpoint stays a toggle and the failed-payment immediate cancel is unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The calendar error tooltip always showed the generic 'An error
occurred while publishing this post' because getPosts never selected
the error column. Select it, but surface ONLY curated provider
messages: bad_body failures whose message came from a provider
handleErrors mapping or a hand-written provider throw. Unmapped API
responses carry the 'Unknown Error' placeholder at the point of
throwing, so anything else - raw API bodies, unexpected exceptions,
internal workflow strings like 'Refresh channel needed' - keeps the
generic tooltip exactly as today.
Curated messages are stored in post.error as {"message":"..."} so
the frontend can distinguish them from everything the column held
until now (internal strings, serialized ActivityFailure dumps), which
all render generic. Internal strings are still stored for debugging
and the raw failure still goes to the Errors table. The
checkPostStatus/finalizePost developer-guard throws are demoted to the
unmapped placeholder (detail moved to the json field) so they can
never surface either. The tooltip is capped at 400px with wrapping.
Verified e2e through real Temporal v1.0.6 workflows on a local run:
a real Pinterest 'Board not found' response surfaced its handleErrors
mapping; an unmapped bad_body, an unexpected exception, a
refreshNeeded early-exit string, and a seeded legacy serialized-blob
row all rendered the generic tooltip (checked in the rendered
calendar DOM).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…edly changeState only wrote the error column when it had a curated message to store, so a post that first failed with a curated bad_body message and then failed again for an uncurated reason (a token refresh failure, an unexpected exception) kept the old value. The calendar tooltip then showed the earlier, unrelated error - exactly the misleading-message problem this branch set out to fix. Overwrite the column on every failure instead: curated messages and internal strings are stored as before, and anything uncurated clears it so the tooltip falls back to the generic text. The full failure is still recorded in the Errors table, so nothing is lost for debugging. Reported by Sentry's PR review bot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Post.error keeps persisting the raw serialized failure; curation moved to getPosts so the calendar and public API both get the curated message or null, retroactively for existing rows.
The repository now returns a curated error message (or null) for every post, so the MCP List Posts tool passes it through as an "error" field and declares it in the output schema, matching the public GET /posts response. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XvHgEEangtx46CmTGCrnL1
… hold all of them When a subscription lapses (trial ends or is cancelled), the FREE downgrade disables every channel. Subscribing again never re-enabled them, so a paying customer landed on a plan with all channels still disabled and the app looking broken. On a paid subscription change, if some non-deleted channels are disabled and the plan's totalChannels can hold all of them, enable everything back. When they don't all fit, nothing is enabled, since there is no way to tell auto-disabled channels from ones the user disabled on purpose. Idempotent: once everything is enabled the branch is a no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KYTuLLhYQsLCdgxBFEXpG9
…l the user after checkout Re-enabling all channels on any subscription create/update also undid channels the user disabled on purpose whenever a same-plan update arrived (renewal, cancel toggle, store re-sync). The enable-all now also requires the new channel limit to be larger than the current subscription's, which still covers trial end, resubscribing (no subscription row, limit 0) and upgrades. After returning from checkout, a notice on the calendar tells the user whether all their channels were enabled again or stay disabled because the plan does not cover them. It opens after the onboarding modal so it is shown on top of it and is not removed by the onboarding close. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…extensions Tag events whose throwing callback has no frame from our /_next/ bundle as third_party_code and do not open the report dialog for them. Frames of Sentry's own callback wrapper are skipped, and frameless "Could not establish connection" rejections are treated as extension noise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… name Adds a nullable customName column on Integration, PUT /integrations/:id/custom-name, and Rename channel / Reset to original name options in the channel menu. Channel lists (app, public API, agent) return the custom name when set. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153xf1m3VSQnd9QrMLhDikq
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # apps/backend/src/api/routes/integrations.controller.ts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153xf1m3VSQnd9QrMLhDikq
Adds a hover-only globe icon to calendar items that opens the post's releaseURL in a new tab. Shown only for non-recurring PUBLISHED posts with a valid http URL, so rescheduled/re-drafted and failed posts hide it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ve a URL A post can end in ERROR after the platform accepted it (a thread comment failed after the parent was published, a follow-up mutation was rejected): its releaseURL is kept, so let the user reach the live post instead of re-publishing it.
…e opener Reddit, Lemmy and Farcaster join several release URLs with commas, so the open-post icon opened one mangled URL; open the first target instead. Also pass noopener,noreferrer so the cross-origin page cannot reach the dashboard tab.
The open-post icon now asks the backend for the post's current release URL on every click instead of opening the URL loaded with the calendar. New GET /posts/:id/release-url calls PostsService.getReleaseURL, which runs the provider's optional resolveReleaseId through resolveRelease (the same generic step checkPostAnalytics uses), persists a newly resolved id and URL, and returns the URL. Providers without a resolver return the stored URL with no platform call. This covers platforms that only expose the public post URL some time after the publish completes (TikTok stores the profile URL until then): the icon opens the real post as soon as the platform has it, and the stored URL is corrected at the same time. Also tightens the icon gap in the calendar item header from 10px to 6px so the extra icon fits narrow week-view items. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…view and Duplicate Complements gitroomhq#2204: the hover icons now end with Preview, Duplicate, Delete, so Duplicate sits between Preview and Delete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(calendar): put Delete right-most in the post hover bar, after Preview and Duplicate
…cial connect Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ct-auth-failure fix(integrations): log the error behind "Authentication failed" on social connect
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Cancelling a subscription that includes team members disables the non-owner members (userOrganization.disabled = true) without removing them. The delete-account guard counted those disabled rows too, so an owner with a cancelled plan (stuck on the billing page, unable to reach the team settings to remove anyone) could never delete their account. getTeam now selects the member's disabled flag and the guard only blocks on active members other than the owner. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…gnore-disabled-members fix(account): ignore disabled team members when deleting an account
…-session-observability fix(integrations): reconnecting a channel wiped its stored refresh token
Billing page redesign (plan cards + invoice history), settings reorganisation (Plugs / Integrations / Apps / Authentication tabs, old /plugs and /third-party routes redirect), new post creation modal with auto RTL text direction, calendar channel filter / published frame / open-post link / curated error tooltip / batch schedules, channel rename (customName), tag editing, media list in the public API and MCP (mediaListTool), superadmin AI-credit reset endpoint. Fixes: Stripe duplicate subscriptions and cancel toggle, channels re-enabled on plan upgrade, refresh-channel limit check, reconnect keeps the new refresh token, Google Workspace invalid_rapt message, Instagram trial reel limit. Infra: one email workflow per email on rate-limited email / email-bulk task queues (auth emails sent synchronously), autopost / missing-post continue-as-new behind patched(), streakWorkflowV2. Conflicts resolved: - CLAUDE.md: union (upstream's webhook no-throw rule appended) - package.json: keep the fork's `pnpm exec prisma` scripts, take upstream's removal of --accept-data-loss from prisma-db-push - billing.controller: keep the DOS shared billing branch of /cancel (feedback email once dos.me accepts the cancel); Stripe path is upstream's "email only when cancel_at is set" - main.billing.component: upstream's redesign, with the DOS mode gating re-applied (monthly-only Plus/Pro cards, yearly toggle hidden, portal/cancel box behind showPortalAndCancel, invoice history hidden because dos.me owns the invoices, 3-column grid for the 3 DOS plans, upstream's cancel-toggle toasts skipped since a dos.me cancel never toggles) - load.tools.service: keep the fork's agentId + tool aliases, add upstream's coreToolNames() - email.service: union imports - plugs / third-party pages: upstream redirects (the pages moved into Settings, so the fork's branded metadata no longer renders) Schema: new nullable Integration.customName, Post.batchId and Post_batchId_idx. Additive, needs the DDL on beta and prod before the new code serves traffic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFwPKMsHCF5VarWHQAsHtJ
…message The branding guard blocked upstream's new uploadFromUrlTool / uploadWidgetTool descriptions: they send agents to docs.postiz.com and to the upstream CLI (`postiz upload`), which defaults to the upstream cloud, so following the hint would push a user's private file to the wrong instance. They now point at this deployment's public API (POST /public/v1/upload) and brand.docsUrl. YouTube's new invalid_rapt refresh message asks a Google Workspace admin to trust the app by name, so it uses the deployment brand instead of "Postiz". docs/fork-delta.md records these and the billing divergences carried through the v2.25.1 sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFwPKMsHCF5VarWHQAsHtJ
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
There was a problem hiding this comment.
ESLint found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
|
Update: the code-scanning findings on this PR are now fixed. This replaces my earlier note here, which said the ESLint alerts would be left as they were.
Locally, ESLint and CodeQL both report 0 findings starting on changed lines. Frontend and backend typecheck, Generated by Claude Code |
There was a problem hiding this comment.
Code Review
This pull request introduces significant enhancements to the billing, subscription, and post editor systems, including a new billing history view, inline image support in the editor, mobile-responsive UI components like bottom sheets, and optimized Temporal workflows using continue-as-new. The code review identified several critical robustness issues: a potential runtime error when accessing nested user arrays without optional chaining, a missing Day.js UTC plugin extension that could cause crashes, an unhandled ChangeEvent case in the editor's drag-and-drop handler, a potential RangeError in currency formatting, and a regex limitation that strips images using single quotes.
| (f) => !f.users[0].disabled | ||
| ); | ||
| return (await this._orgService.getOrgsByUserId(user.id)) | ||
| .filter((f) => !f.users[0].disabled) |
There was a problem hiding this comment.
There was a problem hiding this comment.
Leaving this as is: users[0] can't be missing here. getOrgsByUserId only returns orgs where users: { some: { userId } } holds, and its include filters users to that same userId, so every returned org has exactly that row at users[0]. The f.users[0].disabled filter was already in this file before the sync; upstream only added the .map that narrows the fields.
Generated by Claude Code
|
|
||
| import React, { FC, useCallback, useMemo, useState } from 'react'; | ||
| import useSWR from 'swr'; | ||
| import dayjs from 'dayjs'; |
There was a problem hiding this comment.
The dayjs.utc method is used in formatDate but the utc plugin is not imported or extended in this file. This will cause a runtime crash (TypeError: dayjs.utc is not a function) if the plugin hasn't been initialized globally. Import and extend the utc plugin to ensure it works reliably.
| import dayjs from 'dayjs'; | |
| import dayjs from 'dayjs'; | |
| import utc from 'dayjs/plugin/utc'; | |
| dayjs.extend(utc); |
There was a problem hiding this comment.
Leaving this as is, it doesn't reproduce. BillingHistory is only rendered from main.billing.component.tsx, which imports newDayjs from components/layout/set.timezone.tsx. That module runs dayjs.extend(utc) at import time, before any render, and main.billing.component.tsx itself already relies on dayjs.utc the same way. Adding a per-file extend would add fork delta to an upstream file without changing behaviour.
Generated by Claude Code
| if (inlineImages) { | ||
| // uploads land at the cursor, move it to where the files were dropped | ||
| const editor = editorRef?.current?.editor; | ||
| const { clientX, clientY } = event as DragEvent; | ||
| const position = editor?.view?.posAtCoords({ | ||
| left: clientX, | ||
| top: clientY, | ||
| }); | ||
| if (position) { |
There was a problem hiding this comment.
When files are uploaded via the file picker (clicking the button) instead of drag-and-drop, the event is a ChangeEvent rather than a DragEvent. This means clientX and clientY will be undefined, and passing them to posAtCoords can cause a runtime crash. Add a guard to ensure these coordinates are valid numbers before calling posAtCoords.
| if (inlineImages) { | |
| // uploads land at the cursor, move it to where the files were dropped | |
| const editor = editorRef?.current?.editor; | |
| const { clientX, clientY } = event as DragEvent; | |
| const position = editor?.view?.posAtCoords({ | |
| left: clientX, | |
| top: clientY, | |
| }); | |
| if (position) { | |
| if (inlineImages) { | |
| // uploads land at the cursor, move it to where the files were dropped | |
| const editor = editorRef?.current?.editor; | |
| const { clientX, clientY } = event as DragEvent; | |
| if (typeof clientX === 'number' && typeof clientY === 'number') { | |
| const position = editor?.view?.posAtCoords({ | |
| left: clientX, | |
| top: clientY, | |
| }); | |
| if (position) { | |
| editor.commands.focus(position.pos); | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
Leaving this as is: there is no file-picker path into this onDrop. The editor only spreads getRootProps() (editor.tsx:939) and never renders getInputProps(). With react-dropzone's default useFsAccessApi: false, a click on the root finds no input and opens nothing. So onDrop only fires from a real drop, whose event is a DragEvent with numeric clientX/clientY. Uploads from the media button go through the Uppy uploader, not this handler.
Generated by Claude Code
| const formatAmount = (amount: number, currency: string) => { | ||
| const formatter = new Intl.NumberFormat('en-US', { | ||
| style: 'currency', | ||
| currency: currency.toUpperCase(), | ||
| }); | ||
| return formatter.format( | ||
| amount / 10 ** (formatter.resolvedOptions().maximumFractionDigits ?? 2) | ||
| ); | ||
| }; |
There was a problem hiding this comment.
The Intl.NumberFormat constructor will throw a RangeError if the currency parameter is invalid or unsupported. Since this value comes from external API data, wrap the formatting logic in a try-catch block to prevent a rendering crash.
const formatAmount = (amount: number, currency: string) => {
try {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase(),
});
return formatter.format(
amount / 10 ** (formatter.resolvedOptions().maximumFractionDigits ?? 2)
);
} catch (e) {
return `${currency.toUpperCase()} ${(amount / 100).toFixed(2)}`;
}
};
There was a problem hiding this comment.
Leaving this as is. currency comes from StripeService.getInvoices, i.e. Stripe's invoice.currency, which is always a lowercase ISO 4217 code that Intl.NumberFormat accepts. There is no realistic input that makes it throw, so a fallback formatter here (in an upstream file) would be code without a trigger.
Generated by Claude Code
| export const convertImages = (value: string) => { | ||
| return value.replace(/<img\b[^>]*>/gi, (match) => { | ||
| const src = /\ssrc="(https?:\/\/[^"]*)"/i.exec(match)?.[1]; | ||
| const alt = /\salt="([^"]*)"/i.exec(match)?.[1]; |
There was a problem hiding this comment.
The regular expressions for extracting src and alt only match double quotes. If the HTML uses single quotes (e.g., <img src='...'>), the image will be completely stripped. Update the regex to support both single and double quotes.
| export const convertImages = (value: string) => { | |
| return value.replace(/<img\b[^>]*>/gi, (match) => { | |
| const src = /\ssrc="(https?:\/\/[^"]*)"/i.exec(match)?.[1]; | |
| const alt = /\salt="([^"]*)"/i.exec(match)?.[1]; | |
| export const convertImages = (value: string) => { | |
| return value.replace(/<img\b[^>]*>/gi, (match) => { | |
| const src = /\ssrc=["'](https?:\/\/[^"']*)["']/i.exec(match)?.[1]; | |
| const alt = /\salt=["']([^"']*)["']/i.exec(match)?.[1]; |
There was a problem hiding this comment.
Leaving this as is. Post content goes through sanitizePostContent (DOMPurify, applied in CreatePostDto) before it is stored, and DOMPurify always serialises attributes with double quotes. So a single-quoted <img> never reaches convertImages. The helper is also a deliberate allow-list (only an http(s) src plus the alt), so dropping anything that doesn't match is the safe failure mode, and widening the quote handling would cut against the quote checks it already does.
Generated by Claude Code
The ESLint code-scanning gate failed on 23 react-hooks errors in lines the upstream merge brought in. Fixed without changing behaviour: - useCallback deps completed where the compiler inferred more than the array listed (date picker changeTime, channel rename save, channel filter, customer selector, composer date picker, team invite submit) - teams AddMember declares `t` before the callback that uses it - the composer toolbar gets the TipTap editor from state, reported by a new optional `onEditor` prop on OnlyEditor, instead of reading editorRef while rendering; inlineImagesRef is updated in an effect - the editor rebuild on an extension change and the calendar channel filter reset on a customer switch adjust state while rendering instead of calling setState synchronously in an effect (no render with the stale value any more) Recorded in docs/fork-delta.md as a candidate to send upstream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFwPKMsHCF5VarWHQAsHtJ
…t fixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFwPKMsHCF5VarWHQAsHtJ
CodeQL flagged js/tainted-format-string (high) on the log upstream v2.25.1 added to the social connect error path: the `integration` route parameter was interpolated into console.log's format string while the error was passed as an extra argument, so a crafted path segment with %-placeholders could reshape the log line. The parameter is now a plain argument, the same fix the fork already carries in clipping.service. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFwPKMsHCF5VarWHQAsHtJ
The ESLint code-scanning bot left 12 warnings on lines the upstream merge brought in. All cleared, none left on changed lines: - missing hook dependencies added (channel rename / reset, post-checkout channel notice, inline-images sync, media delete / pick / library); each listed value is stable or a prop, and the effects involved are guarded, so nothing re-runs in a loop - manage.modal memoises existingPosts and channel, so the composer's delete and save callbacks can list them (and customClose, fetch, modal, mutate, t, toaster) instead of closing over the first render - the channel filter avatar and the YouTube badge use the repo's ImageWithFallback / SafeImage instead of a bare <img> (the badge also gets its alt text) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFwPKMsHCF5VarWHQAsHtJ
The CodeQL gate's high alert was js/double-escaping, not the log line: upstream v2.25.1 reformatted the html branch of stripHtmlValidation (inline images), which made the long-standing "& decoded before " / / '" order count as new code. A post containing the literal text " (stored as &quot;) came out as a bare quote. & is now decoded last, so literal entities survive and the normal ones are still decoded. A local CodeQL run (javascript-code-scanning suite, base vs head) confirmed this was the only alert starting on a changed line, so the console.log change from 813f4d5 is reverted to upstream's line - it was never flagged and only added fork delta. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFwPKMsHCF5VarWHQAsHtJ
What kind of change does this PR introduce?
Upstream sync (Feature + Bug fix) across backend, frontend, orchestrator and the Prisma schema. It merges
gitroomhq/postiz-app8e42f09c..91c91f63(= tagv2.25.1, 135 commits since the v2.25.0 sync in #67) intodev. Follow-up commits fix the brand leaks the merge brought in and clear the ESLint and CodeQL code-scanning findings on the merged lines. Fork surfaces stay as they were: DOS shared billing inbilling.controller.tsandmain.billing.component.tsx, the branding engine, MCP tool aliases, and the DOS ID SSO.Why was this change needed?
Upstream shipped v2.25.1. Per ADR-0001, the fork keeps tracking upstream so it gets provider fixes, billing and Stripe hardening, and new MCP and calendar features, without losing its own billing and brand surfaces.
Technical Details & Scope
Upstream highlights
GET /billing/invoices).cancelAt, channels are re-enabled when a plan upgrade can hold them all, webhook failures are logged instead of swallowed./plugsand/third-partynow redirect into Settings.Post.batchId), AM/PM in the date picker.Integration.customName), edit tags from the tags dropdown.GET /public/v1/mediaand an MCPmediaListTool.SuperAdminGuard).sendSingleEmailWorkflow, on the rate-limitedemail/email-bulktask queues. Auth emails (activation, password reset) are sent synchronously. The oldsendEmailWorkflowstays registered so the running one can drain.patched(). Streak moved tostreakWorkflowV2. All of these follow the versioning rules.internalId, YouTube Google Workspaceinvalid_raptmessage, Instagram trial reel limit error, TikTok post id persistence, X, Threads, Listmonk and WordPress fixes./user/organizationsreturn only the fields their callers need.Conflicts resolved
CLAUDE.md: union. Upstream's rule that webhook code must never throw is appended.package.json: kept the fork'spnpm exec prismascripts and took upstream's removal of--accept-data-lossfromprisma-db-push. This matches the fork's own audit item C2.billing.controller.ts/cancel:cancel_atis set.main.billing.component.tsx: took upstream's redesign and re-applied DOS mode on top:PlanCardlabelpropshowPortalAndCancelBillingHistoryhidden, and the portal button keeps "/ Invoices History", because invoices live in dos.meload.tools.service.ts: kept the fork'sagentIdand tool aliases, added upstream'scoreToolNames(). The self-hosted relay now checks only canonical tool names, so the fork's aliases no longer show up as "missing tools" on a self-hosted instance.email.service.ts: union of imports.plugs/third-partypages: took upstream's redirects. The fork's branded metadata no longer renders there.Follow-up commits
fix(sync)):uploadFromUrlTool/uploadWidgetToolhints. They pointed agents atdocs.postiz.comand at the upstream CLI, which defaults to the upstream cloud. They now point atPOST /public/v1/uploadandbrand.docsUrl.react-hookserrors and 12 warnings on merged lines. All cleared:tdeclared before use inteams.component.tsxOnlyEditoronEditorprop) instead of readingeditorRefwhile renderingmanage.modal.tsxmemoisesexistingPosts/channel<img>replaced with the repo'sSafeImage/ImageWithFallbackjs/double-escaping(high, 7.8), inlibraries/helpers/src/utils/strip.html.validation.ts:stripHtmlValidationdecoded&before"/ /', so the literal text"(stored as&quot;) came out as a bare quote.&is now decoded last.javascript-code-scanningsuite, base vs head). It is the only alert that starts on a changed line.docs/fork-delta.md.Schema (deploy gating): two new nullable columns plus an index. Additive only: no existing column changes meaning. Beta and the shared prod DB need this DDL before the new code serves traffic. Prod goes through the DOS-Me ledger as before.
Ops: the orchestrator now also polls the
emailandemail-bulktask queues. Do not list them inEXCLUDE_QUEUE, or emails stop sending.Verification & Testing
pnpm install --frozen-lockfile: lockfile unchanged, since upstream changed no dependencies.tsc --noEmit: clean on backend and orchestrator (tsconfig.build.json) and on frontend.pnpm run build(frontend + backend + orchestrator): passes.build:frontendwas re-run after the lint fixes and passes.npx tsx scripts/branding-guard.ts: passes. Before thefix(sync)commit it failed with 6 leaks.apps/frontend/apps/backend: 0 errors and 0 warnings on changed lines, down from 23 errors and 21 warnings.javascript-code-scanningsuite on base and head: 0 alerts start on a changed line, down from 1.stripHtmlValidation('html', …)spot-checked with tsx:Tom & Jerry→Tom & Jerry&quot;→ literal""q"→"q"pnpm run test:frontend: 14/14 pass.--accept-data-loss: 58/58 pass.QA
/billing./billing, click "Cancel subscription" and submit at least 20 characters of feedback. Expect "Subscription set to canceled successfully", or no toast if dos.me has not reported the cancel yet. Never expect "already set to cancel, so it has been reactivated". TheEMAIL_FROM_ADDRESSinbox gets one "Subscription Cancelled" email./plugsand/third-party. Expect redirects to/settings?tab=plugsand/settings?tab=integrations. Settings shows the Integrations, Plugs, Apps tabs and an Authentication box ("Single sign-on") under Global Settings.send_email_<uuid>workflows on theemail/email-bulkqueues./mcp/<api key>and list the tools.uploadFromUrlToolmust mentionPOST /public/v1/uploadand the deployment's docs URL, with nodocs.postiz.comand nopostiz upload.mediaListToolis listed.Tom & Jerryand the literal characters". The published post showsTom & Jerryand the literal", not a bare".Checklist:
pnpm run build).pnpm dlx tsx scripts/branding-guard.ts).Not changed here, needs a decision (pre-existing): in DOS mode, "Reactivate subscription" calls
/billing/cancel, which always cancels on dos.me and never toggles. On top of that,syncOrgstorescurrent_period_endascancelAtfor every active DOS plan, so paid DOS orgs may see the Reactivate button and the "will be canceled" banner. Whether this is real depends on how dos.me's cancel endpoint and the period end behave, so it is left for a separate fix.🤖 Generated with Claude Code
https://claude.ai/code/session_01XFwPKMsHCF5VarWHQAsHtJ