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 %}
2026-01-28 05:40:39
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.
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.
serve-static middleware4.11.7)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
}
Remediation Steps:
wrangler.toml configuration to ensure proper namespace bindings.Read the full report for CVE-2026-24473 on our website for more details including interactive diagrams and full exploit analysis.
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:
By this point your rest timer expired 45 seconds ago and you're late for your next set.
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.
Here's what we're running:
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:
clientId
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.
What worked:
clerkMiddleware (v5+) works perfectly with Next.js 16What hurt:
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.
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.
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?
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 },
],
}),
});
Ask GPT-4 to generate a workout routine and you get:
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.
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.
Here's the entire interaction model for logging a set:
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.
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.
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.
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.
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.
We built the "perfect" UI in our heads, then watched actual users struggle with it.
OpenTrainer is in alpha. It's free. No ads. No tracking pixels. Your data exports as JSON whenever you want.
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
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:
If you're building something similar, use Convex. Seriously. It's rad for real-time apps.
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.
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/
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