Conversation
…and widget Turns a YouTube video into captioned vertical clips that land in the media library and as draft posts. Adds the Clipping / ClippingClip models, the clipping_minutes credit type, clippingWorkflow + per-clip child workflows, the /clipping REST routes, the MCP clipping tools and the ui://postiz/clipping status widget. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- createClips takes the row of the clipping before looking for clips, so a timed out attempt and its retry can no longer both store a set - a clip is claimed (draftedAt off null) before its draft is created and released when creation fails, so a retry cannot draft it twice - urls are stripped from the logged processor failure - Turkish billing label reads correctly after the number Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…time createPost writes one post per channel, so a failure on a later channel left the earlier drafts in place while the released claim let a retry create them again. The claim now stays, the free slot is looked up before it is taken, and each channel is drafted on its own so one failing channel does not drop the rest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat(clipping): YouTube video clipping workflow (REST + MCP tools + widget)
…delta inventory, CLAUDE.md corrections - ADR-0001: record verified upstream sync policy (pnpm/App Router/SWR+Zustand structure originated upstream, commit 4ba5156 contained in upstream/main) - daily sync corridor stays open for backend and frontend - docs/fork-delta.md: inventory of deliberate fork divergence (owned paths, diverging shared files, planned divergence, frozen contracts) - docs/refactor/minimal-batch.md + minimal-batch-vi.html: approved minimal batch plan (foundation safety + test/docs baseline, UI polish deferred) - CLAUDE.md: fix stale facts (frontend is Next.js 16 App Router not Vite, tailwind.config.cjs, logic lives in libraries/nestjs-libraries, component inventory) and add layout map - ROADMAP.md and CHANGELOG.md point to the batch
# Conflicts: # .env.example # libraries/nestjs-libraries/src/chat/load.tools.service.ts # libraries/nestjs-libraries/src/chat/start.mcp.ts
…store list, approximate stats - 37 channels was unverifiable: integration.manager.ts registers 36 distinct providers; reference the source file instead - zustand stores are composer + modal manager only; timezone is localStorage + dayjs, not a store - mark upstream commit-per-month counts as approximate in minimal-batch.md
…ools The alias map in load.tools.service.ts does not register crove_post_ aliases for the clipping tools, so those claudeHiddenTools entries could never match. The real tool names stay hidden; re-add branded aliases only if the alias map gains clipping entries.
Upstream's clipping workflow added OpenaiService.pickClips referencing a module-level openai client that the fork replaced with getOpenAIClient() / getModel() for OPENAI_BASE_URL and OPENAI_MODEL_NAME support, so the auto-merge produced TS2552 (Cannot find name 'openai'). Align pickClips with the fork pattern used by every other method in this service.
eslint 8.57 cannot parse the flat eslint.config.mjs (next 15+ flat export style), so the ESLint workflow has been red since the flat-config migration: eslint crashed before producing the SARIF file and the upload-sarif step failed. - bump eslint 8.57.0 -> ^9 (resolves 9.39.5) and @typescript-eslint/* 7.18 -> ^8 - bump CI SARIF formatter to @microsoft/eslint-formatter-sarif@3.1.0 (eslint 9 line) - drop the dead .eslintignore (eslint 9 ignores node_modules by default and warns on the legacy file) - flat config unchanged: verified eslint 9.39.5 loads it, backend lints clean (0 errors / 9 warnings), frontend reports 499 errors / 1014 warnings which stay visible through SARIF under the existing continue-on-error transitional gate (audit C9)
…age code - clipping.service.ts: log clipping failures with fixed format strings and data as arguments instead of interpolating tainted values into the template - local.storage.ts: contain removeFile to the upload directory - resolve the requested path and refuse to unlink anything outside the upload root, so a traversal-shaped key can never delete arbitrary files Both fixes touch files introduced/changed by this upstream sync; they are deliberate fork deltas to be recorded in docs/fork-delta.md.
CodeQL js/path-injection does not model the startsWith(resolvedRoot + sep) prefix check as a validated boundary. path.relative + isAbsolute + '..' rejection is the canonical containment form: any path resolving outside the upload root yields a relative path starting with '..' or an absolute one.
docs: minimal batch groundwork - ADR-0001, fork-delta inventory, CLAUDE.md corrections
chore(lint): migrate to eslint 9 and typescript-eslint 8 - ESLint workflow green
chore: sync with upstream Postiz (2026-09-21)
Minimal batch item: frontend had zero tests, so every UI change was carried by manual QA alone. Establishes the frontend unit test track alongside the backend jest suites: - vitest.frontend.config.ts: jsdom environment, @gitroom/* path aliases mirroring tsconfig.base.json, tests under tests/frontend/ - 14 tests across three primitives in libraries/react-shared-libraries/src/form: Button (render, type default/override, click, loading treatment, secondary), Textarea (label render, react-hook-form registration + submit value, explicit error display, disableForm), Checkbox (label render, onChange toggle in disableForm mode, checked state) - pnpm run test:frontend + a CI step in build.yml so the suite gates PRs Translation is mocked at the TranslatedLabel module boundary - the baseline covers component behavior, not i18n.
…t disableForm test name
test(frontend): vitest + testing-library baseline for form primitives
Minimal batch items: - Playwright smoke suite (playwright.config.ts, tests/e2e/): public checks (auth redirect, Crove branding, DOS ID button, login page) verified against beta; authenticated compose -> schedule -> calendar flow scaffolded and gated on E2E_DOS_EMAIL / E2E_DOS_PASSWORD (needs a dedicated beta test account; selectors to be finalized on the first credential-backed run). E2E_BROWSER_CHANNEL lets a machine run on a system browser before the managed chromium build is downloaded. Not wired into CI yet. - docs/ops/prod-compose-reconciliation.md: verified drift catalog between scripts/docker-compose.prod.yaml and /opt/crove on crove-server, Phase A completion note (CROVE_TEMPORAL_POSTGRES_PASSWORD added to the VM env file matching the running password), and the ordered Phase B maintenance-window plan (file sync, healthchecks, dynamicconfig flip, password rotation, legacy crove-postgres / postiz-redis retirement). - CHANGELOG entries for the minimal batch (eslint 9 change, test track).
test(e2e): playwright smoke harness + prod compose reconciliation doc
Phase B was executed on crove-server 2026-09-21 (JOY approved). Two assumptions in the repo file were wrong when deployed and are corrected with live evidence: - dynamicconfig: temporalio/auto-setup:1.28.1 ships only development-sql.yaml and development-cass.yaml; there is no production-sql template, and the ./dynamicconfig mount shadows the image dir. Keep development-sql. - temporal healthcheck: the server binds services to the container IP, not localhost, so the localhost probe reported unhealthy on a fully functional server. Probe now targets $(hostname -i) with $$ compose escaping. Plus deployment notes in the header (compose interpolation reads /opt/crove/.env, not the service-level env_file; crove-web confirmed absent from ghcr and excluded from up). docs/ops updated from plan to executed with residual items.
chore(ops): Phase B corrections to prod compose after live execution
…OKEN The operator gh token lacks the security_events scope required by the code scanning alert dismissal endpoint, and adding a scope is a credential change. A workflow_dispatch job with security-events: write performs the dismissal with GITHUB_TOKEN instead. Remove this file after the alert is dismissed.
chore(ci): one-off workflow to dismiss CodeQL alert 1567
chore(ci): debug GET before dismissal PUT
GITHUB_TOKEN can read code scanning alerts but cannot dismiss them: the dismissal endpoint is restricted to user tokens for the dismissed_by audit trail and returns 404 even with security-events: write. The dismissal needs the operator user token with security_events scope instead. Alert 1567 stays open until then; the containment guard from PR #43 remains in place.
chore(ci): remove one-off CodeQL dismissal workflow
dos.me internal billing endpoints return { success: true, data: {...} }.
Consumers (checkout url, portal url, entitlement fields) read the
endpoint shape directly, so the wrapper made every field undefined —
Purchase silently did nothing and Portal resolved to /undefined.
There was a problem hiding this comment.
Code Review
This pull request introduces a video clipping feature to Crove Post, leveraging Temporal workflows, Deepgram transcription, OpenAI analysis, and RunPod rendering to turn YouTube videos into short vertical clips. It also adds an MCP clipping widget, integrates clipping limits into the billing system, migrates ESLint to version 9, and establishes a frontend testing baseline with Vitest and Playwright. Feedback on the changes highlights a potential runtime TypeError in clipping.service.ts due to an unsafe non-null assertion on the organization object, and a limitation in the dos-me-billing.client.ts unwrapping logic that fails to handle primitive payload types.
| const org = await this._organizationService.getOrgByIdWithSubscription( | ||
| organizationId | ||
| ); | ||
| return (await this._subscriptionService.checkCredits(org!, CREDITS_TYPE)) |
There was a problem hiding this comment.
The org object returned by getOrgByIdWithSubscription can be null if the organization is not found or has been deleted. Using the non-null assertion operator (org!) on line 168 will cause a runtime TypeError when trying to access properties of null inside checkCredits. Adding a defensive null check prevents potential application crashes.
| const org = await this._organizationService.getOrgByIdWithSubscription( | |
| organizationId | |
| ); | |
| return (await this._subscriptionService.checkCredits(org!, CREDITS_TYPE)) | |
| const org = await this._organizationService.getOrgByIdWithSubscription( | |
| organizationId | |
| ); | |
| if (!org) { | |
| return 0; | |
| } | |
| return (await this._subscriptionService.checkCredits(org, CREDITS_TYPE)) | |
| .credits; |
| if ( | ||
| body && | ||
| typeof body === 'object' && | ||
| 'success' in body && | ||
| 'data' in body && | ||
| body.data !== null && | ||
| typeof body.data === 'object' | ||
| ) { | ||
| return body.data as T; | ||
| } |
There was a problem hiding this comment.
The unwrapping logic currently restricts body.data to be of type 'object'. If any dos.me API endpoint returns a primitive value (such as a string, number, or boolean) inside the data envelope, this check will fail and return the wrapped body instead of the unwrapped value. Removing the typeof body.data === 'object' constraint allows the client to robustly unwrap all successful responses regardless of the payload type.
if (
body &&
typeof body === 'object' &&
'success' in body &&
'data' in body
) {
return body.data as T;
}
Promote dev to main
{ success, data }response envelope — Purchase/Portal/Cancel readurlat the endpoint shape; the wrapper made every field undefined (Purchase did nothing, Portal redirected to /undefined). Verified against live dos.me: checkout now returns a Stripe URL.Deploy notes