NewDayKnowledge

How this AI news platform was built, and how it works

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.