Repository navigation
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
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
merge: upstream v2.25.1 (135 commits - billing redesign, settings tabs, creation modal, email queues)
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.
Code Review
This pull request introduces several enhancements and bug fixes across the application, focusing on billing history, integration management, mobile responsiveness, and Temporal workflow optimizations. Key updates include a new billing history component, bottom sheets for mobile settings, custom channel renaming, and a new media list tool. However, several critical issues were identified in the review: a casing mismatch in the billing controller that prevents cancellation emails, potential runtime TypeErrors due to unsafe property access on potentially empty arrays or undefined URLs, a context loss issue in the drag-and-drop provider, and a potential server-side rendering crash when accessing window.matchMedia directly.
| return (await this.provider(org)).setToCancel(org.id); | ||
| const result = await (await this.provider(org)).setToCancel(org.id); | ||
|
|
||
| if (result.cancel_at) { |
There was a problem hiding this comment.
The setToCancel method in StripeService returns an object with cancelAt (camelCase), but here the code checks result.cancel_at (snake_case). This mismatch causes the condition to always evaluate to false, preventing the cancellation email notification from being sent.
| if (result.cancel_at) { | |
| if (result.cancelAt) { |
There was a problem hiding this comment.
Leaving this as is. StripeService.setToCancel returns { id, cancel_at } in snake_case on all three paths: un-cancel, immediate cancel after a failed payment, and cancel at period end (stripe.service.ts). So result.cancel_at is the right key and the email is sent whenever a cancel is set. Switching to cancelAt would stop the email.
Generated by Claude Code
| backend[name] = (event: DragEvent) => { | ||
| if (!isInsideEditor(event)) { | ||
| handler(event); | ||
| } | ||
| }; |
There was a problem hiding this comment.
If the drag handlers in HTML5Backend are prototype methods (which is standard for class methods in JS/TS), calling handler(event) directly as a plain function will invoke it with this set to undefined. This will cause a runtime TypeError when the handler tries to access internal properties of the backend. To preserve the correct context, use handler.call(backend, event) instead.
| backend[name] = (event: DragEvent) => { | |
| if (!isInsideEditor(event)) { | |
| handler(event); | |
| } | |
| }; | |
| backend[name] = (event: DragEvent) => { | |
| if (!isInsideEditor(event)) { | |
| handler.call(backend, event); | |
| } | |
| }; |
There was a problem hiding this comment.
Leaving this as is. In the react-dnd-html5-backend version installed here, HTML5BackendImpl assigns these handlers as instance arrow functions in its constructor (this.handleTopDragStart = (e)=>{…}, dist/HTML5BackendImpl.js:323). They are not prototype methods, so this is already bound lexically and calling handler(event) keeps the backend context. The backend itself registers them the same way, via addEventListener('dragstart', this.handleTopDragStart).
Generated by Claude Code
| export const mediaLibrarySize = () => | ||
| window.matchMedia('(max-width: 1025px)').matches | ||
| ? '100%' | ||
| : 'calc(100% - 80px)'; |
There was a problem hiding this comment.
Accessing window.matchMedia directly during server-side rendering (SSR) will throw a ReferenceError: window is not defined and crash the page. Ensure window is defined before accessing it.
| export const mediaLibrarySize = () => | |
| window.matchMedia('(max-width: 1025px)').matches | |
| ? '100%' | |
| : 'calc(100% - 80px)'; | |
| export const mediaLibrarySize = () => | |
| typeof window !== 'undefined' && window.matchMedia('(max-width: 1025px)').matches | |
| ? '100%' | |
| : 'calc(100% - 80px)'; |
There was a problem hiding this comment.
Leaving this as is. mediaLibrarySize() is never called during render. Every caller runs it inside a click handler or an effect that opens the media modal, and all of them are in 'use client' components:
showModal/openMediacallbacks inmedia.component.tsxanddeveloper.component.tsx- the
show-modalemitter listener registered in auseEffect
So it only runs in the browser and never during SSR.
Generated by Claude Code
| // multi-target posts (several subreddits / communities / channels) | ||
| // join their URLs with commas: open the first one | ||
| tab.location.href = (release.releaseURL || post.releaseURL).split(',')[0]; |
There was a problem hiding this comment.
If both release.releaseURL and post.releaseURL are null or undefined, calling .split(',') on them will throw a TypeError. Ensure the URL is defined before splitting it.
const targetURL = release.releaseURL || post.releaseURL;
if (targetURL) {
tab.location.href = targetURL.split(',')[0];
} else {
tab.close();
toaster.show(
t('post_has_no_public_link', 'This post has no public link'),
'warning'
);
}
There was a problem hiding this comment.
Leaving this as is. The open-post icon that calls openPost only renders when post.releaseURL?.startsWith('http') (calendar.tsx, around line 1329). So post.releaseURL is always a non-empty string when this runs, and (release.releaseURL || post.releaseURL) can't be null or undefined. The cases with no release URL return earlier with the "no public link", "reconnect" and "pending" toasts.
Generated by Claude Code
| (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.
Leaving this as is, and it was already answered on #69. getOrgsByUserId only returns orgs where users: { some: { userId } } matches, and its include filters users to that same userId. So every returned org has that row at users[0]. This filter also predates the sync.
Generated by Claude Code
| setChannelDate( | ||
| sibling.integration, | ||
| dayjs.utc(sibling.posts[0].publishDate).local() | ||
| ); |
There was a problem hiding this comment.
There was a problem hiding this comment.
Leaving this as is. siblings is built in PostsService from the batch groups, and those groups are filtered with batchPosts.length && … before being mapped through groupForEditor. So every sibling has at least one post, and sibling.posts[0] is always defined.
Generated by Claude Code
| // a copied picture also comes as html linking to the site it was copied | ||
| // from, so the file wins unless the html has text of its own | ||
| // @ts-ignore | ||
| const html = event.clipboardData.getData('text/html'); |
There was a problem hiding this comment.
If event is a File[] array rather than a ClipboardEvent, event.clipboardData will be undefined, and calling getData on it will throw a TypeError. Use optional chaining to safely access clipboardData.
| const html = event.clipboardData.getData('text/html'); | |
| const html = event?.clipboardData?.getData?.('text/html'); |
There was a problem hiding this comment.
Leaving this as is. When paste gets a File[], event.clipboardData is undefined, so clipboardItems = event.clipboardData?.items is undefined and the handler returns false a few lines above. The getData('text/html') call is only reached when clipboardData exists.
Generated by Claude Code
What kind of change does this PR introduce?
Release promotion (
dev→main) for the prod deploy. It carries exactly the reviewed diff of #69, with no new commits. That is the upstream Postiz v2.25.1 sync (135 commits) plus the fork-side follow-ups:js/double-escapingfix instripHtmlValidationOnce merged,
build-containers.ymlpublishesghcr.io/dos/crove-post:latest. Nothing else changes for the running prod stack untildocker compose -f scripts/docker-compose.prod.yaml up -dis run on crove-server.Why was this change needed?
Production deploys from
main(:latestimage). #69 is merged and green ondev, so this promotes it.Technical Details & Scope
See #69 for the full write-up, covering upstream highlights, conflict resolutions and follow-up commits. The things that matter for this deploy:
PROD DB prerequisite: run before
compose up. These are two new nullable columns plus an index, all additive. The new code reads them, so prod must have them before the new image serves traffic. They go through the DOS-Me ledger, as with the v2.25.0 table.Temporal / email: emails now run as one
sendSingleEmailWorkflowper email, on the rate-limitedemailandemail-bulktask queues. The orchestrator must poll both queues, so do not add them toEXCLUDE_QUEUE. The oldsendEmailWorkflowstays registered so the running instance can drain.Other workflow changes: autopost and missing-post now continue-as-new behind
patched(), and streak moved tostreakWorkflowV2. All of these are versioned upstream changes, so in-flight runs replay safely.Unchanged: the DOS shared billing flows (dos.me checkout, portal and cancel), DOS ID SSO, the branding engine, and the MCP tool aliases.
Verification & Testing
devataa4172ca(the merge: upstream v2.25.1 (135 commits - billing redesign, settings tabs, creation modal, email queues) #69 merge commit), all of these pass: Build, Branding Guard, ESLint, Code Quality Analysis (CodeQL).Build & Publish Crove Containers(:beta) was still running when this PR was opened.6114dc54, including the ESLint and CodeQL code-scanning gates.pnpm run buildQA
\d "Integration"listscustomName, and\d "Post"listsbatchIdandPost_batchId_idx.Build & Publish Crove Containersrun onmainsucceeds and pushesghcr.io/dos/crove-post:latest.docker compose -f scripts/docker-compose.prod.yaml up -d. Thecrove-postcontainer restarts on the new image, and its logs show no Prisma "column does not exist" errors./billing. Expect Free / Plus $9 / Pro $19 in one row, no Monthly/Yearly toggle, and no "Billing history" table. "Update Payment Method / Invoices History" opens the dos.me portal.send_email_<uuid>workflows on theemail/email-bulkqueues./launches. Rename a channel from its menu, filter the calendar by two channels, then switch the customer. The custom name shows, and the channel filter resets for the new customer./plugs. Expect a redirect to/settings?tab=plugs.Checklist:
pnpm run build).pnpm dlx tsx scripts/branding-guard.ts).🤖 Generated with Claude Code
https://claude.ai/code/session_01XFwPKMsHCF5VarWHQAsHtJ
Generated by Claude Code