<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Meir Vaknin </title>
    <description>The latest articles on DEV Community by Meir Vaknin  (@meirvaknin).</description>
    <link>https://dev.to/meirvaknin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4047588%2F9d94b0f7-e37f-45ae-9c57-3ea23852afb9.jpeg</url>
      <title>DEV Community: Meir Vaknin </title>
      <link>https://dev.to/meirvaknin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/meirvaknin"/>
    <language>en</language>
    <item>
      <title>How I Made Focusity Easier to Use: A Smarter Connect Screen and One Moodle URL</title>
      <dc:creator>Meir Vaknin </dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:47:22 +0000</pubDate>
      <link>https://dev.to/meirvaknin/how-i-made-focusity-easier-to-use-a-smarter-connect-screen-and-one-moodle-url-1jfk</link>
      <guid>https://dev.to/meirvaknin/how-i-made-focusity-easier-to-use-a-smarter-connect-screen-and-one-moodle-url-1jfk</guid>
      <description>&lt;p&gt;I build &lt;a href="https://www.focusity.co.il" rel="noopener noreferrer"&gt;Focusity&lt;/a&gt;, a Hebrew study planner. It takes a student's exams and assignment deadlines and builds an actual study schedule around them: when to start preparing, how to split the material, what to do tonight.&lt;/p&gt;

&lt;p&gt;All of that depends on knowing the deadlines, and getting them in was the worst part of the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow I wanted to delete
&lt;/h2&gt;

&lt;p&gt;Open Moodle, find the calendar, click export, download an &lt;code&gt;.ics&lt;/code&gt; file, dig it out of your phone's Files app, upload it to Focusity. Then a lecturer moves an exam by a week and you do the whole thing again.&lt;/p&gt;

&lt;p&gt;Nobody repeats that. And a planner running on stale deadlines is worse than no planner, because it's confidently wrong about the one thing you're trusting it for.&lt;/p&gt;

&lt;p&gt;Moodle does offer a better door. Every user has a private calendar URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://moodle.college.ac.il/calendar/export_execute.php
  ?userid=45146&amp;amp;authtoken=&amp;lt;40 hex chars&amp;gt;&amp;amp;preset_what=all&amp;amp;preset_time=custom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste it once. Something re-reads it forever. The student never touches a file again.&lt;/p&gt;

&lt;p&gt;That's the whole feature. Most of the work turned out to sit in two places I hadn't planned for: the paste surviving a real phone, and the fetch reaching a college that didn't want to hear from my server.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this actually gets built
&lt;/h2&gt;

&lt;p&gt;I don't type most of my code. I build Focusity with Claude Code: I decide what gets built and what shape it takes, the agent writes it, and I read the diff. The commits sit in my git history under my name with a &lt;code&gt;Co-Authored-By: Claude Opus 5&lt;/code&gt; trailer, because that's what happened.&lt;/p&gt;

&lt;p&gt;That matters for what follows. On the connect screen the agent caught defects I would have shipped without noticing. On the network bug it was no help at all, and why it was no help is most of what I learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The screen where you paste it
&lt;/h2&gt;

&lt;p&gt;The feature is one dialog, and close to everyone who opens it is on a phone. That's where most of the work went, and almost none of it was the parser.&lt;/p&gt;

&lt;p&gt;The first version had a picker listing the institutions we support. If your college wasn't on the list, there was nothing useful to click. The escape hatch, "my institution isn't listed", was an inline link inside a &lt;code&gt;text-xs&lt;/code&gt; paragraph, roughly 16 pixels tall, sitting next to a second one for importing a file manually. Those two links were the entire path forward for exactly the students the feature was built for, at a size you cannot reliably hit with a thumb. Both are now full-width rows with a 44px minimum height, and the picker went from &lt;code&gt;h-10&lt;/code&gt; to &lt;code&gt;h-11&lt;/code&gt; for the same reason.&lt;/p&gt;

&lt;p&gt;The picker also used to preselect the single institution we supported. On a phone that reads as a form already filled in with a college that isn't yours, so it stopped doing that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffexpt1c68uatshtqw6ue.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffexpt1c68uatshtqw6ue.PNG" alt="The Focusity Moodle connect dialog in Hebrew, before an institution is chosen. The picker reads "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The two rows at the bottom used to be inline links about 16 pixels tall.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Neither the picker nor the paste field had a real label. The picker leaned on a placeholder option and the paste field on a loose &lt;code&gt;span&lt;/code&gt; next to it, which looks fine and tells a screen reader nothing. Both use &lt;code&gt;label&lt;/code&gt; and &lt;code&gt;htmlFor&lt;/code&gt; now. The two failure banners were visual only, so they carry &lt;code&gt;role="alert"&lt;/code&gt;, and the decorative icons inside them are hidden from assistive tech. A lock emoji that was doing structural work in the connected panel became a real icon like every other icon in the dialog.&lt;/p&gt;

&lt;p&gt;The paste field is &lt;code&gt;type="url"&lt;/code&gt; with &lt;code&gt;autocorrect&lt;/code&gt;, &lt;code&gt;autocapitalize&lt;/code&gt; and &lt;code&gt;spellcheck&lt;/code&gt; off. A mobile keyboard will cheerfully capitalize the first letter of a URL you just pasted, and then the connection fails for a reason the student has no way to see.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0p38oih9pbm19w56yggk.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0p38oih9pbm19w56yggk.PNG" alt="The same dialog after choosing "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The escape hatch, opened. The grey note under step one says any Moodle works, not only the ones in the list.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;None of it is clever work, but it's the difference between a feature that works and a feature that works for someone who isn't me.&lt;/p&gt;

&lt;p&gt;I want to be fair about where the agent helped here. I asked for a review against a UI and accessibility rule set, got five defects back with the tier they fell in, and fixed them in one pass. Left to myself I would have shipped the 16px links, because I'd tested the dialog on a laptop with a mouse, where they're perfectly clickable.&lt;/p&gt;

&lt;p&gt;That's the same mistake I'm about to describe making with the server. I got caught for it twice in one day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the request comes from
&lt;/h2&gt;

&lt;p&gt;The original design was the obvious one. A table holds one URL per student. A scheduled function fetches the &lt;code&gt;.ics&lt;/code&gt; every night, parses it, and updates tasks.&lt;/p&gt;

&lt;p&gt;It keys on the calendar event's &lt;code&gt;UID&lt;/code&gt;, so a moved deadline updates the existing task instead of creating a second one, and it only ever touches the name and the date. If a student marked something done, a nightly sync must not undo that.&lt;/p&gt;

&lt;p&gt;I tested the pieces. I pulled the real feed with &lt;code&gt;curl&lt;/code&gt; and got a valid calendar back. I ran the parser against that real payload and it produced the right course name and the right timestamp out of UTC. Two test harnesses, 23 assertions between them, covering the parser and the URL validator: plain &lt;code&gt;http&lt;/code&gt; rejected, hosts off the allowlist rejected, the cloud metadata address &lt;code&gt;169.254.169.254&lt;/code&gt; rejected, lookalike domains rejected, paths that weren't the export endpoint rejected.&lt;/p&gt;

&lt;p&gt;Then I deployed it, connected my own account, and got &lt;code&gt;last_sync_error: moodle_http_403&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The URL in the database was byte for byte what I had tested by hand, so the request itself had to be different. First guess was &lt;code&gt;User-Agent&lt;/code&gt;, because college firewalls love rejecting anything that doesn't look like a browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl default UA                  -&amp;gt; 200
no UA at all                     -&amp;gt; 200
Deno-like UA                     -&amp;gt; 200
browser UA                       -&amp;gt; 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same token, same path, same headers. The only variable left was where the request came from, so I had the database make the call and also asked it where it thought it was standing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt;
  &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://moodle.college.ac.il/calendar/export_execute.php?...'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://ipinfo.io/json'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Forbidden&amp;lt;/title&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Transaction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="err"&gt;d&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="mi"&gt;891e526&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"18.139.36.225"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Singapore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Amazon.com, Inc."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 403 page with a "Transaction ID" is a firewall, not Moodle. My Supabase project runs in Singapore. The college is in Israel, and it doesn't take visitors from foreign data centers.&lt;/p&gt;

&lt;p&gt;Nothing was wrong with my code. My code was standing in the wrong country.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving the fetch to the phone
&lt;/h2&gt;

&lt;p&gt;I looked for a server-side answer first, because that's where everything already was. Ask the college to allow our address? Cloud IPs aren't stable or contractual, so that arrangement rots the first time our provider reshuffles and we find out when students complain. Move the server closer? The rule isn't "not Singapore", it's closer to "not a hosting provider", so Frankfurt would very likely get the same treatment, and you can't relocate an existing project anyway. Run a proxy inside the country? Then I'm paying for infrastructure per country, forever, so that one feature works.&lt;/p&gt;

&lt;p&gt;Meanwhile my own phone, on cellular, loaded the export page without complaint. Students are exactly who that firewall was built to let in.&lt;/p&gt;

&lt;p&gt;So the fetch moved to the phone, and there's one catch worth knowing if you try this. Focusity's mobile app is a WebView, and fetching the file from the JavaScript already running in there doesn't work, because the Moodle export sends no CORS headers whatsoever:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-sSI&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Origin: https://app.example.com"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; access-control
&lt;span class="o"&gt;(&lt;/span&gt;nothing&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; means a browser context won't hand the response to your script. But a &lt;strong&gt;native&lt;/strong&gt; fetch isn't a browser context, and CORS doesn't apply to it. The React Native layer can make the request the WebView can't.&lt;/p&gt;

&lt;p&gt;The app already had a bridge for this shape of problem, since native Google sign-in posts a message in and gets an answer injected back, so this became one more message type. Which route runs is decided when the screen opens: device if the app is new enough to answer, server otherwise. An older build simply never replies, times out after four seconds, and falls back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I didn't plan
&lt;/h2&gt;

&lt;p&gt;For a blocked college the server can never use that URL. So why was I storing it there?&lt;/p&gt;

&lt;p&gt;On the device route I moved it to the phone's keychain. Written by the device, read by the device, never transmitted to my infrastructure at all. The server route still exists for colleges that don't block us, where an overnight sync beats one that only runs when the app opens. But this was built for the students behind those firewalls, and for them my database no longer holds a live credential to a college account, so it can't leak one.&lt;/p&gt;

&lt;p&gt;The host allowlist went the same way. It existed because &lt;em&gt;my server&lt;/em&gt; made the request, and an unvalidated host there is a request-forgery primitive. On the device path the phone makes the request, so there's no infrastructure of mine in the path to abuse. The list was buying no security there and costing real students access, so the device path dropped it and kept every structural check. Which is also why the picker can now grow from evidence: a successful connection reports its hostname back, and an institution joins the list because someone actually connected from it.&lt;/p&gt;

&lt;p&gt;That's a better design than the one I set out to build, and I didn't reason my way there. A firewall said no, and only then did the stronger option become obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;The architecture wasn't the mistake. The order of my tests was.&lt;/p&gt;

&lt;p&gt;I verified the URL, the token, the parser, the validator, and the dedup logic. I tested the network path from my laptop, which sits on the same kind of connection my users have, and never once from the environment the code would actually run in. That check was a single query away the whole time, and I only ran it after production made me. It's the same shape as the 16px tap targets. Both were invisible from a laptop, because neither was really about the code.&lt;/p&gt;

&lt;p&gt;Working with an agent made that easier to fall into. Claude Code will write every test you can describe, and more of them than I would have bothered to write by hand, which is exactly what made me feel covered. But the tests all pointed at the code, because the code is what's in the diff. The Singapore address isn't in the diff. Nothing about the files you're both looking at says &lt;em&gt;this will run from a data center on another continent, at an address someone else's firewall has an opinion about&lt;/em&gt;. Deciding that the environment belongs in the test plan isn't a coding task, and nobody was going to do it for me.&lt;/p&gt;

&lt;p&gt;So here's the rule I'm keeping: when your code will run somewhere other than your machine, test from that somewhere first. Your laptop is not a staging environment, and the browser on it is not a phone. Each one has a reputation with other people's systems that you never get to see.&lt;/p&gt;

&lt;p&gt;I study industrial engineering and management, where a lot of the degree comes down to why systems break at the handoffs, in the places where one component quietly assumes something about another that isn't true. Then I built something that broke at the interface between my server and someone else's firewall, and at the interface between my mouse and somebody's thumb. The theory was fine. I just didn't apply it to my own work until it broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a student gets out of it
&lt;/h2&gt;

&lt;p&gt;You paste one link, once, at the start of the semester. After that, when a lecturer pushes an exam back two weeks, your plan moves with it. You don't re-export anything, and you don't find out in week eleven that you've been preparing against a date that changed in week six.&lt;/p&gt;

&lt;p&gt;If your college is one of the ones that blocks us, your login credential also never reaches my servers. It sits on your phone. If you're going to trust a small app with something attached to your degree, that's the shape the trust should have. It happened because a firewall forced my hand, which I'd rather say plainly than dress up as foresight.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a student, and Focusity is what I build when I should probably be studying. The device-side sync is written, tested on a real phone on cellular, and sitting in a TestFlight build. It reaches the App Store when the next release does.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>a11y</category>
      <category>reactnative</category>
      <category>ai</category>
    </item>
    <item>
      <title>Todo Lists Organize Information. I Built an App That Organizes Time.</title>
      <dc:creator>Meir Vaknin </dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:54:45 +0000</pubDate>
      <link>https://dev.to/meirvaknin/todo-lists-organize-information-i-built-an-app-that-organizes-time-3803</link>
      <guid>https://dev.to/meirvaknin/todo-lists-organize-information-i-built-an-app-that-organizes-time-3803</guid>
      <description>&lt;h2&gt;
  
  
  It started in a cafeteria
&lt;/h2&gt;

&lt;p&gt;Three of us were sitting in the college cafeteria, complaining about the same thing.&lt;/p&gt;

&lt;p&gt;And it wasn't that we couldn't find our information. Every college has an app now. You log in and there it is: your timetable, your exam dates, your grades, the assignment deadlines out of Moodle. All of it, in one place, up to date.&lt;/p&gt;

&lt;p&gt;All three of us were still constantly behind.&lt;/p&gt;

&lt;p&gt;Because that app tells you an assignment is due on the 14th. It doesn't tell you that Sunday evening is free and that's when you should be writing it. It reports. It doesn't plan. And underneath the whole semester sits the constant low-grade certainty that something is slipping, and you don't know what yet.&lt;/p&gt;

&lt;p&gt;We weren't short on information, and we all had todo apps on top of it. The problem was a different one entirely: not one of us could answer this question.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What should I be working on right now, and when do I actually have time for the rest of it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question became Focusity.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrvxit246fkchbc7mkgn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrvxit246fkchbc7mkgn.png" alt="Illustration of a student at a laptop surrounded by calendar, notes and book icons"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The pitch, in Hebrew: "Your studies, finally organized."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The distinction that shaped everything
&lt;/h2&gt;

&lt;p&gt;It would have been very easy to build another task manager. Courses, assignments, exams, a nice calendar, done.&lt;/p&gt;

&lt;p&gt;But as I said, the information was never the missing piece. The college app already had it, and had it accurately. Even with everything visible in one place, I still didn't know what to do with Tuesday afternoon.&lt;/p&gt;

&lt;p&gt;So I drew a line early and never crossed it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A todo list organizes &lt;strong&gt;information&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Focusity organizes &lt;strong&gt;time&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything downstream came from that. If the product's job is to decide &lt;em&gt;when&lt;/em&gt;, then the calendar stops being a display surface and becomes the output. The scheduling engine stops being a feature and becomes the product. And a task the user has to drag onto a free slot by hand is a failure.&lt;/p&gt;

&lt;p&gt;That one sentence saved me from about ten features I would otherwise have built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MVP call: starting on a platform I knew I'd leave
&lt;/h2&gt;

&lt;p&gt;I started Focusity on &lt;a href="https://base44.com" rel="noopener noreferrer"&gt;Base44&lt;/a&gt;, an app-building platform.&lt;/p&gt;

&lt;p&gt;I didn't know yet whether the core idea worked, or whether students would even bother importing their schedule. Spending three weeks on auth, database schemas and deploy pipelines before validating any of that would have been optimizing the wrong thing.&lt;/p&gt;

&lt;p&gt;What I bought: a working product in days instead of weeks, and fast iteration on flows while the product definition was still moving. What I paid: a ceiling I couldn't see yet.&lt;/p&gt;

&lt;p&gt;I'll come back to what happened when I hit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing before building
&lt;/h2&gt;

&lt;p&gt;Almost every screen in the app has a Figma design behind it. That wasn't process worship, it was arithmetic. Redrawing a screen in Figma costs minutes. Rebuilding it in React costs hours and leaves scar tissue in the codebase. When the UI is dense with information, like a week grid or a dashboard that has to answer "what now?" at a glance, getting the hierarchy wrong is expensive.&lt;/p&gt;

&lt;p&gt;There's a branch in my git history called &lt;code&gt;feature/match-figma-pages&lt;/code&gt;, and a commit titled &lt;em&gt;"checkpoint before Figma-based calendar/menu QA pass"&lt;/em&gt;. When the code and the design drifted apart, the code is what moved. That's the difference between having designs and treating the design as the spec.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1bnxj6jw1qvvi0fc25z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1bnxj6jw1qvvi0fc25z.png" alt="Feature board showing smart timetable, tasks, exam reminders, grade tracking and automatic daily planning"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The feature set, framed around student moments rather than modules.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The four layers
&lt;/h2&gt;

&lt;p&gt;Focusity is four layers stacked, and almost every feature belongs to exactly one of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   INTAKE                    MODEL                PLANNING              FOLLOW-UP
   ──────                    ─────                ────────              ─────────
   Moodle ICS  ─┐
   CSV / Excel ─┤                              ┌─ auto-scheduler ─┐
   Exam PDF    ─┼──▶  courses · tasks   ──────▶├─ exam prep       ├──▶  analytics
   Grade PDF   ─┤      exams · time blocks     └─ catch-up board ─┘     grades
   Schedule PDF─┤      grades                                            reminders
   Google Cal  ─┘
                        ▲                                                    │
                        └──────────── velocity feedback ─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Intake&lt;/strong&gt; is where most study planners quietly die. Nobody hand-types 22 courses and a semester of class times. So Focusity imports: an ICS feed from Moodle, a CSV or Excel timetable, a PDF exam schedule. Google Calendar connects with two-way sync, so commitments that already live there show up in the plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model&lt;/strong&gt; is one normalized set of entities, so everything downstream reasons over the same data regardless of where it came from. &lt;strong&gt;Planning&lt;/strong&gt; is the engine, covered in the next section. &lt;strong&gt;Follow-up&lt;/strong&gt; closes the loop: what got done, what slipped, how accurate the estimates were.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokvvypvv2ntqgibi1nmu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fokvvypvv2ntqgibi1nmu.png" alt="The Courses screen listing import options: Moodle ICS, CSV/Excel timetable and exam PDF"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Import paths: Moodle ICS, CSV/Excel, exam PDF. Everything routes through a review screen first.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The engine
&lt;/h2&gt;

&lt;p&gt;Given tasks with deadlines and a calendar that's already partly full, place study time. Sounds simple. It isn't, because free time is only the beginning of the constraints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hard vs. soft constraints.&lt;/strong&gt; A class is immovable. So is an exam, and so is a work shift. The only blocks the scheduler may move are the task-study blocks it placed itself on a previous run. Everything else on the calendar, including its own exam-revision blocks, is busy time it plans around. This one rule is what makes re-running it safe: it can never quietly delete something you put there yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pomodoro splitting.&lt;/strong&gt; Work is carved into 50-minute focus blocks with 10-minute breaks. Pomodoro isn't magic, but "study for 4 hours" is a lie you tell yourself, and "four 50-minute blocks" is a thing that actually happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eat That Frog.&lt;/strong&gt; Demanding tasks (high importance, high difficulty, or high energy) claim the earliest slots inside your peak productivity window. You declare that window in settings: Morning, Afternoon, Evening or Night.&lt;/p&gt;

&lt;p&gt;That setting had a wrinkle worth mentioning. Originally the planner stopped at 22:00 for everyone, which made Night nearly identical to Evening: a dropdown option that changed nothing. So Night, and only Night, extends the planning window to midnight. If you explicitly told the app you work at night, it should believe you. If you didn't, it should never push a block to 23:30.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load balancing.&lt;/strong&gt; A task's blocks spread across the days remaining until its deadline instead of piling into one. A second pass lifts the per-day cap for whatever didn't fit, so nothing is silently dropped. It just lands less ideally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Personal velocity.&lt;/strong&gt; My favorite piece. Every session records planned duration and actual duration. After three completed sessions the app computes the ratio and calibrates future estimates, smoothed so one disastrous Tuesday doesn't reset the model.&lt;/p&gt;

&lt;p&gt;The effect: if you consistently need 90 minutes for what you estimate at 60, Focusity stops taking your estimates at face value and reserves what you actually need. You don't have to get better at estimating. The system gets better at estimating &lt;em&gt;you&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Priority itself is a plain, boring formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calcPriorityScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;courseBoostMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;difficulty_level&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imp&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;importance_level&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hoursLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;differenceInHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;urgency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;hoursLeft&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;boost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;course_id&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;courseBoostMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;course_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;urgency&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;boost&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;boost&lt;/code&gt; is where grades feed back in: a course whose current grade sits below a set threshold raises the priority of its tasks, scaled by how many credits it carries.&lt;/p&gt;

&lt;p&gt;Grades aren't a report card in Focusity. They're an input. The same data drives a "what if I retake this" projection that recomputes your credit-weighted average across up to seven courses at once. The question stops being &lt;em&gt;should I retake this course&lt;/em&gt; and becomes &lt;em&gt;which combination moves my average the most, for the least work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdhzqkwhmzv8hbnlesyof.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdhzqkwhmzv8hbnlesyof.png" alt="The dashboard showing a study streak, today's plan and the most urgent task"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Most urgent" surfaced up top, today's plan below, one button to reschedule everything.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0er9g1z8mcllw49talsy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0er9g1z8mcllw49talsy.png" alt="A week calendar grid with class and study blocks laid out across five days"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The plan is the calendar. There is no separate "plan" screen to go read.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the engine is not AI
&lt;/h2&gt;

&lt;p&gt;The obvious assumption in 2026 is that an LLM does the scheduling. It doesn't, and that's the most opinionated decision in the app.&lt;/p&gt;

&lt;p&gt;The scheduler is deterministic, transparent and fast. You can always see why a block landed where it did: deadline, difficulty, importance, your declared peak hours, the free time you actually had.&lt;/p&gt;

&lt;p&gt;An LLM would be worse here on every axis I care about. It's slower, and it's non-deterministic, so re-running the same plan gives a different answer for no reason you can explain. Most importantly, a student who doesn't trust the plan won't follow it, and "the AI decided" is not a reason anybody trusts.&lt;/p&gt;

&lt;p&gt;There's also a practical consequence. The planners are pure functions: same inputs, same output, no I/O, no hidden clock. Even "now" is passed in. That's what makes them testable. The exam planner has a standalone harness that hands it a synthetic calendar and asserts the plan that comes back, block by block. You cannot write that test against a language model.&lt;/p&gt;

&lt;p&gt;I did build a conversational planning agent at one point. It's still in the codebase, disconnected from navigation, and any message sent to it returns a maintenance notice. That's the honest state of that feature: it doesn't do what its interface promises, so it doesn't get to sit in the nav pretending otherwise. &lt;strong&gt;A feature that doesn't work is worse than a feature that doesn't exist&lt;/strong&gt;, because the first costs you trust and the second costs you nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust as a design constraint
&lt;/h2&gt;

&lt;p&gt;Once the product is "an engine makes decisions for you," trust stops being a nice-to-have and becomes the thing you're building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every import goes through a review screen.&lt;/strong&gt; ICS, CSV and PDF imports never write to your calendar directly. You see the parsed rows, fix what's wrong, approve. Parsers fail and university PDFs are chaotic. The answer is to never let a parser have the last word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The exam planner refuses to lie.&lt;/strong&gt; When you set up a preparation plan, it checks whether the work genuinely fits in the free time before the exam. If it doesn't, it says so and reports the shortfall in minutes. Silently truncating the plan would have been easy, and that's the version that gets you a bad grade and a user who never comes back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing missed disappears.&lt;/strong&gt; Everything you skipped lands on one catch-up board, each item paired with a concrete new slot the engine already found in your real calendar. One tap to reschedule. The failure mode I was designing against is the one I know personally: you fall behind, the backlog goes shapeless, and you stop opening the app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0hnv2hc4zw11hmil5ir6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0hnv2hc4zw11hmil5ir6.png" alt="The catch-up board listing missed items, each with a suggested new time slot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;13 missed items, each with a real free slot already found.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I hit the ceiling
&lt;/h2&gt;

&lt;p&gt;About four months in, Base44 stopped being an accelerant.&lt;/p&gt;

&lt;p&gt;The breaking points were data access and auth: I needed queries the platform's data layer didn't express, background jobs, and control over the authentication flow rather than the one I was handed. Every workaround was making the codebase worse in a way that would eventually cost more than a migration.&lt;/p&gt;

&lt;p&gt;So in a single day in July I moved everything: auth to Supabase, then a schema migration replacing the platform's entities with seven real Postgres tables, then every page and component off the platform's data layer. That schema has since grown to 24 tables across 31 migrations, plus Edge Functions and cron jobs that simply weren't possible before. The migration surfaced an auth-flow deadlock and a fistful of data-layer bugs the platform had been papering over. Painful week. Correct decision.&lt;/p&gt;

&lt;p&gt;The takeaway I'd defend in a room: &lt;strong&gt;a good MVP decision includes the condition under which you'll abandon it.&lt;/strong&gt; Base44 did exactly what I chose it for. Staying out of sunk cost would have been the actual mistake, and leaving on schedule was the plan working rather than failing.&lt;/p&gt;

&lt;p&gt;Today: React + Vite on the front end, Supabase for Postgres, auth, Edge Functions and cron, Vercel for hosting, a small FastAPI service on Render for PDF parsing, and an Expo shell wrapping the web app for the stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to sign into the same app
&lt;/h2&gt;

&lt;p&gt;Google and Apple sign-in is one line on a roadmap and three implementations in the code: one for the browser, one for the native shell, one for the WebView bridge. Inside a WebView you have no access to the Google session already signed in on the device, so instead of "continue as…" the user gets an empty login form every single time.&lt;/p&gt;

&lt;p&gt;And Sign in with Apple isn't a design choice at all: App Store guideline 4.8 requires it once you offer third-party login. Some of your architecture gets written by someone else's policy document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where reality pushed back
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rejected by the App Store.&lt;/strong&gt; Guideline 4.5.4: push notifications can't be on by default. Fair, and fixed by making them opt-in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hebrew comes out of PDFs backwards.&lt;/strong&gt; Right-to-left text arrives reversed from table extraction and has to be reconstructed before anything can be parsed. The service finds the header row by scoring rows against Hebrew keywords and maps columns dynamically. That's what lets one parser handle exports from different institutions without hardcoding column positions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A native rewrite that didn't ship.&lt;/strong&gt; I got three phases into a React Native port (scaffold, business logic, UI) before concluding the WebView shell would reach students months sooner at a fraction of the maintenance cost. I deleted the port. Shipping beat purity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;Focusity is &lt;a href="https://apps.apple.com/il/app/focusity/id6796308900?l=he" rel="noopener noreferrer"&gt;live on the App Store&lt;/a&gt;. The Android build is in testing with testers, not publicly available yet. It works in any browser, mobile-first, and it's free. There's no paid plan today and no card.&lt;/p&gt;

&lt;p&gt;It has real early users. Not many. Their feedback has been positive on the idea and the design, and I want to be precise about what that is and isn't: informal feedback from a small group, not user research, with no usage metrics behind it. What it did was confirm the problem is real and the direction is right. The next step is widening that circle and collecting feedback in a structured way, so prioritization runs on something better than my own judgment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnetxfzjqz4zmhn1tk3o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvnetxfzjqz4zmhn1tk3o.png" alt="The analytics screen showing a productivity score, weekly study hours and achievement badges"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A productivity score combining estimation accuracy, focus and consistency, along with XP, streaks and streak freezes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I actually worked
&lt;/h2&gt;

&lt;p&gt;Semester load meant Focusity moved slowly for months. Since finishing the semester, and while doing reserve duty a week on and a week off, it's had real focused time. The git history shows it honestly: two near-silent months in late spring, then just under 300 commits in July.&lt;/p&gt;

&lt;p&gt;The loop stayed the same throughout: define a problem, map the flow, build a version, use it, find what's unclear or broken, improve it. That's not a methodology I read about; it's what happens when you're your own first user.&lt;/p&gt;

&lt;p&gt;Every change ships through a branch and a pull request, and every schema change is a numbered, reviewable migration rather than a hand-edit against the database. There are 31 of them so far. That's not ceremony for a solo project. It's what lets me look back and see exactly when a decision was made and what it changed.&lt;/p&gt;

&lt;p&gt;And yes, I used AI tooling throughout, including Claude Code, for building, testing, iterating and debugging. What it did was compress the distance between "I know what this should do" and "this works." What it didn't do was decide what the problem was, who it's for, what the engine optimizes for, which features to cut, or when a plan is honest.&lt;/p&gt;

&lt;p&gt;There's a symmetry there I only noticed later. I use AI as a development tool and refuse to use it as the product's decision-maker, for the same reason in both directions. The judgment has to be attributable to someone. In the code, that's me. In the app, it's a transparent engine the student can inspect and overrule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to the cafeteria
&lt;/h2&gt;

&lt;p&gt;That cafeteria conversation got one thing exactly right: the problem was never that we didn't know what we had to do. It was that nobody could tell us when.&lt;/p&gt;

&lt;p&gt;That's a scheduling problem, not a note-taking problem. Once you see it that way, you build something different.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Focusity&lt;/strong&gt;: &lt;a href="https://apps.apple.com/il/app/focusity/id6796308900?l=he" rel="noopener noreferrer"&gt;App Store&lt;/a&gt; · &lt;a href="https://www.focusity.co.il/home" rel="noopener noreferrer"&gt;focusity.co.il&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hebrew-first, RTL from the ground up, free while I grow it. If you're a student, or you build for them, I'd genuinely like to hear what you think.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>career</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
