MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

Earth WebGL Demo

2026-01-28 05:44:07

Photorealistic 3D earth and space scene demo rendered and animated in WebGL.
https://github.com/enesser/earth-webgl

{% codepen https://codepen.io/zaujwujw-the-builder/pen/KwMZwNb %}

CVE-2026-24473: The Infinite Fallback: How Hono Leaked Your Cloudflare KV Keys

2026-01-28 05:40:39

The Infinite Fallback: How Hono Leaked Your Cloudflare KV Keys

Vulnerability ID: CVE-2026-24473
CVSS Score: 6.3
Published: 2026-01-27

A logic flaw in Hono's serve-static middleware for Cloudflare Workers allowed attackers to bypass the asset manifest and read arbitrary keys from the underlying KV storage. It turns out that a convenient fallback mechanism is indistinguishable from a gaping security hole.

TL;DR

Hono's static asset adapter used a logical OR operator (||) to fallback to the raw request path if a file wasn't found in the manifest. This allowed attackers to request any key present in the Cloudflare Workers KV namespace, potentially exposing internal configuration or secrets if they shared the same storage bucket as your cat photos.

Technical Details

  • CWE ID: CWE-200 / CWE-668
  • CVSS v4.0: 6.3 (Medium)
  • Attack Vector: Network
  • Privileges Required: None
  • Impact: Information Disclosure
  • Patch Status: Released (v4.11.7)

Affected Systems

  • Hono Framework (JavaScript)
  • Cloudflare Workers using serve-static middleware
  • Hono: < 4.11.7 (Fixed in: 4.11.7)

Code Analysis

Commit: cf9a78d

fix: serve-static for Cloudflare Workers reads arbitrary key

--- a/src/adapter/cloudflare-workers/utils.ts
+++ b/src/adapter/cloudflare-workers/utils.ts
@@ -36,7 +36,7 @@ export const getContentFromKVAsset = async (
     ASSET_NAMESPACE = __STATIC_CONTENT
   }

-  const key = ASSET_MANIFEST[path] || path
+  const key = ASSET_MANIFEST[path]
   if (!key) {
     return null
   }

Exploit Details

  • N/A: No public exploit code available yet, but trivial to reproduce manually.

Mitigation Strategies

  • Enforce Strict Whitelisting: Ensure that static file servers only serve files explicitly listed in a manifest.
  • Namespace Isolation: Never store sensitive configuration or internal application state in the same KV namespace used for public static assets.
  • Input Validation: Sanitize and validate all path parameters before passing them to storage backends.

Remediation Steps:

  1. Upgrade Hono to version 4.11.7 or higher immediately.
  2. Audit your Cloudflare Workers KV namespaces. If you are mixing secrets and assets, separate them into distinct namespaces.
  3. Review your wrangler.toml configuration to ensure proper namespace bindings.

References

Read the full report for CVE-2026-24473 on our website for more details including interactive diagrams and full exploit analysis.

I Didn't Like Other Workout Tracking Apps, So I Built My Own.

2026-01-28 05:37:27

I've been lifting for years. And I've tried every workout app out there. Strong. Hevy. FitNotes. Ladder. They all fall short in the same predictable way.

You're 30 seconds into your rest period, sweaty, trying to log a set, and the app wants you to:

  1. Tap "Add Set"
  2. Tap the weight field
  3. Wait for the keyboard
  4. Type the weight
  5. Dismiss the keyboard
  6. Tap the reps field
  7. Wait for the keyboard AGAIN
  8. Type the reps
  9. Dismiss the keyboard AGAIN
  10. Scroll to find the "Save" button
  11. Tap "Save"

By this point your rest timer expired 45 seconds ago and you're late for your next set.

The Problem Nobody Was Solving

Most workout apps treat logging like you're sitting at a desk with a cup of coffee and unlimited time.

Meanwhile, in the real world, you've got maybe 60-90 seconds between sets.

So we built OpenTrainer. Two taps per set.

The Tech Stack (And Why We Chose It)

Here's what we're running:

  • Next.js 16
  • Convex
  • Clerk
  • OpenRouter
  • shadcn
  • Vercel

Why Convex?

Convex is awesome for this use case.

Traditional workout apps use REST APIs. You tap "Log Set" → HTTP request → server processes → database write → HTTP response → UI updates. If you're on sketchy gym WiFi, that's 2-3 seconds of loading spinner hell.

Convex gives us real-time sync with zero setup. The mutation fires, the optimistic update shows instantly, and the actual database write happens in the background. If it fails, it rolls back automatically. If you go offline mid-workout, it queues the mutations and syncs when you're back online.

We have optimistic updates working with seven lines of code:

const logSet = useConvexMutation(api.workouts.addLiftingEntry);
const handleLogSet = () => {
  logSet({
    workoutId,
    exerciseName: "Bench Press",
    clientId: generateClientId(), // Deduplication
    kind: "lifting",
    lifting: { setNumber: 1, reps: 8, weight: 225, unit: "lb" }
  });
  // UI updates instantly. Done.
};

Convex handles:

  • Optimistic UI updates
  • Deduplication via clientId
  • Offline queueing
  • Conflict resolution
  • Real-time sync across devices

The Schema

We use a discriminated union pattern for entries. One table, multiple exercise types:

// entries table
{
  kind: "lifting" | "cardio" | "mobility",
  lifting?: {
    setNumber: number,
    reps: number,
    weight: number,
    unit: "kg" | "lb",
    rpe?: number
  },
  cardio?: {
    durationSeconds: number,
    distance?: number,
    intensity?: number
  },
  mobility?: {
    holdSeconds: number,
    perSide?: boolean
  }
}

This beats having separate tables for lifting_entries, cardio_entries, mobility_entries. Querying a workout's full history is a single query. This way if we want to add a new exercise category we just extend the union.

Next.js 16

What worked:

  • Parallel routes made the bottom navigation clean
  • Metadata API makes SEO easier
  • Clerk's new clerkMiddleware (v5+) works perfectly with Next.js 16

What hurt:

  • The dev server randomly decides to recompile everything. Often.
  • Hot reload occasionally needs a manual refresh
  • Some Convex types needed manual casting (minor annoyance)

Auth Setup: Clerk + Convex requires a JWT template in the Clerk dashboard. Standard stuff, nothing custom:

// convex/auth.config.ts
export default {
  providers: [{
    domain: process.env.CLERK_JWT_ISSUER_DOMAIN!,
    applicationID: "convex",
  }],
}

You just need to grab your JWT issuer URL from Clerk and set it as an env var. That's it.

The AI Layer

Here's where we diverged from every other fitness app.

Most apps with "AI" just slap ChatGPT on a prompt and call it a day. We wanted something smarter.

Why OpenRouter Instead of Direct APIs

We use OpenRouter as our AI gateway. One API, multiple models. Right now we're using Gemini 3 Flash through OpenRouter's unified endpoint.

Why OpenRouter?

  • No vendor lock-in - Switch models with one line of code
  • Cost optimization - OpenRouter shows pricing for every model
  • Fallback logic - Can implement automatic failover to Claude/GPT if Gemini is down
  • One API key - Don't need separate Google, Anthropic, OpenAI accounts
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.OPENROUTER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "google/gemini-3-flash-preview",
    messages: [
      { role: "system", content: systemPrompt },
      { role: "user", content: userMessage },
    ],
  }),
});

The Routine Generation Problem

Ask GPT-4 to generate a workout routine and you get:

  • Generic "bro split" garbage
  • Exercises you don't have equipment for
  • Zero consideration for your actual goals
  • Hallucinated exercise names ("reverse cable overhead skull-tricep extension")

We built an equipment audit during onboarding. User checks off what they have access to. Planet Fitness member? Cool, we know you have Smith machines and dumbbells, not barbells. Home gym? Tell us what you've got.

Then we pass that to Gemini with a structured prompt:

const routinePrompt = `
Generate a ${goal} program for ${experienceLevel} lifter.

EQUIPMENT AVAILABLE:
${equipment.join(", ")}

CONSTRAINTS:
- ${weeklyAvailability} days per week
- ${sessionDuration} minutes per session
- Must use ONLY the equipment listed above
- Use standard exercise names (no made-up movements)

OUTPUT FORMAT: JSON matching this schema...
`;

This works way better than you'd expect. Gemini's actually pretty good at constraint satisfaction when you give it clear boundaries.

The Training Load Calculation

Most apps calculate "volume" as sets × reps × weight. That's... fine. But it doesn't account for intensity.

We use a modified training load formula that factors in RPE (Rate of Perceived Exertion):

function calculateTrainingLoad(entry: Entry): number {
  if (entry.kind !== "lifting") return 0;

  const { reps = 0, weight = 0, rpe = 5 } = entry.lifting;
  const volume = reps * weight;
  const intensityFactor = rpe / 10;

  return volume * intensityFactor;
}

A set of 5 reps at 225 lbs with RPE 9 (brutal) counts for more than 10 reps at 135 lbs with RPE 5 (warm-up). This gives us way better insights into actual training stress.

The UI Philosophy

Here's the entire interaction model for logging a set:

  1. Tap the weight number → Quick adjust (+5 lb) or type any value
  2. Tap "Log Set" → Done

We use large tap targets (minimum 48px). Everything important is in thumb-reach on a phone held one-handed. The rest timer starts automatically after you log a set. Haptic feedback on every interaction so you get confirmation even with sweaty hands.

The Stepper Component

Building good steppers for weight/rep adjustment was surprisingly tricky:

<SetStepper
  value={weight}
  onChange={setWeight}
  step={5}  // +/- 5 lbs per tap
  min={0}
  max={1000}
  label="Weight"
  unit="lb"
  enableDirectInput  // Tap the number to type
/>

We're using optimistic updates here too. The value updates in local state immediately, then fires a debounced mutation to Convex to persist it.

The Stuff That Broke

Problem #1: The Rest Timer Kept Getting Killed

Mobile browsers are aggressive about killing background JavaScript. We had users complaining the rest timer would stop if they switched to Spotify between sets.

Solution: Web Workers + Service Workers + loud-ass notifications.

// rest-timer-worker.ts
self.addEventListener("message", (e) => {
  if (e.data.action === "start") {
    const interval = setInterval(() => {
      self.postMessage({ timeRemaining: getRemainingTime() });
    }, 1000);
  }
});

Plus we request notification permissions so we can send an annoying alert when rest time is up. It works. Users hate it. But they also don't miss their rest periods anymore.

Problem #2: Clerk JWT Validation Failing Randomly

Convex needs to validate Clerk JWTs. The issuer domain kept changing between clerk.accounts.dev and accounts.dev for a reason I'm still not clear on.

We set up a .env var for the issuer domain and made it configurable.

Problem #3: The "My Phone Died Mid-Workout" Problem

Users would start a workout, log 10 sets, phone dies, come back and... everything's gone.

We added a recovery mechanism:

// On app load, check for incomplete workout
const checkForIncompleteWorkout = async () => {
  const activeWorkout = await getActiveWorkout(userId);
  if (activeWorkout && activeWorkout.status === "in_progress") {
    // Show "Resume workout?" dialog
    showResumeDialog(activeWorkout);
  }
};

Now if your phone dies, the workout stays in "in_progress" state. When you open the app again, it asks if you want to resume. Crisis averted.

What We'd Do Differently

1. User Testing Sooner

We built the "perfect" UI in our heads, then watched actual users struggle with it.

Try It

OpenTrainer is in alpha. It's free. No ads. No tracking pixels. Your data exports as JSON whenever you want.

Try it here

Or clone the repo and run it yourself. It's Apache 2.0 licensed. Do whatever you want with it.

GitHub: house-of-giants/opentrainer

TLDR

We built a workout tracker that doesn't suck. Two taps per set. Real-time sync. AI that actually knows your gym has a Smith machine, not a barbell. Next.js + Convex + Clerk + OpenRouter.

The hardest parts weren't the tech. They were:

  1. Making everything feel instant (optimistic updates everywhere)
  2. Keeping timers alive on mobile (Web Workers + notifications)
  3. Building UI that works with sweaty hands (big tap targets, haptic feedback)

If you're building something similar, use Convex. Seriously. It's rad for real-time apps.

Decision Latency Is the Real Risk in Projects

2026-01-28 05:33:37

**Most projects don’t fail because the plan was wrong.

They fail because critical decisions take too long.

Not because no one knows what needs to be done.
Not because the data is missing.
But because the decision keeps getting delayed, softened, or deferred.

A week becomes a sprint.
A sprint becomes a phase.
A phase becomes “we’ll deal with it later.”

On paper, the project is still moving.
In reality, momentum is leaking out through indecision.

This is what I mean by decision latency.

It’s the time between when a decision becomes necessary and when someone is willing to own it. That gap is where most project risk is created.

What makes it dangerous is that it usually looks reasonable.

“We just need one more data point.”
“Let’s see how it trends.”
“We’ll take that offline.”
“We’ll re-baseline next cycle.”

Each of those sounds sensible in isolation. Together, they quietly stall the project.

Tools don’t fix this.

Dashboards, frameworks, and AI can improve visibility and analysis, but they don’t reduce decision latency. In some cases, they make it worse by giving people more reasons to wait.

More data.
More scenarios.
More options.

But no decision.

I’ve written before about why AI struggles with real project work. This is a big part of it. AI can tell you what usually happens next. It can’t tell you which trade-off you’re prepared to live with when the information is incomplete and the pressure is real.

That choice isn’t analytical.
It’s accountable.

Decision latency also hides behind good governance.

Steering committees meet. Papers are circulated. Risks are noted. Actions are captured. And still, the core decision gets deferred because no one wants to be the one who makes it too early.

The longer that goes on, the fewer options remain. By the time the decision is forced, the project has already paid the price.

Experienced project managers learn to recognise this early. They stop asking, “Do we have enough information?” and start asking a harder question:

“What happens if we don’t decide now?”

That’s usually when the real risk becomes visible.

Projects don’t need perfect information.
They need timely decisions with clear ownership.

Everything else is support.

threejs-shaders_ sphere-gears-shaders

2026-01-28 05:28:18

Check out this Pen I made! This is a Shader recreation based on the “LIVE Shade Deconstruction tutorial for Sphere Gears”, by Inigo Quilez, 2019 - https://iquilezles.org/

I built a client-side tax filer with React (No Database)

2026-01-28 05:23:29

I hated TurboTax so I built this using React and PDF-lib. It runs locally. Try it here: https://tax.redsystem.dev