<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>by-cfj - blog</title><link>https://blog.by-cfj.com/</link><description>Writing by Cedric F. Jacob.</description><language>en</language><atom:link href="https://blog.by-cfj.com/rss.xml" rel="self" type="application/rss+xml"/>
<item><title>Leaving WHOOP without leaving my history behind</title><link>https://blog.by-cfj.com/p/leaving-whoop-without-leaving-my-history-behind</link><guid isPermaLink="true">https://blog.by-cfj.com/p/leaving-whoop-without-leaving-my-history-behind</guid><pubDate>Fri, 11 Sep 2026 07:08:36 GMT</pubDate><description>I exported nearly seven years of WHOOP data, 1,847 workouts and 2,368 sleeps, and built a simple importer to move that history into Google Health using the new Google Health API. Here’s how the WHOOP export works, what Google Health can and cannot import, and how to bring your workout and sleep hist</description><category>whoop</category><category>google health</category><category>fitbit</category><category>boxing</category><content:encoded><![CDATA[<p>My WHOOP export starts on 12 November 2019. The first row is a boxing session: 30 minutes, strain 12.6, average heart rate 155. The last row is a functional fitness session from this week. In between are 1,847 workouts, 2,368 sleeps and a little over 7,600 answered journal questions. I mean... I did not use the whoop all that time, but most of it.</p><pre><code>2019-11-12 00:00:00,2019-11-12 23:22:02,UTC+01:00,2019-11-12 17:04:19,2019-11-12 17:34:50,30,Boxing,12.6,421.0,175,155,0,3,9,86,1,false</code></pre><p>That is almost seven years of training. With some longer Whoop breaks in between. When I decided to move my tracking over to Google Health, I had one condition: that history comes with me.</p><p>It turned out to be way less simple than it sounds.</p><h2>The export is the easy part</h2><p>WHOOP gives you your data without a fight. You request an export, you get a ZIP, and inside are four CSV files:</p><p>File What's in it <code>workouts.csv</code> Every activity: start, end, type, strain, calories, average and max heart rate, HR zones <code>sleeps.csv</code> Every sleep and nap: onset, wake, and minutes of light, deep, REM and awake time <code>physiological_cycles.csv</code> Daily recovery, resting heart rate, HRV, SpO₂, skin temperature <code>journal_entries.csv</code> The daily yes/no journal</p><figure><img src="/media/bycfj/blog/5432c6ee-4003-48ab-b429-d640efa8ff49-Screenshot_2026-09-11_085725.png" alt=""></figure><p>My mix is mostly functional fitness, boxing, plus a long tail of weightlifting, running, yoga, jiu jitsu and the odd trampoline session I'd forgotten about.</p><p>So I had the data. I just needed somewhere to put it.</p><h2>The import button that doesn't exist</h2><p>Google Health replaced the Fitbit app in May 2026 and is now Google's home for activity, sleep and health data. I assumed there would be an &quot;Import&quot; button somewhere. There isn't.</p><p>Crap!!!!</p><p>Google Health pulls third-party data in through connections: Health Connect on Android, Apple Health on iPhone, and direct integrations with some services. The only file-style import is for medical records. Google Takeout will happily give you your data out, but there's no way to put a file back in.</p><blockquote><p>WHOOP does sync to Google Health via Health Connect, but that only covers data going forward. My seven years of history had no way in.</p></blockquote><h2>The side door: the Google Health API</h2><p>Alongside the new app, Google released the Google Health API as the successor to the old Fitbit Web API. Most of it is about reading data, but a few data types also support writing. Two of them are exactly the ones I cared about:</p><ul><li><p><code>exercise</code>, for workout sessions</p></li><li><p><code>sleep</code>, for sleep sessions with stages</p></li></ul><p>That was enough to work with. I am dev! If I could turn WHOOP's CSV rows into the JSON the API expects and send them one by one, the history would land in Google Health like any other source.</p><p>So I built a small importer.</p><h2>What the importer does</h2><p>It's a single HTML file. You open it in a browser, drop in your WHOOP CSVs, and it does the rest. Nothing is uploaded anywhere except to Google's own API.</p><ol><li><p><strong>Load the export.</strong> It recognises each CSV by its header row.</p></li><li><p><strong>Build data points.</strong> Workouts become <code>exercise</code> entries with the correct type, duration, calories and average heart rate, and sleeps become <code>sleep</code> entries with a stage timeline.</p></li><li><p><strong>Sign in.</strong> Paste an access token (more on that below).</p></li><li><p><strong>Write.</strong> Start with a test run of two entries, check the app, then send the rest. It retries on rate limits, keeps a log, and can resume if a run is interrupted.</p></li><li><p><strong>Undo.</strong> If something looks wrong, feed it the log and it deletes everything it created.</p></li></ol><blockquote><p>The WHOOP strain goes into the workout name, so &quot;Boxing (WHOOP strain 12.6)&quot; is still visible in Google Health. </p></blockquote><p>WHOOP also stores local times plus a timezone offset, so the importer converts everything to proper UTC. Sessions from a US trip don't end up shifted by six hours.</p><p><strong>Want to use it for your own WHOOP data? Get in touch and I'll send you the importer.</strong></p><h2>Getting a token without building an app</h2><p>This is the part I expected to be painful. Normally, talking to a Google API means creating a Cloud project, an OAuth client, a consent screen, test users and so on.</p><p>In the end I didn't need any of that. Google's <strong>OAuth 2.0 Playground</strong> was enough. These are the two scopes you need, as full URLs separated by a space:</p><pre><code>https://www.googleapis.com/auth/googlehealth.activity_and_fitness.writeonly https://www.googleapis.com/auth/googlehealth.sleep.writeonly
</code></pre><ol><li><p>Open <code>developers.google.com/oauthplayground</code>.</p></li><li><p>Paste the scopes above into &quot;Input your own scopes&quot;.</p></li><li><p>Click <strong>Authorize APIs</strong> and sign in with the Google account you use for Google Health.</p></li><li><p>Click <strong>Exchange authorization code for tokens</strong> and copy the access token (it starts with <code>ya29.</code>).</p></li><li><p>Paste it into the importer.</p></li></ol><p>The token lasts about an hour. If it expires mid-run, click &quot;Refresh access token&quot; in the Playground, paste the new token, and hit Write again. Everything already imported is skipped.</p><p>My first attempt failed with <code>Error 400: invalid_scope</code>. If you see that, check that you pasted the full <code>https://…</code> URLs. If it still fails, click the gear icon in the Playground, tick &quot;Use your own OAuth credentials&quot;, and use a client from your own Google Cloud project with the Google Health API enabled.</p><h2>What doesn't come across</h2><p>I want to be upfront about the limits. It is far from perfect</p><ul><li><p><strong>Recovery, HRV, resting heart rate, SpO₂, skin temperature.</strong> The Google Health API can't write any of these. They live only in WHOOP's cycles file. Damn!</p></li><li><p><strong>Recovery and strain as scores.</strong> These are WHOOP's own metrics and have no equivalent in Google Health. Strain survives only as text in the workout name. Damn again!</p></li><li><p><strong>The journal.</strong> There's nowhere to put it. I do not care much.</p></li><li><p><strong>Sleep stage order.</strong> WHOOP's export has minutes per stage, not a minute-by-minute hypnogram. The importer builds a realistic timeline in roughly 90-minute cycles, with deep sleep early and REM late, so the totals per stage match WHOOP exactly. The exact order within a night is reconstructed, not measured. Nice!</p></li><li><p><strong>Some activity types.</strong> Google's docs only confirm a handful of exercise types. The importer tries specific ones like <code>BOXING</code> first and falls back to a generic workout if the API rejects them. Either way, the name is kept. I am sure this can be done way better of course.</p></li><li><p><strong>Daily totals.</strong> Workout calories are stored on the session but don't feed into Google Health's daily calorie totals. That would need minute-level data the export doesn't contain. That's OK.</p></li></ul><p>For me, workouts and sleep were what mattered. Seven years of sessions and nights is the part I'd actually miss.</p><h2>If you want to do the same</h2><ol><li><p>Request your export from WHOOP and unzip it.</p></li><li><p>Get in touch with me for the importer, then open it in a browser. (Just send me an email, I am happy to share)</p></li><li><p>Load the CSVs and set the <strong>&quot;Up to&quot; date</strong> to the day before WHOOP started syncing to Google Health. Otherwise the recent weeks end up in there twice.</p></li><li><p>Get a token from the OAuth Playground.</p></li><li><p>Run with a limit of 2, check the entries in the app, then set the limit to 0 and let it run.</p></li><li><p>Download the import log when it's done. It's your undo button.</p></li></ol><h2>Why this matters to me</h2><p>Training data is a strange kind of personal record. On any given day it's just a number. Over years it's the only honest diary I keep: the months where boxing took over, the stretches where it was all functional fitness, the weeks where sleep fell apart. </p><p>Switching devices shouldn't mean starting that diary from page one. WHOOP deserves credit for making the export easy. Getting the data back into another platform was the hard part, and now it doesn't have to be.</p><blockquote><p>But hey: I still got the full data, maybe one day I can import every part of it!</p></blockquote><p>If you want the importer for your own move, or you're stuck on the same problem with a different tracker, get in touch. I'm happy to share the code and help you get it running.</p><figure><img src="/media/bycfj/blog/c72a502b-3d2d-48ee-bccf-acd723770536-538cec47-a4e5-42f5-a037-fb81efe491cc.png" alt=""></figure>]]></content:encoded></item>
<item><title>Introducing: FIGHTCAL.by-cfj.com</title><link>https://blog.by-cfj.com/p/introducing-fightcal-by-cfj-com</link><guid isPermaLink="true">https://blog.by-cfj.com/p/introducing-fightcal-by-cfj-com</guid><pubDate>Fri, 05 Jun 2026 21:04:53 GMT</pubDate><description>The best fight calendar for fighting sports fans that will transform how you enjoy fight content in the future.</description><category>human-writer</category><category>project</category><category>fighting</category><category>fightcal</category><category>sports</category><category>boxing</category><category>mma</category><content:encoded><![CDATA[<p>If you are a fan of the fighting arts, you will have felt the pain of trying to find the event that you're looking for. Even if you found it, at what time at your particular location will it be aired? </p><p>Was it 2 a.m. in your country or was it 2 a.m. at the event location? </p><p>When is the ring walk actually going to happen? </p><p>Can I watch it on free TV or live stream it on some random new website that I never heard of? Is it on DAZN and you have to pay extra? </p><blockquote><p>&quot;I feel your pain!&quot;</p></blockquote><p>And until today I never really thought about fixing it myself. But why not? I have been feeling the exact pain since I started watching events live on TV. </p><p>And I always wished for one place to get all my information and not have to window shop for new websites, find one that I like, then find out that an event hasn't been added to it. </p><h2>The solution</h2><p>Sometimes the small jump from A to B is difficult even though it totally makes sense. You have the skill, you understand the problem, you are passionate about it. </p><p>Well without talking about it any longer, this is what I want to do. </p><p>The world's best calendar for fighting events worldwide.</p><p>Driven by a smart engine in the back and administrated by a fight fan.</p><p>I mean, it's not an easy undertaking. </p><p>The data is scattered, unreliable and hard to come by.</p><p>May I present what will be, in my humble opinion, one of the coolest things ever :)</p><h2>What you will get</h2><ul><li><p>One calendar for all of combat sports. Boxing, MMA, BJJ, judo, kickboxing, and thai boxing events, all in a single place.</p></li><li><p>Filter to exactly what you want. Narrow by sport, tier (major / pro / club), promotion (UFC, Matchroom, ONE, GLORY, and more), region, date range, gender, and title-fights-only.</p></li><li><p>Search instantly. Find fighters, events, or cities with free-text search.</p></li><li><p>Two ways to view. A chronological agenda grouped by date, or a month grid to see the whole month at a glance.</p></li><li><p>Shareable filtered links. Every filter is baked into the URL, so a view like &quot;big boxing title fights in Europe&quot; is just a link you can copy and send to a mate.</p></li><li><p>Countdown to the next big card. A live timer ticking down to the next major event.</p></li><li><p>Full fight details. Tap any event for the main event, full undercard, date, time, venue, location, broadcaster, and region.</p></li><li><p>Add to your calendar. One-click .ics download or add straight to Google Calendar.</p></li><li><p>Save your fights. Bookmark cards and sync them across devices.</p></li><li><p>Works great on mobile. Responsive and touch-friendly.</p></li></ul><p>But for now just a promise</p><blockquote><p>&quot;I will put my heartblood into this project to make it the best it can be!&quot;</p></blockquote><p>Heartblood? The Germans will know. It basically means I'll put my heart and soul into this.</p><p>So follow along, this is growing quickly.</p><p><a href="http://FIGHTCAL.by-cfj.com">FIGHTCAL.by-cfj.com</a></p><p>Sincerely, </p><p>Cedric F. Jacob</p><p><em>(a fight fan)</em></p><figure><img src="/media/bycfj/blog/f58ff062-ba66-4078-92a9-9af2792bad6c-file_00000000c46c7243822d4a97beebb86a.png" alt="Cedric and his boxing coach"><figcaption>Me and my boxing coach</figcaption></figure>]]></content:encoded></item>
<item><title>Hello World</title><link>https://blog.by-cfj.com/p/hello-world</link><guid isPermaLink="true">https://blog.by-cfj.com/p/hello-world</guid><pubDate>Wed, 03 Jun 2026 12:00:00 GMT</pubDate><description>Welcome to my blog. You do not have to read this one. It makes no sense. It's just a test to be honest. I wanted to see what the different formatting options look like.</description><category>hello-world</category><content:encoded><![CDATA[<h2>Welcome</h2><p>Okay wow, here we go. I never thought I would create another blog in my life after feeling overwhelmed by the huge amount of AI-generated content on LinkedIn and other web pages.</p><p>To be honest: I hate it.</p><p>I don't want to read it.</p><p>Have I created some myself?</p><p>Yes, and it felt awful.</p><h2>The Dilemma</h2><p>Will I create written AI content in the future? Yes and no.</p><p>I think AI can be great to draft a quick email or to summarize something, but when it comes to writing and trying to bring a point across, I feel the human element should remain.</p><p>I love reading well-written content. I'm a sucker for good lyrics, for good stories, for good books. When I was younger I read a lot of books about programming. If someone found their own way of teaching me a subject, it really filled me with joy and made me happy and made me a way better student.</p><p>And that's why I start by promising: </p><blockquote><p>&quot;I will not write these blog posts with AI.&quot;</p></blockquote><p>And yes, they might be clunky at times. I might ramble at times. So much that AI would want to replace it with 20 emojis instead.</p><h2>Fuck that 🙂</h2><p>Here is a numbered list instead:</p><ol><li><p>Just to check how my numbered lists render.</p></li><li><p>To show off my design. I like it.</p></li><li><p>Because 3 rows look better than 2.</p><p></p></li></ol><p>I love it! But what about bullet lists? They need to look cool too.</p><ul><li><p>Another list to test</p></li><li><p>Brilliant as well</p></li><li><p>I can get used to this</p></li></ul><p>And a photo?</p><figure><img src="/media/bycfj/blog/7203576b-c452-46f0-be74-f1bae7a64f8f-best.png" alt="best.png"><figcaption>Me and my book</figcaption></figure><p>How about a couple of them?</p><div data-carousel=""><figure><img src="/media/bycfj/blog/e03d1d70-e02a-4b57-9f6b-984425873c0a-244627610_568974701044017_8364580276557254202_n.jpg" alt="244627610_568974701044017_8364580276557254202_n.jpg"></figure><figure><img src="/media/bycfj/blog/76cc26c0-5c9c-45ce-b945-a0cf0dac8a39-168829_f670bc1faae45932bb2a546155a48833.jpeg" alt="168829_f670bc1faae45932bb2a546155a48833.jpeg"></figure><figure><img src="/media/bycfj/blog/7f0119aa-6350-4420-be95-ae97d37aea8b-test.jpg" alt="test.jpg"></figure><figure><img src="/media/bycfj/blog/63bf1d95-db2c-42d5-8dad-05acee1df028-WIN_20241002_15_48_30_Pro.jpg" alt="WIN_20241002_15_48_30_Pro.jpg"></figure><figure><img src="/media/bycfj/blog/2d51c348-93e5-44a7-9ce4-150da7d1cbcb-WIN_20240924_10_37_21_Pro.jpg" alt="WIN_20240924_10_37_21_Pro.jpg"></figure></div><p>WOW!</p><p>And some code?</p><pre><code class="language-javascript">console.log(&quot;Hello world&quot;);</code></pre><p>Now that is a blog post.</p><p>Thanks for reading.</p>]]></content:encoded></item>
</channel></rss>
