Logs
Bug reports
Known defects, with a repro and a status.
docs/BUGS.md
Known defects. A bug without a repro is a rumour, so every row has one.
Security issues do not live here β they go in the findings register, which keeps its rows after closure.
Status: π΄ open Β· π‘ accepted (known, not worth fixing yet) Β· β fixed.
Open
| # | Status | Symptom | Repro | Notes |
|---|---|---|---|---|
| B-023 | The prod deploy failed on quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z ... 401 Unauthorized. Repro: merge anything to main. | The same outage twice. Docker Hub stopped serving minio/minio in Sept 2026, so the image moved to quay.io and was pinned (#260); quay.io has now stopped serving that repository anonymously β checked with a quay-issued pull token, latest and even the tags list answer 401, so it is the whole repository and not one retired tag. The deploy broke not because MinIO is needed but because docker compose pull pulls every image in the file, so a third party refusing to re-serve an image the droplet already had stopped a release that had nothing to do with it. | roll-vm.sh now tells the two cases apart: api and web ARE the release, so they must pull and a failure aborts; everything else is infrastructure already on the droplet, refreshed best-effort with --ignore-pull-failures. Then, before anything is recreated, every image docker compose config --images names must resolve locally β a genuinely absent image fails there, with the running stack untouched, rather than half-way through up -d. Prod was never down: the pull step aborts before anything is recreated, which is what that ordering exists for. Still open: MinIO is served from a registry we do not control, and this is the second registry to drop it. Mirroring it to ghcr.io is the actual fix. | |
| B-002 | π‘ | A freshly created Postgres volume has no schema, so every authenticated call 500s with The table public.users does not exist. | Rename a compose volume, docker compose up -d postgres, call any endpoint | The 500 is correct β a missing table is internal. The fix is npm run db:push, which is in Getting started. A friendlier startup check is possible but would run on every boot to catch a first-run mistake. |
Fixed
| # | Symptom | Cause | Fixed by |
|---|---|---|---|
| B-025 | Connecting Instagram left the user on a raw JSON page at studio.lightningleap.store/api/v1/oauth/instagram/callback showing {"detail":"platform error (invalid_request)"}. Repro: connect Instagram with a Facebook login that has no Page linked to an Instagram Business account. | The browser lands on the OAuth callback straight from the platform's consent screen. When the adapter refused the connect, the callback threw a 424 ApiError, so the browser rendered that error body and had no link back to the app. | The callback redirects to the redirect_after path with oauth_error=<code> and oauth_platform=<platform>. Only the stable code goes into the URL because the message can contain the provider's raw response. The message is still logged exactly as before. apps/web renders the two parameters in a separate PR. |
| B-024 | Every LinkedIn post carrying a PDF failed with Publish failed: invalid_request: linkedin ugcPosts 422: ERROR :: /specificContent/com.linkedin.ugc.ShareContent/shareMediaCategory :: "DOCUMENT" is not an enum symbol. Repro: compose a LinkedIn post, attach a PDF, publish. Every time, since the feature shipped. | ugcPosts.shareMediaCategory accepts NONE / ARTICLE / IMAGE / VIDEO. "DOCUMENT" was assumed rather than read off LinkedIn's enum β documents do not exist in the legacy share API at all, only in the versioned Posts API. The upload half was already correct (/rest/documents), so the post produced a valid urn:li:document: URN and then handed it to an endpoint with no word for it. The suite covering the feature passed throughout, because its harness stubbed ugcPosts to succeed and the assertion read back the category we had just written β it asserted our belief about LinkedIn rather than LinkedIn's behaviour. | Documents publish through POST /rest/posts, the only endpoint where a document URN means anything: singular content.media.id, commentary in place of shareCommentary.text, the mandatory distribution block, a LinkedIn-Version header, and visibility as a plain string β the nested MemberNetworkVisibility map is accepted there and silently ignored, which would leak a CONNECTIONS-only post to the whole feed while looking like success. Images, video, articles and text stay on ugcPosts. The harness now scripts both endpoints and the regression asserts ugcPosts is not called for a document β a negative assertion, because that is the only form that could have failed. |
| B-020 | Scheduling a post that was still under review reported βPost scheduled.β and then the post never appeared under Scheduled β it stayed in Pending with a date on it, and its hour passed in silence. Repro: turn on approvals for a project, send a post for review, open it from Content and press Schedule. | Two faults in one press. (1) The machine has no pending_review β scheduled edge, so needsScheduleTransition correctly sent no status and only a time β but the form reported success regardless. The publisher claims on status === "scheduled" and reads nothing else, so the variant was invisible to it. (2) The form wrote the time onto the VARIANTS and never onto the post. scheduleApprovedPost reads post.scheduledAt the moment an approval lands β that is how an approved post reaches the queue without anybody pressing Schedule again β so the date was discarded at exactly the moment it was needed, and sign-off answered βApproved, but the post has no publish dateβ about a date somebody had already chosen. | planSchedule (lib/post-scheduling.ts) sorts every variant into queue / re-time / blocked / settled, and describeSchedule turns that into what the toast says β a post nothing could queue now reads βDate saved, but nothing is queued β this post is waiting for approval.β The modal also says it before the button, not only after. The time is written to the post first and unconditionally; safe against a live approval because scheduled_at is not one of the API's CONTENT_FIELDS, so a scheduling-only edit cannot invalidate a sign-off. 11 new cases in post-scheduling.test.ts, and the round trip is pinned at the API in approval-before-schedule.test.ts. |
| B-021 | After an approver requested changes there was no way to get the post approved. Approve did nothing, and no other action appeared anywhere. Repro: request changes on a post, edit it, try to get it signed off. | changes_requested is terminal (approvals/state.ts), so decide answers 409 and the aggregate never moves β correct, because the approver has already voted at that stage and the revised post deserves a fresh read. The exit is a NEW approval: submitForReview drops the settled row and re-opens the cycle at stage 0, and the variant machine has the changes_requested β pending_review edge it needs. Both halves worked. The button was never drawn. ReviewDecision did if (isTerminal(status)) return null, and canResubmit β written, documented and unit-tested β had no call site anywhere in the app. | ReviewDecision now renders a Send for review again panel on a settled approval the viewer may resubmit, through the useStartApproval hook that already existed, on both the approval page and the review dialog. Content and the Instagram hub gained an Open review action for changes_requested / rejected posts β the list response carries the status but not the approval, so "Submit for review" (gated on canSubmitForReview, always false on a list) could never have been the way back. Publish is deliberately dropped from those two states: pushing out something an approver refused, from a card, in one click. Re-deciding the settled row stays refused. Three API tests pin the sequence end to end: the 409 on a second verdict, the fresh approval at stage 0 with variants back in pending_review, and the post's date surviving the round trip so sign-off queues it. |
| B-019 | Menus and floating panels were see-through: the project switcher's dropdown showed "Overview", "Content" and "Calendar" from the nav behind its own project names, so two sets of text overlapped and neither was readable. Repro: open the project switcher in the project sidebar. | The Clearhouse port made --card a frosted 82% veil β right for a card resting ON the page, wrong for anything floating OVER it. --popover stayed opaque for exactly that reason, but ~20 ad-hoc dropdowns, drawers and toolbars had been written with bg-card before the veil existed, and turned translucent the moment it arrived. The UI primitives (modal, actions-menu, date-range-picker, chart tooltip, inputs) were already on bg-popover and were unaffected, which is why this looked like a one-off rather than a class. | Every overlay surface moved to bg-popover: the project, currency and team dropdowns, the notification tray, the assistant and Style Lab panels, the email composer's menus and its window, the inbox and rich-text toolbars, the audit and campaign drawers, the docs drawer and its sticky mobile bar, the calendar's sticky week header, and the landing-page block toolbar. In-flow bg-card/50 placeholders were left alone β they are meant to sit on the page. |
| B-018 | Every LinkedIn document post failed, and kept failing: platform_error: linkedin documents initializeUpload 426: {"status":426,"code":"NONEXISTENT_VERSION","message":"Requested version 20240101 is not active"} β retrying automatically. Repro: publish a PDF post to LinkedIn. | Two faults. (1) The LinkedIn-Version header was the hard-coded literal "202401". LinkedIn retires versions on a rolling window of about a year, so the value went dead on LinkedIn's schedule and no LinkedIn document post could ever succeed again β the fix was a code change and a deploy, in a string nobody owns. (2) The 426 was classified platform_error, which is in the publisher's RETRYABLE_CODES, so a permanent configuration failure was retried five times across the backoff window to reach the same refusal, and the post's failure reason said "platform error" about a setting. | The version is a property of the registered app, set per project in Integrations β Platform apps (stored non-secret in the credential's extra, read back into the form, and carried across a secret rotation), falling back to LINKEDIN_API_VERSION and then to the adapter's own default. The 426 now throws invalid_request β not retryable β with a message naming the screen and a current version to type. Covered by adapter-linkedin-document.test.ts (version header; 426 fails once) and channel-credentials.test.ts (round-trip, rotation, clearing). |
| B-017 | The Inbox stayed permanently empty for Instagram and Facebook. Comments and DMs arrived on the platform; nothing ever reached the app. No error appeared anywhere β the webhook URL verified, the Meta dashboard reported the fields Subscribed, and every delivery endpoint answered 200. | Two independent faults, either of which alone was fatal. (1) Nothing was ever delivered. Meta needs a per-account subscription (POST /{page-id}/subscribed_apps) on top of the app-level field subscription ticked in the dashboard. No such call existed anywhere in the repository, and neither did the permission it requires (pages_manage_metadata), nor the one that lets messages be delivered at all (instagram_manage_messages; Facebook Page DMs need pages_messaging, a Messenger capability this app does not have, so they stay off β see Integrations). Step 1 of a two-step handshake looks identical to a finished one from the dashboard. (2) Deliveries would have been dropped anyway. meta-webhooks.ts handled field: "comments" with the Facebook feed payload shape β requiring verb === "add" and item === "comment", reading value.comment_id and value.message. A real Instagram comment carries no verb and no item, puts its id in value.id and its body in value.text, so the first guard returned null on every one of them and logged nothing, because "not an event we care about" is the normal path. The suite was green because its fixture sent the Facebook shape under an object: "instagram" envelope β it proved the mapper matched the fixture, not that the fixture matched Meta. | ChannelAdapter.subscribeWebhooks, implemented for both Meta adapters and called at the end of every OAuth connect (non-fatal: a channel that publishes but has no inbox is degraded, not broken); the outcome is recorded on provider_meta.webhookSubscription so an empty inbox has a stated reason. POST /channels/:id/resubscribe re-runs it for accounts connected earlier. The comments mapper now normalises the two genuinely different payloads. New inboxScopes bucket on AdapterCapabilities carries the three added permissions. Regression tests use the payload Meta actually sends: webhooks.test.ts (4 cases, processed was 0 before) and channels-adapters.test.ts (5 cases pinning host, id and token on the subscribe call). See Integrations, "Webhooks are a TWO-step subscription". |
| B-011 | A user who signed up through the app became an owner who could do nothing β POST /projects returned Missing required permission: projects:write. Unrecoverable from inside the app: granting yourself the role needs roles:manage, which you also did not have. | The Better-Auth cutover (2026-08-01) added POST /auth/signup-with-org as a second signup path. The E12 RBAC slice (2026-08-04) back-filled the UserRoleAssignment write into the legacy POST /auth/signup only. getUserPermissions() reads the assignment and never falls back to user.role, so the new path produced a user labelled "owner" with an empty permission set. Every test passed because src/test/helpers.ts signs up through the legacy route, and the new endpoint had no test at all. | ensureBuiltInRoles + ensureAssignment added to auth-v2/routes.ts, inside a transaction with the org-link update and with compensating deletes on failure. New src/test/signup-with-org.test.ts asserts the resulting permission set, not just the row β 3 of its 4 cases fail against the old code. Existing accounts: npm run rbac:backfill --workspace @verjson/api. |
| B-012 | /login threw a hydration error on every visit; React discarded the form subtree and rebuilt it client-side. | isPasskeySupported() reads window.PublicKeyCredential and was called during render, so the server rendered no passkey block and the client's first render wanted one. The helper's own doc comment described it as a render-time feature-detect, which is what invited the call. | Moved behind useState(false) + a mount effect so the first client render matches the SSR output; the doc comment now says browser-only and explains the failure mode. |
| B-013 | npm run db:push and npm run seed failed from a clean clone with Environment variable not found: DATABASE_URL, despite Getting started saying cp .env.example .env is enough. | The single .env lives at the repo root, but both commands run with their cwd at apps/api. Prisma only looks in its own working directory, Node does not auto-load .env, and the root dotenv dependency was declared but imported nowhere except vitest.config.ts. seed.ts did import "dotenv/config", which resolves to the non-existent apps/api/.env. | New apps/api/prisma.config.ts loads the root .env for every Prisma CLI command (and replaces the deprecated package.json#prisma block); dev, seed and rbac:backfill pass --env-file-if-exists=../../.env. dotenv never overwrites an already-set variable, so deployments that inject env are unaffected. |
| B-014 | npm run lint failed on dev with 14 errors, while STATUS claimed "β
0 errors". Seven were the core/-must-not-import-Hono boundary rule firing on legitimate route modules. | The rule's allowlist matched src/modules/**/routes.ts β the bare filename only. Modules that outgrew one router had split it into siblings (phone-routes.ts, push-routes.ts, β¦), each still building a Hono<Env> that app.ts mounts. The code was correct; the allowlist was stale. One file had already been papered over with an inline eslint-disable. | Allowlist widened to src/modules/**/*routes.ts, the now-redundant eslint-disable removed, and the seven genuine dead imports/expressions deleted β including a stray no-op range; statement in run-report.ts. |
| B-001 | The E2E suite would not start while the dev server was running. | Playwright's port was pinned to 3100 to avoid reusing an unrelated app, but Next refuses a second dev server for one directory at any port β so the suite could not run exactly when you most want it. | Playwright now uses 3000 and reuseExistingServer attaches to it. The original worry is covered by the first spec asserting this app's own heading; a stranger on :3000 fails immediately rather than producing a false green. CI still starts its own. |
| B-004 | /api/v1/health reported queue: down on every fresh boot. | The probe reads cached broker state and never dials (S-010), and nothing had connected yet β so a healthy stack reported degraded until the first publish. | The API connects to the broker at startup, like the database pool. Failure is logged, not fatal. |
| B-005 | Agent-run listing 500'd for an external role. | The scope filter used { id: "__never__" } to mean "match nothing"; Postgres rejects that against a @db.Uuid column, so the fail-closed branch errored instead of returning nothing. | Branches composed rather than negated. |
| B-006 | A missing Stripe API key was reported as Invalid signature. | stripe() throwing "not configured" sat inside the constructEvent try/catch. | Webhook verification uses a client built for that one job, constructed outside the try. |
| B-007 | next build failed at prerender on /login. | It reads ?error= via useSearchParams, which opts a route into dynamic rendering unless wrapped. | Split into a Suspense boundary plus the form. |
| B-008 | The API's dist was unrunnable. | tsc does not rewrite @/* path aliases on emit, so the output carried specifiers Node cannot resolve. | Bundle with tsup. |
| B-009 | Most of the landing page was invisible. Everything below the first heading stayed blank; the hero's build-status tiles never appeared at all. | Reveal interpolated its delay as `${delay}s` while every caller passed milliseconds, so delay={320} meant a 320-second transition delay. The elements were in the DOM at opacity: 0, and Playwright's toBeVisible() ignores opacity β so the smoke test passed while a human saw an empty page. | One character (ms). The regression guard asserts the computed opacity of every ancestor rather than mere presence β landing.spec.ts βΊ "delayed reveals actually become visible". |
| B-010 | The landing page scrolled sideways at 1280px, 1024px and 390px. | The sticky header's seven nowrap section links could not shrink, so they pushed the action buttons past the viewport edge and the whole document overflowed. | The rail scrolls inside itself (min-w-0 + overflow-x-auto), the actions are shrink-0, and the wordmark drops below sm. Guarded at five widths by landing.spec.ts βΊ "the page never scrolls sideways". |
| B-015 | Clicking a published post in Content opened "Schedule this post" β a form that would only accept a future date and then failed against the API, because published is terminal in the platform-post state machine and publishing/published are its PROTECTED_STATUSES. Repro: publish a post, open Content β Published, click the row. | The Content row links every post to /calendar/content?post=<id>, and the calendar answered that param with the schedule form regardless of the post's state. The submit also fanned the new time across every variant, so on a partly published post the first rejection aborted the loop and left the still-queued variants unscheduled. | isPostSchedulable (apps/web/src/lib/post-scheduling.ts) decides which modal the ?post= target opens: a post with nothing left to queue gets the read-only preview β caption, status, permalink β instead of a date picker. The schedule loop skips protected variants, so a partly published post still schedules its remaining channels. |
| B-016 | A video post in the Content list showed a grey square with a film glyph β every video looked like every other video, and like a post whose upload had failed. The same list offered no way to move a date: the only route to the schedule form was to notice the calendar existed and find the post again there. Repro: Content β Scheduled on a project with a video post. | The row drew <Video /> for kind === "video" rather than a frame, because feeding an .mp4 to an <img> renders the broken-image icon β the glyph was the workaround. (A PDF post hit the <img> branch and got that broken icon.) Rescheduling was never on this screen at all; the row link goes to the post record, and the schedule form lived inline in the calendar page where no other screen could reach it. | PostThumbnail (apps/web/src/components/organic/post-thumbnail.tsx) paints a real <video> seeked to 0.1s via the existing videoPosterSrc, muted and non-autoplaying, with a play glyph over it; documents keep a glyph, having no frame to show. The calendar's schedule form moved out to SchedulePostModal and both screens now mount the same one, so the role split (commit vs propose), the fan-out across variants and the protected-variant skip stay in one place. It seeds from the post's own time (defaultScheduleAt) rather than "an hour from now", which for a reschedule is the difference between a nudge and a silent move. |
| B-022 | The command centre read βlast updated 9 Septemberβ for fifteen days while the repo shipped daily, and three merged features β the Clearhouse brand, team chat, the walkthrough β had no row on the board at all. Repro: open /dashboard on any day after 9 Sep. | Not a broken script. dashboard-sync.yml ran on every push to main and succeeded every time: it computed the log, committed it, and force-pushed chore/dashboard-sync-auto. Only its last step failed, identically on every run β GitHub Actions is not permitted to create or approve pull requests β because the org disables that. The workflow treated this as acceptable and printed ::notice:: on the reasoning that βa human opens it in a secondβ. Nobody did, and nobody could: a notice on a green run lives in the summary of a job no one has a reason to open, precisely because it is green. The missing feature rows are the other half β the script deliberately never writes one, since a commit subject is not a status, so that part was always a person's job and nobody had done it. | The fallback now leaves something a person meets: ::warning:: instead of ::notice::, and one issue, reused rather than reopened per push, carrying the one-click compare link and the two ways to stop it recurring. Actions may create issues with the default token, which is what makes the fallback worth anything. The step also prefers a DASHBOARD_SYNC_TOKEN secret over the default token when the repo has one, so the PR opens by itself without granting Actions PR rights org-wide. The stranded branch is #303 (54 entries, meta.updatedAt β 2026-09-24) and the three missing features are F-176 β F-178 (this PR). The run stays green either way: the data IS recorded, and a red mark on main for a job that did its work teaches people to ignore red marks on main. |