feat(product): back-merge upstream jobs, merge, import, tag visibility and email branding (#506, #507, #518, #520, #445) - #29
Merged
Conversation
…le bound (QuackbackIO#506) Pruning ran on every lease-reap tick even though retention is measured in days, and its per-row retention predicate could never use job_queue_terminal_idx, so every tick was a full scan of each workspace's terminal rows. Split the prune onto JOB_PRUNE_INTERVAL_MS (default 1h) and lead the predicate with the shortest retention as a constant bound so the partial index applies; lease reaping keeps its fast cadence. Co-authored-by: Cursor <cursoragent@cursor.com> (cherry picked from commit f57343d) Fork note: the hunk that adds the prune-clock test to worker.test.ts also carried upstream's `describe('dormancy')` block from QuackbackIO#500, which this fork does not take (single tenancy). Only the prune-clock test is kept. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e merge assessment request (QuackbackIO#507) * fix(merge): strict item schema so OpenAI Structured Outputs accept the request `assessMergeCandidates` described its response items with `z.record(z.string(), z.unknown())`. zod renders that with a `propertyNames` keyword, which OpenAI's Structured Outputs reject: 400 Invalid schema for response_format 'structured_output': In context=('properties', 'results', 'type', '0', 'items'), 'propertyNames' is not permitted. The request is refused before the model runs, so every merge check fails and no merge suggestion is ever created. `checkPostForMergeCandidates` then never stamps `mergeCheckedAt`, so the sweep retries the same posts on every pass. The loose item shape was deliberate — it let a single malformed item be skipped instead of failing the whole batch. Under structured outputs that tolerance is moot: the provider guarantees the shape. `results` keeps `.catch([])`, so a present-but-wrong-shaped top level still degrades to "no assessments" rather than failing the request, and the `typeof` guards in the filter loop stay as they are. Adds a regression test that asserts the schema handed to `chat()` emits no `propertyNames`. It fails on the previous schema and passes on this one. Fixes QuackbackIO#505 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style(merge): shorten schema comment and restore JSDoc spacing --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: James Morton <james.morton@quackback.io> (cherry picked from commit de5af85)
…O#518) In-app CSV import advertised author_name but attributed posts to the uploader whenever author_email was empty. Name-only rows now create (or reuse) a portal contact, matching AuthorSelector. Rows with neither author_name nor author_email are skipped instead of falling back to the importer. Preview, template, and helper copy follow the same contract. (cherry picked from commit d116df3)
…#520) * feat(tags): per-tag portal visibility ("Show on portal") Tags were always shown on the public portal, which surprised customers who used them for internal triage. Adds post_tags.is_public (default true, so nothing changes on upgrade) with a "Show on portal" switch in tag settings. Non-team portal viewers only see public tags — in filter lists, on post cards/detail, link embeds, widget search and the apps search/suggest endpoints — and a tagIds URL filter naming an internal tag is inert for them so post membership can't be inferred. Team actors keep the full catalog so they can still assign internal tags from the portal. The flag is exposed on /api/v1/tags, the OpenAPI schema and the tags CSV export. Co-authored-by: Cursor <cursoragent@cursor.com> * test(fleet): acknowledge 0276_post_tags_is_public in the post-0248 replay span pin Co-authored-by: Cursor <cursoragent@cursor.com> * fix(tags): close two internal-tag leaks flagged in review - Public roadmap: caller-supplied tagIds now go through the same actor-aware public-tag guard as the feed, so an internal tag id in a /roadmap URL is inert for non-team viewers. Admin-configured base filters are unchanged (not caller-controlled). Real-DB test added. - Client cache: ['portal','tags'] and ['portal','data'] carry a role-dependent tag catalog under a fixed key; invalidate both on every portal auth transition (sign-in success, header sign-out, access-gate sign-out) so a team session's internal tags never leak into the following signed-out session, and vice versa. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(dep-graph): record roadmaps -> posts edge (roadmap runtime tag filter reuses publicTagCondition); acyclic Co-authored-by: Cursor <cursoragent@cursor.com> * fix(portal): reset (not invalidate) viewer-scoped caches on every auth transition Review follow-ups on the client cache fix: - invalidateQueries keeps data that route loaders read back through ensureQueryData, so a stale team-scoped tag catalog still rendered for the next anonymous view. Switch to resetQueries. - Public roadmap results are now actor-dependent for runtime tag filters; include ['portal','roadmapPosts'] (and 'posts'/'post', which embed the filtered catalog) in the viewer-scoped set. - Route the inline "sign out" links in the feedback header and comment form, plus the access-gate sign-in success, through the same reset. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(roadmaps): redact internal tag ids from public roadmap base filters listPublicRoadmaps returned each roadmap's admin-configured baseFilter verbatim, so a public roadmap curated by an internal tag handed that tag id to anonymous callers, who could correlate it with the returned posts. Non-team viewers now get baseFilter.tagIds filtered to public tags (the key is dropped when nothing remains); membership is unchanged since the curation itself is admin-defined. Team actors are unredacted. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(portal): include ['portal','roadmaps'] in the viewer-scoped reset (baseFilter is now redacted per viewer) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(portal): include the ['publicPosts'] infinite feed in the viewer-scoped reset Co-authored-by: Cursor <cursoragent@cursor.com> * fix(portal): remove (not reset) viewer-scoped caches on auth transitions resetQueries restores a query's initialData, and usePublicPosts seeds the feed from the SSR payload that way, so a team member's feed page (with internal tags) came straight back after a reset. removeQueries drops the entries outright; every call site follows with router.invalidate(), which re-runs loaders and rebinds observers to fresh queries as the new viewer. Unit test now pins all three failure modes (invalidate retains, reset restores initialData, remove clears). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> (cherry picked from commit c34aef7) Fork note: upstream numbers this migration 0276, which this fork already uses for 0276_identity_provider_logo_key (upstream's 0273, renumbered on the first back-merge). Renumbered to 0277_post_tags_is_public (idx 254, when 1785700000035), SQL unchanged. Migrator-gate span, CONTRACT.md (255 scanned) and GRAPH.md (116 edges) regenerated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The sign-in, invite, welcome, and password-reset emails hardcoded "Quackback" in their subjects and headings, so an install running under its own brand still sent mail naming the platform in the one place in-app theming, a custom logo, and a custom domain never reach — the recipient's inbox. Add an EMAIL_BRAND_NAME env var (default "Quackback") and thread it through those subjects and templates as a product name that stays separate from each workspace's own name, so "join Acme on Quackback" keeps both halves distinct. Existing installs are unchanged. (cherry picked from commit 0aa54a8)
…or rule Upstream QuackbackIO#520 drops the viewer-scoped react-query caches on every portal auth transition so a team member's internal tags never leak into the next anonymous view, and QuackbackIO#518 makes the CSV importer require an author. Upstream's tests leave the reset paths in the comment form, the feedback header, the portal header's cross-tab sign-in and the access gate's sign-out unexecuted, along with the tag export's new is_public column and the created_at fallback in the row schema. These suites close that: each sign-out test seeds a real QueryClient and asserts the scoped key is gone while an unrelated one survives, and the translated components are asserted against the German catalogue. The sample-CSV generator (`bun run db:generate-csv`) lost its anonymous-author branch in QuackbackIO#518, and the importer would now skip such rows. It runs as a process — argv and a file write at module scope — so it joins the CLI entry points in the coverage exclusion, in the same shape as `scripts/*-check.ts`, and a new suite spawns it and feeds every generated row through the importer's own parser and schema. `coverage-scope.test.ts` pins the widened exclusion. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…back-merge The scanner suite `module-state.test.ts` timed out under the full parallel run and passed alone; the re-run at six workers lost `runner.test.ts` instead, whose prune count is a database-wide number that a neighbouring suite's prune can take first. Both are the 8x entry's shape, recorded there. Nothing in the running service changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Third and last of the selective back-merges from upstream (
quackbackio/quackback): product fixes and the job queue. Four merged upstream commits plus one open upstream pull request, cherry-picked with-xso each keeps its upstream author:perf(jobs): pruning terminal job rows moves onto its own hourly clock (JOB_PRUNE_INTERVAL_MS, default 1h) and leads with a constant retention bound so the partial index applies. Lease reaping keeps its cadence.fix(merge): the merge-candidate assessment schema is strict, so OpenAI Structured Outputs accept the request again. Before this, every merge check failed and the sweep retried the same posts on every pass.fix(import): CSV import honoursauthor_name; a row with neither name nor email is skipped instead of being attributed to the importer.feat(tags): per-tag portal visibility ("Show on portal"), with the review follow-ups that close the internal-tag leaks on the public roadmap and in the client cache.feat(email)by @faizan47, still open upstream:EMAIL_BRAND_NAME(defaultQuackback) replaces the hard-coded product name in the sign-in, invite, welcome and password-reset emails.Billing, workspace dormancy and the Slack assistant stay out, as in #27 and #28.
What changes for an operator
0277_post_tags_is_public: addspost_tags.is_publicwith defaulttrue, so every existing tag stays visible and nothing changes on upgrade until someone flips a tag. Upstream numbers this migration 0276; this fork already uses 0276 for the identity-provider logo key (feat(auth): back-merge upstream auth basics (#485, #482, #486, #524) #27), so the file and journal entry are renumbered (idx254,when1785700000035). The SQL is unchanged. Without the renumbering the migrator would have skipped it silently, because it only applies entries with awhenabove the last applied one.JOB_PRUNE_INTERVAL_MSandEMAIL_BRAND_NAME. Unset, the service behaves as before except for the prune cadence, which drops from every reap tick to hourly.worker.test.tsalso carried thedescribe('dormancy')block from feat(fleet): park job loops and fleet sweeps for workspaces nobody visits QuackbackIO/quackback#500, which this fork does not take. Only the prune-clock test is kept.Verification
Two fork commits sit on top of the picks: the tests (
test(portal)) and a SELF-IMPROVE note.bun run typecheck: 0 errors.bun run lint: clean.bun run db:check-driftagainst a database migrated through 0277: no drift.bun scripts/i18n-check.ts: PASS — the translated portal components feat(tags): per-tag portal visibility ("Show on portal") QuackbackIO/quackback#520 touches receive no new English text; the tag settings and CSV import pages were English-only before and stay so.DIFF_BASE=main bun scripts/mutation-check.ts—PASS: this change touched no file the manifest declares.The touched files are printed as not mutation-graded by name, as before this change.Tests
Upstream's own tests leave the QuackbackIO#520 cache-reset paths and two QuackbackIO#518 details unexecuted, so the branch adds four suites and extends three: the comment form, the feedback header, the portal header (cross-tab sign-in) and the access gate (sign-out) each seed a real QueryClient and prove the viewer-scoped keys are gone while an unrelated key survives; the tag export writes
is_public; the row schema keeps a parseablecreated_atand falls back for a blank or unparseable one (with afast-checkround-trip over ISO timestamps).The sample-CSV generator lost its anonymous-author branch in QuackbackIO#518. It is a process (argv and a file write at module scope), so it joins the CLI entry points in the coverage exclusion — same shape as
scripts/*-check.ts, pinned bycoverage-scope.test.ts— and a new suite spawns it and feeds every generated row through the importer's own parser andcsvRowSchema. That is the contract the generator has to keep: the sample file must import without silently losing posts.Local run over every affected directory with
REQUIRE_TEST_DB=1:The one failure is
jobs/__tests__/runner.test.tsseeing a prune count of 0 for a row it had just aged, which passes three times in a row alone: neighbouring suites prune the same sharedjob_queuetable, so under parallel load whichever process prunes first takes the other's row. The skipped file isapi-integration-advanced.test.ts, which needs a running server. Both are recorded in SELF-IMPROVE.The e2e spec
apps/web/e2e/tests/admin/settings-tags.spec.tsfrom QuackbackIO#520 is out of the unit gate's scope by design and runs in thee2e-smokejob.Security note
EMAIL_BRAND_NAME(QuackbackIO#445) is operator-controlled configuration read throughgetEnv, rendered by React inside the email templates (escaped) and interpolated into plain-text subjects; it never reaches a query or a shell. OWASP A05 (security misconfiguration) is the relevant category and the default keeps the previous behaviour.post_tags.is_public(QuackbackIO#520) is enforced server-side in the public post, roadmap and API paths for non-team actors, so an internal tag id supplied in a URL is inert for them — OWASP A01 (broken access control), and the reason QuackbackIO#520's review follow-ups are included in full.Not in this pull request
🤖 Generated with Claude Code