DEV Community

Cover image for Dev Log: 3 September 2026 — Everything I Fixed Today Was Already Written
Nasrul Hazim
Nasrul Hazim

Posted on

Dev Log: 3 September 2026 — Everything I Fixed Today Was Already Written

Six commits across two codebases today — a deployment platform and an identity system. Different stacks, different problems. Reading them back in one sitting, the same shape six times:

The feature was written. Someone just couldn't reach it.

Not one of these was a missing implementation. The role management existed. The setup-command runner existed. The delivery tracking existed. The "already verified" message existed. All of it shipped, tested, working — and unreachable, because of a default, a branch, a missing entry point, or a token that had already been spent.

That's a genuinely different category of bug from "we haven't built it yet", and it's much harder to see, because every code review of the feature passes. The feature is fine. The path to it isn't.


The badge in the @else

Start with the smallest one, because it's the clearest.

A team page shows members and lets an owner change their roles. The component had addMember, changeRole and removeMember all along. But the page read as though role management didn't exist.

The badge naming a member's current role was inside the @else branch of @can('manageMembers').

Sit with what that means. A plain member saw everyone's role and could change nothing. An owner could change any role and never saw what it currently was — and the dropdown offered "Set as Lead", "Set as Member", "Set as Viewer" identically, with no marker on the one already true. The page never named a role to the one person allowed to act on it.

{{-- Before: the person with the power was the one kept in the dark --}}
@can('manageMembers', $team)
    <x-dropdown>...</x-dropdown>
@else
    <x-badge>{{ ucfirst($member->pivot->role) }}</x-badge>
@endcan

{{-- After: the badge is information, not a permission --}}
<x-badge>{{ $roleLabels[$member->pivot->role] }}</x-badge>
@can('manageMembers', $team)
    <x-dropdown>...</x-dropdown>
@endcan
Enter fullscreen mode Exit fullscreen mode

Two follow-ons worth naming:

The label came from ucfirst() on a raw pivot value. Untranslated, and it disagreed with the labels in the dropdown sitting right next to it, which came from the enum's label(). If you've got an enum with label()/color(), that enum is the only place a role should ever be turned into words. I pass a [value => label] map in from render() rather than resolving the enum in the Blade file — an inline @php() holding a nested static call is its own landmine in a component view.

Three of four confirmations were arriving a page late. The toast component lives in the layout and pulls session toast.* keys as it renders. So a session()->flash() from a Livewire action that doesn't redirect surfaces on the next full page load — which, for addMember, removeMember and changeRole, means never at the moment it meant anything. Those dispatch a browser event now. delete() still flashes, because it redirects, and that's what the flash mechanism is actually for.

If you're mixing Livewire actions and a layout-level toast: flash is for redirects, dispatch is for stays. Getting that backwards produces a bug where the user's action appears to do nothing and then a stale confirmation appears somewhere unrelated.

And a third: removeMember() refuses to remove the organisation owner, and reported that refusal via addError('memberEmail') — a field on the add member form. So clicking Remove put a validation error on an input the operator wasn't touching, and said nothing about what actually happened. It's a dispatched error toast now, and the menu item is disabled for the owner instead of offered and then refused. The server-side guard stays, obviously: a disabled control is not an authorisation check.

The runner with no door

Same shape, one level up.

There's an action and an MCP tool for running an application's declared post-install commands. Both have existed for weeks. What didn't exist was any way to reach them from the application's page in the UI.

So the workaround for "I need to run a seeder on this host" had become: push an empty commit to trigger a deploy. A workaround with a commit in it. That's the tell that something's unreachable rather than unbuilt — the workaround is absurd and everyone's fine with it.

The panel I added is mostly wiring, but two decisions in it are worth writing down.

The list of commands is read from the same config the deploy reads. Not a copy. Two lists drift, and the drift doesn't arrive as a diff — it arrives as "it worked when I ran it by hand and failed on deploy", six weeks later, on someone else's shift.

The refusal order matters, and it's the opposite of the obvious one. My deployment policy already refuses while an application is provisioning or being destroyed. So authorizing first handed an operator who is mid-deploy a bare 403 — for something that is not a permission problem at all. The status check runs first now, and says what's actually true:

public function __invoke(RunSetupRequest $request, Application $application): RedirectResponse
{
    // Status BEFORE authorization: a mid-deploy operator is not
    // unauthorized, and telling them so sends them to the wrong person.
    if (! $application->status->canRunSetup()) {
        return back()->with('error', $application->status->setupBlockedReason());
    }

    $this->authorize('runSetup', $application);

    // ...
}
Enter fullscreen mode Exit fullscreen mode

The usual objection to ordering it this way is information disclosure — you've told an unauthenticated-ish caller something about the resource before checking their rights. Worth checking every time, and here it's fine: the page is already tenancy-scoped, so anyone who can read that status could read it from the deployment list beside it. If that weren't true, authorize first and accept the worse error message. That trade-off is real, and it goes the other way often enough that it's worth naming rather than assuming.

Two smaller things:

  • The output is kept and shown. The reason an operator reached for this is that the app was refusing to start. "Setup completed" with no output leaves them exactly as unable to distinguish a seeded database from a silently skipped seeder as they were before.
  • The force checkbox carries a sentence, not the word "force". These commands are first-install by nature and mostly not idempotent — a seeder calling User::create() fails the second time, and re-provisioning a tenant database is worse than failing. The label says that.

It runs what the application already declared, in order, once. It is not a shell and not a step toward one. An arbitrary-command surface on a customer's production node is a different feature with a different threat model, and I'd want to design it as one.

The tracking that was complete, and off

Over in the identity system, every message on the mail-history detail screen read "No delivery events recorded" — including the account-verification mail whose delivery we're asked to prove more often than any other.

The package was complete. The defaults weren't. The config declared tracking as env('MAIL_HISTORY_TRACK_OPENS', false), and production's .env carried neither key. So the tracking routes were never registered and no pixel was ever injected. I probed it live before touching anything: both the open and click endpoints answered 404.

// config/mailhistory.php
'track_opens'  => env('MAIL_HISTORY_TRACK_OPENS', true),
'track_clicks' => env('MAIL_HISTORY_TRACK_CLICKS', true),
Enter fullscreen mode Exit fullscreen mode

Flipping a default is a one-line diff and a real decision. There is deliberately no UI switch for it — whether an activation email was opened is not an operator preference, it's an audit question. And defaulting to true means no environment needs an .env edit to be correct, which matters when the environment you most need it in is the one you touch least.

The consequence you have to think about: click rewriting genuinely starts running in production for the first time. So Laravel's signed email-verification URL joins the exclusion list, because its ?expires=...&signature=... is exactly the escaped-ampersand round trip that breaks under rewriting — and a broken signature locks an account out of verifying itself. Turning on a feature that has never run in production is a deploy, not a config change.

Two counting rules that went into the statistics service, both of which are the kind of thing that silently produces a plausible wrong number:

  • Engagement is counted from the events table, never from status. status holds only the latest state, so a click hides the open that preceded it. Count events, not states.
  • Count distinct messages, not event rows. One recipient opening four times must not push an open rate past 100%. If a percentage can exceed 100, the denominator is wrong.

Then a set of surfaces on top: summary cards computed inside the Livewire component so they follow the active filters instead of silently contradicting them; an empty state that distinguishes "nobody opened it" from "nothing was ever measured" (they used to render identically, which is how the original bug hid for so long); a report page with period, status and origin breakdowns; and three MCP tools behind the same permission as the screens.

The MCP tools never return a message body or a clicked URL — only the destination host. Verification and reset links live in there, and a read-only reporting tool is not a place to hand them out.

Every surface now reports the tracking state next to the figures. A number whose collection was off for an unknown period is not a number.

The token that was already spent

The best one, and the one with real user pain attached.

A personal-email verification link works once. The token is nulled the moment it's spent — correctly, that's the point. But the lookup is by that token. So a second click on a link that had worked perfectly fell into the Invalid or expired verification link branch. The already_verified branch sitting right below it was unreachable by construction.

$subject = Subject::where('verification_token', $token)->first();

if (! $subject) {
    return $this->invalidLink();   // ← every second click lands here
}

if ($subject->personal_email_verified_at) {
    return $this->alreadyVerified(); // ← unreachable: token is gone by now
}
Enter fullscreen mode Exit fullscreen mode

Users whose accounts had verified flawlessly were being told their account was broken. That's a support ticket, then six more.

Nulling the token stays. What changes is the response:

  • Post/Redirect/Get. On success it redirects to a landing route carrying the subject id in the session, so a refresh no longer replays a spent token URL. Verification is a state change; it should not be sitting behind a re-runnable GET in someone's history.
  • An unresolvable token redirects to that same landing page, which reports "Email Already Verified" rather than a red failure. And here's the honest part: once the token is gone, the two cases genuinely cannot be told apart. A never-issued token and a spent one look identical. So the page says what's true for the overwhelming majority, and keeps a quiet "never verified?" hint for the rest. When you can't distinguish two cases, pick the message that's right most of the time and leave a door for the exception — don't show a failure page to be safe.
  • The mail no longer claims the link "will expire in 24 hours". There's no expiry column, and the verification never checked the token's age. The claim was fiction — and it's precisely what made the old failure page read as plausible. A user clicks their link twice, sees "invalid or expired", remembers the email said 24 hours, and concludes the system is right and they're late. A false reassurance in an email is what turns a confusing error into a believed one.
  • The two dead failure views are deleted, along with the branch.

The one that wasn't already written

One bug today was mine, fresh, and shipped an hour earlier: a baseline of security headers I'd added to every generated nginx vhost was appending rather than deferring, so applications that set their own X-Frame-Options: DENY were answering with DENY and SAMEORIGIN, which browsers read as neither.

That one got its own post, because the nginx mechanics deserve the space — "nginx add_header appends. It doesn't override." (companion post, link at review time). Short version: add_header can't ask whether the upstream already sent a header, a map on $upstream_http_* can answer before you ask, and a security header added carelessly is a downgrade rather than an addition.

Worth noting how it was found, though, because it fits today's theme from the other side: I went and checked the live response headers right after claiming they were proven. The test suite was green. The suite verified the config generator, which was working exactly as written. Nothing in CI could have caught it, and I'd have believed CI for weeks.

Takeaway

Six bugs, one shape. If I had to write the search query for tomorrow:

  • Anything inside an @else. Ask who's in the other branch and what they lose.
  • Any branch that can't be reached given how the state above it is mutated. The already_verified check was dead code that looked like a safety net.
  • Any env(..., false) default in a config for a feature you assume is on. Then go probe the endpoint in production instead of assuming.
  • Any action reachable only from an MCP tool, a console command, or a deploy. If the workaround involves an empty commit, the door is missing.
  • Any two states that render identically. "No events recorded" meaning both nobody engaged and nothing was measured is how an off switch hides for months.

None of these show up as failing tests, because the code under test is correct. They show up when you use the thing as the person it was built for — which is the cheapest verification step available and the one easiest to skip.

Top comments (0)