Troubleshooting
Every issue below actually happened while building this project, in this order. Reading through these is one of the best ways to learn how real debugging works, since each one starts from a confusing symptom and ends with understanding the actual cause.
1. Ranking silently let through articles it should have excluded
- Symptom
- An off-topic political article showed up in the final ranked list, even though the configured topics only included AI, Technology, Startups, and Space.
- Cause
- rank.ts had logic to exclude blocked topics, but no logic at all to require a match against the included topics. An article just needed to avoid the exclude list, not actually match anything on the include list.
- How it was found
- By actually reading the real output data in the database after a real run, instead of just trusting that the code compiled and ran without errors. Compiling clean and running without crashing tells you nothing about whether the logic is doing the right thing.
- Fix
- Added a second filter step that requires at least one included topic to match, when any are configured.
- Lesson
- Passing type checks and running without errors is a much lower bar than actually being correct. Always look at real output, not just exit codes.
2. Login silently failed even though the password was right
- Symptom
- Logging in returned no error, but also no valid session.
- Cause
- The bcrypt password hash contains dollar signs, with a shape like $2b$10$abc123. Next.js's .env loader performs shell-style variable expansion on $NAME patterns, so it was trying to substitute $2b, $10, and the rest as if they were separate environment variable references, and replacing each with an empty string since no such variables existed. Only the part of the hash after the last dollar sign survived.
- How it was found
- By adding a temporary console.log inside the login logic to print the actual hash value the running server was using, which revealed it was truncated compared to what was actually written to .env.
- Fix
- Escaped each dollar sign as \$ in the .env file, which tells the loader to treat it as a literal character instead of the start of a variable reference.
- Lesson
- When a value with special characters, like a hash or a password, goes into a config file, always check whether that file's format treats those characters specially. A silent, wrong value is much harder to debug than an error.
3. Vercel deploy failed on Prisma, even though it worked locally
- Symptom
- npm run build worked perfectly on the local machine, but the same project failed to build on Vercel with an error about Prisma's Query Engine not being found.
- Cause
- Vercel caches node_modules between deploys for speed. Prisma normally generates its database client code as a step after npm install, but with a cached node_modules, that generation step was being skipped.
- How it was found
- The deploy logs showed the Prisma Query Engine failure on Vercel even though local builds worked.
- Fix
- Added a postinstall script to package.json, "prisma generate", so it runs automatically every time, cache or no cache.
- Lesson
- Your own machine and the deploy server are not identical environments, even when running the exact same code, because of differences like caching behavior. Read the actual deploy logs rather than assuming a local success guarantees a remote success.
4. Vercel silently refused to deploy at all
- Symptom
- vercel deploy appeared to hang for many minutes with no clear progress, eventually inspectable as status UNKNOWN.
- Cause
- Git had auto-generated a commit author email, name@Machine-name.local, that did not match any verified email on the connected GitHub account, and Vercel's GitHub integration blocks deploys from commits whose author cannot be verified, as a security measure.
- How it was found
- By loading the Vercel deployment's inspect page directly in a browser and reading the actual page text, which stated the real reason plainly, rather than continuing to stare at a CLI that was not printing it.
- Fix
- Set the correct git user.email and user.name for the repository, then rewrote the existing commit history, safe to do since nothing had been pushed yet, so every commit's author matched.
- Lesson
- When a tool's own error output is unhelpful, look for a web interface or logs endpoint that might show more detail than the command line does.
5. A real run failed to save, after ten minutes of real work
- Symptom
- The agent successfully fetched, classified, and generated content for real articles, and then failed at the very last step, saving to the database, with a "can't reach database server" error.
- Cause
- Neon's free tier automatically suspends the database after a period of inactivity to save cost. The agent spends several minutes talking only to the AI model between database calls, which was long enough for the database to go to sleep, and the existing database connection did not automatically recover once it woke back up.
- How it was found
- The failure happened at the final database save after the AI work had already completed.
- Fix
- Added a small retry helper, withDbRetry in lib/db.ts, that waits a few seconds and tries again up to several times if a database call fails before giving up.
- Lesson
- Any pay-for-what-you-use cloud service that can sleep when idle needs code that expects and handles that sleep/wake cycle, not code that assumes a connection made once will always still work later.
6. The scheduled version of a working script failed instantly
- Symptom
- Running the agent by hand in Terminal worked. The exact same command, scheduled to run automatically via launchd, failed immediately with a "node: No such file or directory" style error.
- Cause
- launchd runs scheduled jobs with a very minimal set of environment variables, and does not read your shell's normal startup files, like .zshrc or .bash_profile, the way an interactive Terminal session does. Without those, even basic commands like npx could not be located.
- How it was found
- The failure only happened under launchd, not in an interactive Terminal run.
- Fix
- The script explicitly sets its own PATH at the very top, listing the exact folders where node and npm live, instead of assuming they will already be findable.
- Lesson
- Code that works when you run it yourself may be relying on environment setup that only exists because you personally set it up once, long ago, and forgot about it. Anything meant to run unattended needs to be explicit about what it depends on.
7. The same scheduled script then failed a different way
- Symptom
- After fixing the PATH issue above, the very next attempt failed instead with a message about esbuild being installed for the wrong CPU architecture.
- Cause
- This particular Mac has more than one Node.js installation, from different tools, built for different chip architectures: Apple Silicon arm64 vs. Intel-compatible x64 under Rosetta. The project's dependencies had been installed using one specific Node version, but the PATH fix from the previous bug happened to put a different Node version first, so native binary packages loaded the wrong-architecture file.
- How it was found
- The architecture error appeared only after the PATH problem was fixed enough for the script to reach dependency loading.
- Fix
- Pinned the PATH to point at the exact Node installation that had actually been used for npm install, instead of any Node that happened to be findable.
- Lesson
- Fixing one bug can reveal a second, different bug that was hidden behind it. The first fix was correct and necessary, it just was not sufficient by itself.
8. A working page started failing after switching machines or restarting
- Symptom
- The exact same dashboard code, that had worked minutes earlier, started returning a 500 error about Prisma's Query Engine not being found for a specific chip architecture.
- Cause
- The root cause was the same as bug 7, but hitting Prisma's native binary this time instead of esbuild's. Whichever Node process happened to run prisma generate determined which single architecture's binary got built, and a later, different Node process could not use it.
- How it was found
- The error looked different from the esbuild error, but the architecture-specific native binary detail pointed back to the same multiple-Node-installs problem.
- Fix
- Told Prisma to generate binaries for multiple architectures at once, using the binaryTargets setting in schema.prisma, so it works no matter which installed Node happens to run it.
- Lesson
- The same underlying cause, multiple Node installs on one machine, can surface as different-looking errors in different tools. Once you have correctly diagnosed a root cause once, it is worth checking whether other, seemingly unrelated failures share it.
9. A one-time setup script would not see its own configuration
- Symptom
- While building the LinkedIn login helper script, a quick test showed that a value clearly present in .env was read back as undefined inside a script run with tsx.
- Cause
- Unlike the Next.js dashboard, which loads .env automatically, and unlike the main agent script, which had .env explicitly loaded by the shell script that runs it, this new standalone script had no explicit .env loading at all, and tsx does not provide that automatically.
- How it was found
- A quick script test showed the value as undefined even though the key was present in .env.
- Fix
- Added a small, dependency-free function at the top of the script that reads and parses .env itself.
- Lesson
- Do not assume a behavior you observed working in one part of a project applies everywhere else in the same project. Different tools and entry points can have different defaults.
10. Stat cards on the dashboard showed permanently stale zeros
- Symptom
- The Overview page's six stat cards (NEW/SCORED/GENERATED/APPROVED/REJECTED/PUBLISHED) showed GENERATED climbing forever while every other card stayed at zero, no matter how many posts were actually approved, rejected, or published.
- Cause
- The cards counted Article.status, but nothing in the codebase ever updates an article's status past GENERATED. Approving, rejecting, and publishing all happen on a GeneratedContent row (one article can have two: a Twitter draft and a LinkedIn draft, each with its own independent outcome), not on the Article itself.
- How it was found
- A user screenshot showing all-zero stat cards next to a clearly successful, real run with real counts in the log above it -- a direct mismatch between two pieces of data on the same page.
- Fix
- Pointed the cards at GeneratedContent.status instead, which is what actually changes when real review and publish actions happen.
- Lesson
- When a database has two related tables, decide up front which one a summary view actually reflects, and revisit that decision if you later add a feature that breaks the original assumption.
11. A production redirect silently sent real visitors to a stranger's website
- Symptom
- Visiting the deployed dashboard while logged out should redirect to the login page. Testing revealed it was instead redirecting to a completely unrelated, real third-party website, not this project's actual domain.
- Cause
- NEXTAUTH_URL in Vercel's production environment variables had been set, early in the project, to a guess at the eventual domain before the real one was known, and it was never corrected afterward.
- How it was found
- Not by manual testing (every manual check reused an already-valid login session, which never exercises this code path) -- it was found while writing an automated end-to-end test for exactly this scenario.
- Fix
- Corrected NEXTAUTH_URL to the real domain in Vercel, redeployed, and confirmed with a fresh, no-cookie request that the redirect now points to the right place.
- Lesson
- This is exactly the class of bug that only automated, literal browser testing catches. Every prior API test had always carried a valid session cookie forward, so this path had genuinely never been exercised.
12. The automated test suite passed locally but failed in CI for 8 minutes
- Symptom
- The Playwright test suite passed cleanly when run by hand. The exact same suite, run automatically by GitHub Actions on every push, hung for over 8 minutes and then failed every test that needed to log in.
- Cause
- Auth.js, the login library, refuses to process requests from a host it does not recognize as trustworthy, unless told otherwise. That trust is automatic on Vercel, but a plain production build run anywhere else hits this check for real.
- How it was found
- By deliberately reproducing the exact CI environment locally instead of guessing from the CI logs alone, which only showed a generic 'server configuration problem' message.
- Fix
- Added a trustHost setting to the login configuration, which is safe here since the app already controls the real expected host itself via NEXTAUTH_URL.
- Lesson
- A generic, security-conscious error message can hide a very specific, easy fix. When a tool's user-facing error is intentionally vague, check its own server-side logs first.
13. LinkedIn Company Page posting was blocked by a hard platform rule, not a review queue
- Symptom
- LinkedIn's own API for posting to a Company Page showed a permanently greyed-out Request access button, with no way to even submit a request, let alone get approved.
- Cause
- LinkedIn does not allow requesting that specific product on a developer app that already has any other product added. The existing app already had two other products on it.
- How it was found
- LinkedIn's own developer documentation, which states this rule directly, confirmed by the exact tooltip shown on the greyed-out button.
- Fix
- Used Zernio, a third-party service that already holds LinkedIn's Community Management API approval as a partner, instead of creating a second developer app and waiting on a fresh LinkedIn review with no fixed timeline.
- Lesson
- A platform's own official API is not always the fastest real path to a working feature. A well-established, honestly-labeled third-party service that already did the approval work can be a legitimate shortcut.
14. Platform tabs still used a leftover two-column layout
- Symptom
- After Review Queue posts were split into separate platform tabs, one selected platform still looked squeezed into half the available width, leaving a large empty area beside it instead of letting the post card use the full row.
- Cause
- The UI still had a two-column grid from the older mixed-platform layout, where Twitter/X and LinkedIn drafts appeared side by side at the same time. Once the platforms moved into separate tabs, that grid no longer matched the data on screen, but it was still forcing each tab's single visible card to behave like one of two columns.
- How it was found
- By checking the rendered Review Queue layout after the platform-tab change and noticing that the selected tab still reserved space for a second platform that was no longer visible.
- Fix
- Removed the leftover two-column wrapper for the platform-tab view, so the active platform's generated-post card can fill the available width.
- Lesson
- When a layout changes from showing multiple things together to showing one selected thing at a time, the old container rules need to be reviewed too. Hiding the extra content is not enough if the parent layout still thinks it is arranging multiple items.
15. Automated mobile tests passed, but the page still overflowed horizontally on a real phone width
- Symptom
- New mobile-specific Playwright tests for the Review Queue passed cleanly, but a direct check of the page's actual rendered width at a real phone viewport (375px, iPhone SE size) showed the page was nearly 3 times wider than the screen.
- Cause
- Two issues stacked together. A CSS Grid holding the editable textarea and its live preview side by side used the standard responsive pattern, but Grid items default to a minimum width equal to their content's natural size, so even the single-column layout refused to shrink. Separately, the post text (often containing a long unbroken URL) used a style that preserves line breaks but does not allow breaking a single long word.
- How it was found
- The automated tests checked that elements were visible and clickable, which they were -- they never checked the actual pixel width of the rendered page. A deliberate check comparing scrollable width against visible width at a real phone size caught what the functional tests could not.
- Fix
- Added a Tailwind utility (min-w-0) to the grid container and its children so they could actually shrink, and a second utility (break-words) to the post-text paragraph so a long unbroken URL wraps instead of forcing the page wider.
- Lesson
- A test confirming an element is visible is not the same as confirming the page fits the screen it claims to support. For responsive design, measure the real rendered width at a real target size, do not just check that responsive-looking class names exist in the code.