惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
Apple Machine Learning Research
Apple Machine Learning Research
The Cloudflare Blog
WordPress大学
WordPress大学
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 叶小钗
博客园 - 聂微东
阮一峰的网络日志
阮一峰的网络日志
腾讯CDC
博客园 - 三生石上(FineUI控件)
V
V2EX
有赞技术团队
有赞技术团队
V
Visual Studio Blog
小众软件
小众软件
Jina AI
Jina AI
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - Franky
量子位
T
Tailwind CSS Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
P
Palo Alto Networks Blog
Cisco Talos Blog
Cisco Talos Blog
I
Intezer
Project Zero
Project Zero
A
Arctic Wolf
P
Privacy International News Feed
V
Vulnerabilities – Threatpost
L
Lohrmann on Cybersecurity
S
Securelist
C
Cybersecurity and Infrastructure Security Agency CISA
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Tor Project blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
Security @ Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Google DeepMind News
Google DeepMind News
N
News and Events Feed by Topic
TaoSecurity Blog
TaoSecurity Blog
L
LINUX DO - 热门话题
G
GRAHAM CLULEY
Help Net Security
Help Net Security
N
News | PayPal Newsroom
W
WeLiveSecurity
G
Google Developers Blog
Microsoft Security Blog
Microsoft Security Blog
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
C
Check Point Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
I shipped a paid web app in half a day using Claude Code + Codex (Garmin AI CSV converter)
naoki_JPN · 2026-05-07 · via DEV Community

Note: This article reflects information as of May 2026.

What I built

Garmin AI Export — a web app that converts the activity, sleep, and health data ZIP exported from Garmin Connect into clean CSVs that ChatGPT, Gemini, and Claude can readily analyze.

🔗 https://garmin-ai-export.vercel.app

If you've worn a Garmin watch for years, you've accumulated a massive trail of data: runs, sleep records, heart rate, stress, Body Battery — all of it. Garmin Connect lets you export everything, but what you get is a tangled bundle of nested JSON and binary FIT files. Hand that directly to ChatGPT and tell it to "analyze this," and it won't know where to start.

This app:

  1. Takes the raw ZIP you exported from Garmin Connect
  2. Unpacks and parses it entirely in your browser
  3. Returns a ZIP containing four files — activities.csv, sleep.csv, daily_health.csv, laps.csv — plus an AI prompt template

Feed that into the Code Interpreter on ChatGPT, Gemini, or Claude and you can immediately ask things like "show me how my pace has trended" or "find correlations between sleep and next-day performance."

And here's the punchline: I built and shipped this — including the payment integration — in about half a day, by pairing Claude Code (Opus 4.7) with Codex (GPT-5 Codex). I'll get into the role-split below, but the short version is: Claude Code played PM and reviewer; Codex did the implementation.

Tech stack

  • Next.js (App Router) + Tailwind CSS
  • JSZip — ZIP unpacking and re-compression
  • @streamparser/json — streaming parser for huge JSON
  • fit-file-parser — for Garmin's binary .fit files
  • PapaParse — CSV generation
  • Web Worker — to keep the main thread responsive
  • Square Payment Links — payments (Apple Pay / Google Pay supported)
  • Vercel — hosting

The key principle is "everything in the browser." Garmin exports can be hundreds of megabytes to multiple gigabytes — there's no way you're streaming that to a server (Vercel's request body limit is 4.5MB; you're done before you start). Plus, on principle, I didn't want users' health data sitting on my server.

So upload, parse, convert, and download all happen inside the user's browser. The only server-side code is a tiny API route that creates a Square payment link.

How Claude Code and Codex split the work

This project ran on two AI agents with clearly separated roles.

Role Agent Responsibilities
PM / Research Claude Code (Opus 4.7) Translate requests into GitHub Issues, manage labels, set priorities
Implementation Codex (GPT-5 Codex) Cut branches, write code, open Draft PRs
Code review Claude Code (Opus 4.7) Read PR diffs, leave sharp inline comments on GitHub
Merge / deploy Claude Code (Opus 4.7) Approve, squash-merge, verify production
Approval / GO Me (the human) Just say "OK," "implement #X," "ship it"

The actual flow looks like:

I describe what I want
  → Claude Code creates a GitHub Issue (label: triage)
  → I say "OK" → label changes to ready
  → I say "implement #X"
  → Codex cuts a branch, writes code, opens a Draft PR
  → I say "please review"
  → Claude Code posts a thorough review on GitHub
  → Codex addresses the comments
  → Claude Code re-reviews → LGTM, squash merge
  → Vercel auto-deploys
  → Claude Code curl-checks production

Enter fullscreen mode Exit fullscreen mode

What's interesting about this setup is that using a different model for review actually works. When Claude Code reads code that Codex wrote, there's no self-confirmation bias — comments like "disabled={!result} is dead code here," "you're using a private JSZip API," "the worker isn't terminated on reset" surface naturally. You don't get this when the same model writes and reviews.

I never wrote a line of code. I only said "I want this," "OK," and "ship it."

Things that bit me

I stepped on a few mines worth documenting.

1. Parsing 135MB JSON on the main thread froze the page

Garmin's *_summarizedActivities.json is over 100MB for heavy users. A naive JSON.parse(text) blocks the main thread for ~20 seconds and the browser starts asking the user if they want to kill the page.

The fix is two-layered.

(a) Move it to a Web Worker

// src/workers/garmin-converter.worker.ts
self.onmessage = async (event) => {
  const result = await convertGarminExportCoreBuffer(event.data.file, postProgress);
  self.postMessage({ type: "done", result }, { transfer: [result.buffer] });
};

Enter fullscreen mode Exit fullscreen mode

Putting the ArrayBuffer in the transfer list does a zero-copy handoff to the main thread. Even a 100MB+ buffer crosses the worker boundary instantly because nothing is copied.

(b) Stream-parse the JSON itself

Use @streamparser/json to iterate over the giant array element-by-element:

import { JSONParser } from "@streamparser/json";

const parser = new JSONParser({
  paths: ["$.*.summarizedActivitiesExport.*"],
});

parser.onValue = ({ value }) => {
  rows.push(extractActivityRow(value));
};

Enter fullscreen mode Exit fullscreen mode

The $.* prefix in paths is a sneaky trap. The actual Garmin JSON shape is [{"summarizedActivitiesExport": [...]}] — an array at the root. So you have to indicate "the array root" with $.*, otherwise nothing matches and your CSV ends up empty. I forgot this on the first pass and spent way too long staring at empty output.

2. Reaching into JSZip's private API and getting burned

The first version of the code, written by Claude, was using zip.file().internalStream("uint8array"). It worked, but it's an undocumented JSZip internal API with no type definitions. Review caught it and we rewrote it:

const buffer = await entry.async("uint8array");
const chunkSize = 1 << 20; // 1MB
for (let offset = 0; offset < buffer.length; offset += chunkSize) {
  // process 1MB at a time, yielding back to the event loop
  await yieldToEventLoop();
  processChunk(buffer.subarray(offset, offset + chunkSize));
}

Enter fullscreen mode Exit fullscreen mode

yieldToEventLoop() is just new Promise(resolve => setTimeout(resolve, 0)). Without it, you're back to freezing the UI on huge files.

3. The iOS Safari Blob + IndexedDB landmine

For the payment gate, I needed to persist the converted ZIP somewhere while the user bounces over to Square's checkout and back. The natural choice: stash a Blob in IndexedDB. But:

The biggest risk is the case where iOS Safari can't restore the Blob after payment.

This actually happens. iOS Safari has a long-standing bug where Blobs stored in IndexedDB come back corrupted when retrieved, especially for larger Blobs. "Paid the money, can't get the file" is the worst possible UX, so:

// Blob → ArrayBuffer before storing
const buffer = await result.blob.arrayBuffer();
await store.put({ buffer, files, filename, ... });

// Reconstruct the Blob from the ArrayBuffer when retrieving
const blob = new Blob([record.buffer], { type: "application/zip" });

Enter fullscreen mode Exit fullscreen mode

ArrayBuffer round-trips cleanly through IndexedDB even on iOS Safari. Catching this in review saved a lot of pain.

4. The Worker keeping running after a Reset

If a user clicks "Reset" while a conversion is in progress, you have to actually terminate the worker — otherwise it leaks memory and burns CPU.

let activeWorker: Worker | null = null;

export function abortConversion() {
  activeWorker?.terminate();
  activeWorker = null;
}

Enter fullscreen mode Exit fullscreen mode

Easy to forget. A review comment of "is this still running in the background after reset?" is what surfaced it.

Adding payments

I gated the download behind a paywall.

Why Square?

Candidates were Stripe, PayPal, Square, Paddle. Reasons I picked Square:

  • Stripe rejected my application (sole-proprietor application takes time)
  • Apple Pay and Google Pay are included by default (zero extra implementation)
  • Payment Links API gives you hosted checkout in seconds — no card form on my own site, so no PCI DSS scope

The implementation is shockingly small. Server-side, you just hit Square's API to mint a payment link:

// src/app/api/square/payment-link/route.ts
const squareResponse = await fetch(
  `${getSquareApiBaseUrl()}/v2/online-checkout/payment-links`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SQUARE_ACCESS_TOKEN}`,
      "Content-Type": "application/json",
      "Square-Version": "2026-01-22",
    },
    body: JSON.stringify({
      idempotency_key: crypto.randomUUID(),
      quick_pay: {
        name: "Garmin AI Export",
        price_money: { amount: PRICE, currency: "JPY" },
        location_id: process.env.SQUARE_LOCATION_ID,
      },
      checkout_options: {
        allow_tipping: false,
        redirect_url: "https://garmin-ai-export.vercel.app?paid=true",
      },
    }),
  },
);

Enter fullscreen mode Exit fullscreen mode

Redirect the user to the returned payment_link.url, they pay (card or Apple Pay or Google Pay) on Square's hosted checkout, and they come back to your site with ?paid=true appended.

Soft gate, by design

I do not server-side verify that the user actually paid. If ?paid=true is in the URL, the download unlocks.

This is intentional:

  • I have no database and no user accounts, so there's no real way to verify (you could log purchases via webhook, but you'd need a way to identify users)
  • For a tool meant for individual personal use, someone bypassing it by typing the URL is fine
  • Philosophy: "If you feel like paying, please pay. If not, that's also fine."

About currency

Square only lets you receive revenue in your account's home country currency. My account is registered in Japan, so JPY only. When an overseas user pays via Apple Pay, Apple does the FX conversion on their side, and the merchant always sees JPY in the books. So an English UI with JPY pricing is perfectly workable for international users.

Deploy and cost

Moving from Vercel Hobby to Pro immediately

The Vercel Hobby plan is strictly "personal, non-commercial use." Running a paid site on Hobby violates the ToS and risks account termination.

→ I upgraded to Pro ($20/month) right after wiring up payments.

Bandwidth is a non-issue

I worried at first: "These Garmin ZIPs can be hundreds of MB — am I going to blow through bandwidth?" But once I thought about it:

  • Uploading the Garmin ZIP → handled in-browser (never touches Vercel)
  • Downloading the converted ZIP → from browser to local disk (never touches Vercel)
  • Payment screen → on Square's domain (never touches Vercel)

Vercel only serves the initial page load (~2MB) and the Square API call (~1KB). Pro's 1TB allotment covers ~500,000 users. The browser-only architecture really pays off here.

Google Analytics and a favicon

Last touches: GA and a custom favicon.

GA4 via next/script with afterInteractive:

{SHOULD_LOAD_GOOGLE_ANALYTICS ? (
  <>
    <Script
      src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
      strategy="afterInteractive"
    />
    <Script id="google-analytics" strategy="afterInteractive">
      {`window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', '${GA_MEASUREMENT_ID}');`}
    </Script>
  </>
) : null}

Enter fullscreen mode Exit fullscreen mode

Favicons in Next.js App Router are convention-based — drop src/app/icon.tsx and src/app/apple-icon.tsx and they're auto-served at /icon and /apple-icon. You generate them dynamically from ImageResponse so no external image file required:

// src/app/app-icon-image.tsx
export function createAppIconResponse(size: { width: number; height: number }) {
  return new ImageResponse(
    (
      <div style={{ background: "#104c3f", borderRadius: "20%", /* ... */ }}>
        <svg viewBox="0 0 24 24" fill="none" stroke="white">
          {/* FileArchive icon SVG paths */}
        </svg>
      </div>
    ),
    size,
  );
}

Enter fullscreen mode Exit fullscreen mode

Reflections on building with Claude Code + Codex

What worked well

  • The role-split is fast. Claude Code focusing on requirement-shaping and review while Codex steadily implements is a great fit for "one human plus a fleet of AIs."
  • Mixed-model review actually catches things. When the same model writes and reviews, it misses stuff. A different model has no qualms about saying "wait, this is wrong."
  • When I got stuck, Claude Code could surface fixes like "iOS Safari has a known IndexedDB Blob bug, switch to ArrayBuffer storage" without me prompting it.
  • Codex is callable from CLI, so I can shoot off "implement this issue and open a PR" via Bash in one shot.

What I made sure to do

  • Never let "I'd kinda want this" turn into "started implementing." I wrote an explicit rule in CLAUDE.md: implementation only starts on "implement #X."
  • Reviews always go to a different model (Claude Code) so the implementer (Codex) doesn't grade its own homework.
  • I don't say "merge it" — I say "ship it to main" / "release to prod" so deployments are an explicit step.

The "I'll just whip up a thing" mode is incredibly powerful. The flip side is that without checks, it'll happily over-build, so the review-and-approval ritual is worth keeping.

Wrap-up

  • Half a day → a working web app with payments, analytics, and a custom favicon
  • Browser-only architecture wins on both server cost and privacy
  • Claude Code (PM/reviewer) + Codex (implementer) as a two-agent setup gives you both speed and quality

If you're a Garmin user curious to throw your own activity data at ChatGPT, Gemini, or Claude, give it a try.

🔗 https://garmin-ai-export.vercel.app