<?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>Forem: Frandy Slueue</title>
    <description>The latest articles on Forem by Frandy Slueue (@frandy-slueue).</description>
    <link>https://forem.com/frandy-slueue</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3852939%2F1e8afc0e-6346-41a9-8347-6f9f280ede47.jpg</url>
      <title>Forem: Frandy Slueue</title>
      <link>https://forem.com/frandy-slueue</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/frandy-slueue"/>
    <language>en</language>
    <item>
      <title>The coordinate space bug that four rewrites couldn't fix</title>
      <dc:creator>Frandy Slueue</dc:creator>
      <pubDate>Sat, 04 Apr 2026 10:29:55 +0000</pubDate>
      <link>https://forem.com/frandy-slueue/the-coordinate-space-bug-that-four-rewrites-couldnt-fix-3khi</link>
      <guid>https://forem.com/frandy-slueue/the-coordinate-space-bug-that-four-rewrites-couldnt-fix-3khi</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2F8c5iavdx5x7j75w21fg1.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.amazonaws.com%2Fuploads%2Farticles%2F8c5iavdx5x7j75w21fg1.png" alt="frandy-dev timeline component" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I spent most of today's session on a bug that turned out to be architectural, not logical.&lt;/p&gt;

&lt;h3&gt;
  
  
  The setup
&lt;/h3&gt;

&lt;p&gt;frandy.dev has an animated timeline section. Cards sit in a horizontal track. You can fold them to peek width by dragging the right edge. A neon light travels across three spine tracks and flashes when it reaches each card's node dot.&lt;/p&gt;

&lt;p&gt;The flash timing was wrong — off by varying amounts depending on scroll position and card state.&lt;/p&gt;

&lt;h3&gt;
  
  
  The failed approaches
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Fixing the threshold — larger, smaller, speed-dependent&lt;/li&gt;
&lt;li&gt;Adding direction guards — only flash when approaching&lt;/li&gt;
&lt;li&gt;Fixing the position math — accounting for scrollLeft, cardWidths state&lt;/li&gt;
&lt;li&gt;Full rebuild of the detection loop&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of it worked consistently.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual problem
&lt;/h3&gt;

&lt;p&gt;The traveling light existed in &lt;strong&gt;viewport space&lt;/strong&gt; — pixels from the left edge of the visible overlay.&lt;/p&gt;

&lt;p&gt;Node positions were calculated in &lt;strong&gt;card space&lt;/strong&gt; — accumulated pixel widths across all cards, starting from the left of the entire track including scrolled-off cards.&lt;/p&gt;

&lt;p&gt;These only agree at one specific state: scroll = 0, all cards open. Any deviation and they diverge.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;measure&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;oRect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&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;dots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-tl-node]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &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;dot&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;dots&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&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;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;oRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;overlayWidth&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="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// off-screen, skip&lt;/span&gt;
    &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt; &lt;span class="p"&gt;});&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;Stop calculating. Measure from the DOM. &lt;code&gt;getBoundingClientRect()&lt;/code&gt; gives you the actual rendered position in the coordinate system you're already using.&lt;/p&gt;

&lt;p&gt;Re-measure on scroll, card width changes, filter changes, resize. Use a dirty flag so you measure at most once per animation frame.&lt;/p&gt;

&lt;p&gt;The light now flashes exactly on the node dot. Every time. Because it's reading reality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Broader takeaway
&lt;/h3&gt;

&lt;p&gt;When your coordinate math keeps drifting from what you see on screen, you're probably in the wrong coordinate space. The browser already computed the correct answer. &lt;code&gt;getBoundingClientRect()&lt;/code&gt; hands it to you.&lt;/p&gt;




&lt;h3&gt;
  
  
  What else shipped
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Theme system&lt;/strong&gt; — full light/dark mode with 4 accents. localStorage beats admin default. Desktop dropdown, mobile slide-down sheet that swaps visibility with BackToTop on scroll.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeline UX&lt;/strong&gt; — rubber-band drag, spring physics, sticky first card, double-tap to open panel, card index watermark, breathing peek dot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3-track spine pulse&lt;/strong&gt; — comet tail, speed variation, escort offset, node flash with spin + ripple, future card color fallback, clean restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UI polish&lt;/strong&gt; — chip/TabBar borders fixed, BackToTop realigned, section padding and nav height increased, text opacity improved.&lt;/p&gt;




&lt;h3&gt;
  
  
  Where things stand
&lt;/h3&gt;

&lt;p&gt;The site is not shipped yet. Timeline desktop is done. Next is a design-only pass — mobile layout for every section, then admin. No new features. Getting everything looking right before it goes live.&lt;/p&gt;




</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I sat on my engineering skills for years. Here's what I'm doing about it.</title>
      <dc:creator>Frandy Slueue</dc:creator>
      <pubDate>Tue, 31 Mar 2026 07:31:37 +0000</pubDate>
      <link>https://forem.com/frandy-slueue/i-sat-on-my-engineering-skills-for-years-heres-what-im-doing-about-it-3ma8</link>
      <guid>https://forem.com/frandy-slueue/i-sat-on-my-engineering-skills-for-years-heres-what-im-doing-about-it-3ma8</guid>
      <description>&lt;p&gt;I graduated from Holberton School Tulsa — now Atlas School of Tulsa — as a full stack software engineer. Then I mostly didn't use those skills for a while. Life moved in other directions and the urgency faded.&lt;/p&gt;

&lt;p&gt;Recently I started job hunting in software engineering and the market made things clear pretty quickly: AI has changed the landscape significantly, the competition for roles is intense, and a credential without visible work to back it up doesn't move the needle the way it used to. I needed more than a diploma.&lt;/p&gt;

&lt;p&gt;So I thought about what actually separates developers who get hired from developers who don't. The answer I kept coming back to was documented, public work — projects where someone can see the decisions, the reasoning, the mistakes, and the progress. A build diary, essentially. A record of how you think.&lt;/p&gt;

&lt;p&gt;That's what I didn't do at Atlas School. I built things, but I didn't document them. I'm doing that now, starting with my portfolio.&lt;/p&gt;




&lt;h3&gt;
  
  
  The old site problem
&lt;/h3&gt;

&lt;p&gt;There's already a version of frandy.dev. It's live right now — static, not particularly well thought out, built without much intention. It doesn't represent what I can do, and I've known that for a while.&lt;/p&gt;

&lt;p&gt;The new version replaces it completely. This is a full stack application with a real backend, a projects CMS, animated theme backgrounds I designed myself, and a level of craft the original site never had. The goal: show what I've actually learned over the years, not just prove I can ship a webpage.&lt;/p&gt;




&lt;h3&gt;
  
  
  What I built
&lt;/h3&gt;

&lt;p&gt;The complete backend for frandy.dev:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; (Python 3.12) with auto-generated OpenAPI docs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL 16&lt;/strong&gt; with SQLAlchemy 2.x async ORM and Alembic migrations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Compose&lt;/strong&gt; — five services: FastAPI, Next.js, PostgreSQL, Umami, Nginx&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT auth&lt;/strong&gt; with httpOnly cookies and bcrypt password hashing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Projects CMS&lt;/strong&gt; — CRUD, image upload with validation, drag-and-drop reorder, publish/draft states&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contact form&lt;/strong&gt; with Resend transactional email (fire-and-forget — form always succeeds regardless of email delivery)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub stats cache&lt;/strong&gt; — APScheduler pulls commits, languages, pinned repos every 3 hours into PostgreSQL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Theme system&lt;/strong&gt; — four dark themes, stored in the database, auto-rotates every 90 days, admin can override anytime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume manager&lt;/strong&gt; — upload a PDF, it updates the download URL sitewide instantly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: github.com/frandy-slueue/frandy.dev&lt;/p&gt;




&lt;h3&gt;
  
  
  The planning lesson — the real one
&lt;/h3&gt;

&lt;p&gt;My old way of working: start coding, figure out the details as I go. Pick the tech stack mid-feature. Change the database schema after the routes that depend on it are already written. It works, eventually — but it's slower and more frustrating than it needs to be.&lt;/p&gt;

&lt;p&gt;For this project I committed to something I'd never fully done before: a complete specification document before opening a code editor. Every section of the site. Every API endpoint with its request and response shape. Every database table with column names and types. The Docker Compose services. The Nginx config. The deployment steps. The DNS setup.&lt;/p&gt;

&lt;p&gt;All of it on paper before line one.&lt;/p&gt;

&lt;p&gt;Here's what it actually produced: every time I sat down to code and didn't know what to build next, the spec had the answer. Every time I was tempted to cut a corner, I checked the spec and remembered why I'd decided not to. The planning didn't slow the build down. It made the build predictable. Predictable is fast. This is the thing I wish someone had said clearly to me at school.&lt;/p&gt;




&lt;h3&gt;
  
  
  First-time tools — stepping out deliberately
&lt;/h3&gt;

&lt;p&gt;Part of how I'm approaching projects now is deliberately including tools I've never used. Real pressure — something that actually has to work — produces different learning than a tutorial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Umami&lt;/strong&gt; — self-hosted analytics. I wanted control over visitor data without handing it to a third party. Umami runs as a Docker service and I proxy its API through FastAPI. The frontend never talks directly to Umami.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nginx&lt;/strong&gt; — reverse proxy for SSL termination and traffic routing. I'd understood it conceptually but never configured one for a real multi-service application. It made sense once I reframed it: Nginx is a traffic director, not a server in the application sense. Every request comes in, Nginx decides which container handles it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resend&lt;/strong&gt; — transactional email. Clean API, minimal SDK. The one design decision I was careful about: the email send is wrapped in try/except and returns &lt;code&gt;False&lt;/code&gt; on failure without raising. The form submission always succeeds. Whether the email lands is a separate concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DigitalOcean&lt;/strong&gt; — where the finished application will live. That comes after the frontend. The research is done.&lt;/p&gt;

&lt;p&gt;The research into each of these tools was genuinely rewarding. Every technology made more sense once I understood the problem it was designed to solve. None of them were as intimidating on the other side as they looked from the outside. I keep learning this and then forgetting it when the next unfamiliar thing appears.&lt;/p&gt;




&lt;h3&gt;
  
  
  The bugs and struggles — specific ones
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The bcrypt/passlib compatibility issue.&lt;/strong&gt; &lt;code&gt;passlib[bcrypt]&lt;/code&gt; breaks silently with newer versions of the &lt;code&gt;bcrypt&lt;/code&gt; package. The error — &lt;code&gt;AttributeError: module 'bcrypt' has no attribute '__about__'&lt;/code&gt; — gives you nothing useful about what's actually wrong. I looked at the hashing logic, the database connection, the settings class. All fine. The problem was two lines in &lt;code&gt;requirements.txt&lt;/code&gt;. Fix: pin &lt;code&gt;passlib[bcrypt]==1.7.4&lt;/code&gt; and &lt;code&gt;bcrypt==4.0.1&lt;/code&gt; together. I now pin every dependency before writing application code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The swapped files.&lt;/strong&gt; Built the contact router and email service in the same session, saved their contents into the wrong files. Both were syntactically valid Python — no errors, nothing flagged. The server told me the contact module had no &lt;code&gt;router&lt;/code&gt; attribute and I had to trace it back by reading both files. Happens when you're moving fast across many new files. Small mistake, harder to catch than it looks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardcoded container paths.&lt;/strong&gt; Upload directory set to &lt;code&gt;/app/uploads&lt;/code&gt; — correct in Docker, a &lt;code&gt;PermissionError&lt;/code&gt; locally. Looked like a permissions problem before I realized the path simply doesn't exist outside the container. Fix: environment variable with a local fallback, declared in Pydantic &lt;code&gt;Settings&lt;/code&gt;. Rule: runtime paths belong in environment config, not source code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alembic "target database is not up to date."&lt;/strong&gt; Tried to generate a migration before applying existing ones. Always run &lt;code&gt;alembic upgrade head&lt;/code&gt; before &lt;code&gt;alembic revision --autogenerate&lt;/code&gt;. Every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pydantic strict extra fields.&lt;/strong&gt; Added &lt;code&gt;UPLOAD_DIR&lt;/code&gt; to Docker Compose env before adding it to the &lt;code&gt;Settings&lt;/code&gt; class. &lt;code&gt;BaseSettings&lt;/code&gt; raises a validation error for any undeclared environment variable. Fix: declare in &lt;code&gt;Settings&lt;/code&gt; first. Always.&lt;/p&gt;




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

&lt;p&gt;Pin all dependencies before writing line one. Write seed scripts alongside the models. Commit at a finer grain — one logical unit per commit so you can trace what broke and when. Test locally earlier in each session instead of writing a lot before running anything.&lt;/p&gt;




&lt;h3&gt;
  
  
  Where things stand
&lt;/h3&gt;

&lt;p&gt;Backend is done. Frontend is next — Next.js 16+, Framer Motion, four custom animated backgrounds, and a morphing mosaic grid. I'll document that build the same way I documented this one.&lt;/p&gt;

&lt;p&gt;I'm excited to see what the finished thing looks like. The spec was ambitious. The backend delivered on it.&lt;/p&gt;

&lt;p&gt;Wish me luck.&lt;/p&gt;

&lt;p&gt;Repo: github.com/frandy-slueue/frandy.dev&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Frandy Slueue — Full Stack Software Engineer&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Atlas School of Tulsa (formerly Holberton School Tulsa)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
