DEV Community

Cover image for dev.to's Dashboard Can't Count Its Own Posts
Daniel Nwaneri
Daniel Nwaneri Subscriber

Posted on

dev.to's Dashboard Can't Count Its Own Posts

Summer Bug Smash: Clear the Lineup 🐛🛹

This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.

Project Overview

forem is the open source platform behind dev.to itself. I've had it starred and cloned for months and never opened the codebase — it's Rails, and I don't write Ruby. Jess's post was the reason that finally changed.

github.com/forem/forem

Bug Fix or Performance Improvement

#23687 is a one-line report: a user published exactly one post, and the dashboard's "Posts" counter said 2.

Not writing Ruby meant I couldn't guess my way to the fix from vibes. I had to actually trace it — reading DashboardsController, the sidebar partials, and the Article model until the shape of the bug was undeniable, not assumed.

The "Posts" badge in the dashboard sidebar renders @user.articles_count — a counter_culture cache on User that increments for every Article row belonging to that user, full stop. No filter on type, no filter on state:

# app/models/article.rb
counter_culture :user
Enter fullscreen mode Exit fullscreen mode

But the link that badge sits on always opens the same default view: DashboardsController#show with no params. That view only lists non-archived, full-post-type articles:

# app/controllers/dashboards_controller.rb
@articles = target.articles.from_subforem.includes(:organization)
@articles = params[:state] == "status" ? @articles.statuses : @articles.full_posts
@show_archived = params[:filter].to_s.casecmp("archived").zero?
Enter fullscreen mode Exit fullscreen mode

Forem has three article types — full_post, status (a short "Boost" update), and fullscreen_embed — and the counter doesn't distinguish between them, or between archived and active. The badge counts everything. The list under it shows a strict subset. Anyone who's ever posted a status update, or archived a post, sees a number that doesn't match what they can actually click into and see — exactly what got reported in #23687.

I couldn't verify that in Ruby, but I recognized the shape of it instantly once it was laid out: a cached count drifting from what a filtered view actually renders. I've shipped that exact bug in JavaScript. Same failure, different syntax.

Code

github.com/forem/forem/pull/23690

The fix doesn't touch the shared articles_count counter — that cache is read elsewhere for badges and spam heuristics, where "every article this user has ever made" is the correct meaning. Instead, DashboardsController gets a helper scoped to match what the Posts tab actually renders, and both the full-page and AJAX sidebar actions use it instead of the raw cache:

# The "Posts" nav item always links to the default (non-archived, full posts
# only) view of the user's own dashboard, so its indicator should reflect
# that same scope rather than the user's raw articles_count, which also
# includes statuses and archived posts that never show up in that list.
def posts_count_for(user)
  user.articles.from_subforem.full_posts.where(archived: false).count
end
Enter fullscreen mode Exit fullscreen mode

My Improvements

There was no way for me to eyeball this and trust it — I can't read Ruby well enough for that, and there's no Ruby or Postgres on the machine I was working from, so I couldn't run the spec suite locally either. Verification had to happen somewhere else: I wrote regression specs asserting a user with one full post, one status, and one archived post should see a count of exactly 1, pushed the branch, and let Forem's own CI be the judge instead of my own confidence.

CI caught something real on the first run — not in the fix, in my test. create(:article, type_of: "status") failed its own model validation, because status-type articles in Forem aren't allowed to have body markdown, and the factory's default does. I found the pattern already used elsewhere in the suite (body_markdown: "", main_image: nil), fixed the two specs, and pushed again.

That failure is the actual proof this wasn't guesswork dressed up as a fix. If I'd been able to run specs locally I might have caught it before pushing; instead the project's own CI did the job a local run would have.

Same lesson my other two entries kept landing on: The Cloudflare Worker That Ran Perfectly and Still Failed Twice and I Was Filming a Demo of My Monitoring Tool. The Monitor Wasn't Monitoring. — "it compiled" and "it's correct" are different claims, and only one of them is worth trusting.

Everything's green now — 19 successful checks, 1 skipped, 0 failures, including the shard that runs dashboard_spec.rb. The PR is open against forem/forem and waiting on a maintainer review, since third-party fork PRs need one before merge. Not merged yet as of writing this — I'd rather say that plainly than imply otherwise.


Different from my other two entries in one way: I don't write Ruby. Claude found the bug and wrote the fix. I picked the issue and gated everything that left my machine — the fork, the push, the PR, the CLA. Full delegation on the code, not on whether it shipped.

Top comments (24)

Collapse
 
sylwia-lask profile image
Sylwia Laskowska

Great post! I'd love to contribute something to Forem one day as well!

Also... those submissions are way too good. I was counting on winning that skateboard! 😂

Collapse
 
dannwaneri profile image
Daniel Nwaneri

Sylwia, do it . mine sat starred and uncloned for months before I actually opened it. The hardest part isn't the code, it's the opening. (And honestly, I'd trade the skateboard for the merge . still waiting on that part.)

Collapse
 
merbayerp profile image
Mustafa ERBAY

I enjoyed the emphasis on verification over confidence. Not knowing Ruby didn’t stop you from reasoning about the bug because the underlying problem—cached state diverging from filtered data—is language-agnostic. Regression tests and CI are much stronger evidence than “it looks right.” Great contribution, and good luck with the merge!

Collapse
 
dannwaneri profile image
Daniel Nwaneri

Mustafa, language-agnostic is the plausible way to say what I was circling. The shape of the bug (cache vs. filtered view drifting apart) I could reason about cold.

Writing the actual fix in idiomatic Ruby and knowing counter_culture's real behavior instead of guessing at it, wasn't language-agnostic at all. That's the part I still needed Claude for.

Collapse
 
merbayerp profile image
Mustafa ERBAY

That’s a distinction I completely agree with. Understanding the failure mode and implementing an idiomatic fix are two different skills.

AI is becoming remarkably good at bridging language-specific knowledge, but it’s still up to the developer to define the problem, challenge assumptions, and verify that the solution preserves the system’s intended behavior. That’s where engineering judgment still makes the difference. Thanks for the clarification

Thread Thread
 
dannwaneri profile image
Daniel Nwaneri

That's the whole job description now, honestly. defining the problem and setting the verification bar are the parts that don't delegate.

Collapse
 
gnlassi profile image
Ghulam Nabi

Well said.
Reasoning about the bug = language agnostic.
Shipping the fix in idiomatic Ruby + knowing counter_culture internals = not agnostic at all 😅

Glad Claude helped bridge that gap.

Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala

I don't know Ruby either, so this was nice to read 😄 I'd love to contribute to Forem one day too, but the Ruby codebase has always made me a little hesitant to try.

Hoping your PR gets merged, Daniel! Great work on this one.

Collapse
 
dannwaneri profile image
Daniel Nwaneri

Hema ,same boat not knowing Ruby is basically the whole premise of this one. The blocker was never reading Rails, it was reading Rails alone. Claude did the actual code work; I picked the target and gated everything that left my machine before it shipped. That's a real workaround for genuinely not knowing a languag, not a training-wheels version. What's the first thing you'd want to fix if you gave it a shot??

Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala

I haven't explored the Forem repo much yet, so I don't have a specific fix in mind 😄 I'd probably start with one of the good first issues and see where that takes me. Definitely want to give it a shot though!

Thread Thread
 
dannwaneri profile image
Daniel Nwaneri

Hema, heads up before you go looking: I checked and "good first issue" had zero open matches on forem/forem when I went hunting for mine . 213 closed, none open. The "always open for contribution" bug label is what's actually active right now. Worth starting there instead.

Collapse
 
svyatov profile image
Leonid Svyatov

@hemapriya_kanagala look at the controller snippet in the post, most of a Rails app looks like that. Ruby is much easier to read than to write, the hesitation drops fast once you start.

Collapse
 
leob profile image
leob

I think I've noticed some occasional "glitches" in 'like' counters on articles and comments, but the glitches always seemed ephemeral or temporary ... nice job you did with this one!

Collapse
 
dannwaneri profile image
Daniel Nwaneri

leo , that tracks, actually. Reactions run through the same counter_culture mechanism I found powering the posts count in this bug (app/models/reaction.rb). I didn't dig into why yours self-corrects but same underlying pattern, different symptom. If it's reproducible, might be worth its own issue.

Collapse
 
leob profile image
leob

I ran into one instance where it did not self-correct - the scenario was like this:

I posted a comment, and then normally it always starts with 1 like - that happens automatically, even when nobody clicks "like": it starts with 1, not with 0 ...

But in this case it started with zero :-)

So I just liked my own comment (only case I've ever done that, lol), to "fix" it :-)

Thread Thread
 
dannwaneri profile image
Daniel Nwaneri

hadn't heard that before — comments seeding at 1, not 0, by default. If that's real, that's a different failure shape than mine: I had a stale read, yours sounds more like a missing default write. The self-fix by liking your own comment is the best repro step in this whole thread, 😀😀😀

Thread Thread
 
leob profile image
leob • Edited

Very, very rare "glitch", a comment starting out at 0 instead of 1 (likes, that is) - in all those years I've seen it just once ... yeah, liking my own comment was a very effective fix! 😁

Thread Thread
 
dannwaneri profile image
Daniel Nwaneri

leo, one in years is basically the control group confirming it's real. 😁

Thread Thread
 
leob profile image
leob

It was absolutely real, I'm not making it up - I'm also sure I wasn't dreaming :-)

Must have been something "really rare", but no idea what - race condition? Cosmic ray or meteorite hitting some piece of hardware? Dunno, the only thing I know is that it happened ...

Collapse
 
akash_yadav_03c587d791345 profile image
akash yadav

Dev.to’s dashboard may sometimes display an incorrect post count because of caching delays, draft or published status differences, deleted posts, or temporary synchronization issues. If the number does not match the posts visible on your profile, checking the published-post list, refreshing the dashboard, or waiting for the platform to update may help.

At Aqva Marketing, accurate content tracking is important for measuring publishing consistency, audience engagement, and SEO performance. When platform dashboards show inconsistent data, it is better to verify the actual published content and use additional analytics tools rather than relying on one metric alone.

Collapse
 
talha_ramzan_3878156fea8c profile image
Talha Ramzan

"I don't write Ruby" and then casually opens a PR against the actual dev.to codebase is the programming equivalent of "I don't really cook" right before someone plates a soufflé. Also deeply relatable that the bug was a counter counting things a filter refuses to show, the "it's not lying, it's just answering a different question" defense is doing a lot of work for both software and my own excuses on why my step count doesn't match how tired I feel.

Collapse
 
tech_grundy profile image
The Tech Grundy

"A cached count drifting from what a filtered view actually renders" is such a classic, language-agnostic bug.

The decision not to touch the global articles_count cache on User is spot on. Leaving the raw counter intact for spam heuristics while introducing a dedicated helper (posts_count_for) scoped to active, non-archived full posts fixes the UX without breaking background logic. Great writeup on tracing the flow through DashboardsController without relying on Ruby familiarity!

Loved the section on relying on CI when local setups aren't available. Catching the create(:article, type_of: "status") validation failure because the factory default included markdown body text was a neat demonstration of how factory defaults and model validations can surprise you.

Writing explicit regression specs asserting that a user with 1 post, 1 status, and 1 archived post sees a count of exactly 1 makes this PR super clean. Great contribution to the Bug Smash!

Collapse
 
gnlassi profile image
Ghulam Nabi

Excellent piece of post.

Collapse
 
svyatov profile image
Leonid Svyatov

@dannwaneri I read my own dev.to numbers through the API and not the dashboard, mostly out of habit. Now I have an actual reason :)