Cody Woolaver

Software engineer portfolio

This is my personal site: part portfolio, part project notebook, and part place to keep useful little web tools alive. I'm updating it into a clearer home for my work, my background, and the experiments I want to keep building over time.

Site Update · September 23, 2026

Giving LLM agents controlled access to this site

I have started asking LLM agents to maintain two kinds of content on this site: blog posts and the finished models in my 3D project library. The work itself is straightforward, but the old path was not. An agent might need to drive a browser, wait for me to complete a login, work around an admin form, or prepare a database migration that would not become visible until the next application release. That was too much machinery for changing content that the application already knows how to store.

I wanted a narrow way to give an agent access when I ask for a specific change, without giving it my admin password or leaving a permanent credential in a conversation. The result is a content API for blog posts and 3D projects, plus a short-lived handoff flow designed for an agent to use directly.

The handoff begins on the admin dashboard. It shows a 12-character temporary code and a five-minute countdown in a compact, collapsible card. I can copy the code, cycle it immediately, or disable code exchange entirely. The code is derived with HMAC from the server secret, the admin account, and a random per-account nonce. The database keeps the nonce, issue time, failure state, and scope policy; it does not need to keep the displayed code as reusable plaintext. A successful exchange rotates the nonce, so the same code cannot be used twice. Five failed attempts lock exchange for five minutes.

That temporary code is exchanged for a random bearer token that lasts seven days. The raw token is returned once. The command-line client writes it to an owner-only file with mode 0600, while the server stores only its SHA-256 digest. The token can be revoked from the admin page, and disabling the handoff also revokes the tokens created through it. The client reports the expiry and a renewal date one day before it, so an agent can ask me for a fresh five-minute code before the weekly credential runs out.

Access is split into blog and project read, write, and delete scopes. The normal handoff grants blog and project read/write access, while delete remains an explicit admin setting. One early version let the agent request its own subset during exchange. That sounded conservative, but it meant a token created while updating a 3D project could not later be reused for a blog post. The server now grants the complete scope set selected on the admin page. The agent no longer makes that policy decision, and an existing narrow token can be renewed immediately when its access needs to be replaced.

The token is only a technical capability. It does not turn possession into permission to change anything it can reach. The repository instructions still require a request naming the blog post, project, file, or deletion. That separation lets the weekly token remain useful across ordinary work without treating every available endpoint as an authorized action.

The API itself uses JSON collection and item routes for listing, reading, creating, updating, publishing, unpublishing, and deleting blog posts and projects. Creation requires an explicit normalized slug, which gives an agent something stable to check after an uncertain request instead of blindly repeating a POST. Item reads return a strong ETag. Updates, uploads, and deletions must send that exact value in If-Match; stale state returns HTTP 412 and forces the agent to read the current item again before changing it.

The project side also accepts final model files, printable resources, and writeup media. Those uploads pass through the same file validation and preview generation used by the admin interface. The response includes the updated project, file metadata, and public URLs, which lets the client verify the completed change without scraping a browser page. Permanent deletion is more constrained: published entries must first return to draft, and project files or media can only be removed from a draft project.

A small command-line client ties the pieces together. It discovers the private token, checks its status, builds authenticated requests, streams large uploads, preserves ETags, and prints structured results without printing the bearer token. For a new blog post, an agent lists the current entries, prepares a JSON payload, sends blog create, then reads the new numeric ID back with blog get to verify the title, body, status, and public URL.

The instructions around that client turned out to matter as much as the endpoints. During the first real blog attempt, an agent still followed the old migration workflow when live access was unavailable. I removed the blog migration generator and made the content API the only delivery path for authored posts. If access is technically blocked, the agent now keeps a ready-to-submit JSON draft and reports the exact step needed to resume. It does not quietly turn the post into an application deployment.

The finished workflow is deliberately small from my side. About once a week, I can give an agent the current five-minute code. It stores a revocable seven-day token and can use the same credential for the blog and 3D library work I request. I keep control of the scope policy and every content instruction, while the agent gets a direct, machine-readable path to do the work and verify the result.

Read full post

Site Update · September 22, 2026

A home for my 3D projects

I've started using LLMs to help turn small, practical ideas into 3D models. Sometimes the result is a part to print; sometimes it is a model that helps me work out how something should fit together. The useful part does not end when a file is exported. I want to be able to find the finished design again, understand what it is for, and know which files belong to it.

That was getting harder as the projects accumulated. A model sitting in a folder or at the end of a conversation has very little context. A filename might identify the shape, but it rarely explains the intended use, which pieces go together, or whether a PDF template is needed alongside the printed part. I wanted one place for the final version of each project, not another collection of half-finished experiments.

So I added a 3D projects library to this site. Each project gets its own page with a short writeup and the final files that make it useful. A simple design may have one model; a larger one can have several components, an assembled view, and supporting files. STL and 3MF downloads sit alongside self-contained GLB models and practical resources such as printable PDF or PNG templates. Those documents are part of the project, not an afterthought.

The page should make the object understandable before anyone downloads a large model. Each file has an automatically generated preview, and 3D files can be opened in an interactive viewer when a closer look is useful. For a multi-part design, the assembled view gives the pieces context that separate file names cannot. The full model only loads when someone chooses to view it, so the project page can stay quick to browse.

I also wanted adding a finished project to feel like publishing content, not shipping a new version of the website. The admin pages let me write the description, attach the final models and supporting images or documents, check the previews, and publish the entry without another code deployment. The boundary is deliberate: the library is for files I am ready to keep and share, while work in progress can stay in my working folders.

Although printing is what brought me here, the library is for 3D modeling projects generally. A design does not need to end as a physical print to deserve a clear record. What matters is that the finished model, its related files, and the explanation of how they fit together live in one stable place.

LLMs have made it easier for me to start making these things. This library is the other half of that habit: a way to keep the useful results after the chat and the working files have faded into the background. When I want to reuse a design—or share it with someone else—I can point to a project page that shows what it is and provides the files that actually matter.

Read full post

Site Update · September 21, 2026

Building my own short links and QR codes

I use short links often enough that depending on another service had started to feel unnecessary. This site already stores and serves my ShareX uploads, so adding a small URL shortener felt like a natural extension: one more useful tool that could live alongside the rest of the Portfolio instead of somewhere else.

The basic version is deliberately straightforward. A short link has a unique slug, a destination, an optional title, and a visit count. The admin side lets me create memorable links, update where they lead, and remove them when they are no longer useful. That ability to change the destination is the part I care about most. I can print or share one stable address without treating the first destination as permanent.

The public redirect is small, but it has a few important boundaries. Destinations are validated before they are saved and checked again before a redirect is served. Redirect responses are explicitly non-cacheable so changing a destination takes effect instead of being hidden behind a stale browser or intermediary response. They also carry a no-referrer policy, which keeps the destination from receiving the short-link page as referral information. Successful visits are counted without making an ordinary redirect look like an administrative edit.

I also connected the shortener to ShareX. It now has a separate URL-shortening configuration in addition to the existing file and text upload configuration. Sending a URL through that path creates a random short link and returns the compact address immediately. The generated entry still appears in the same admin area, so it can be given a title, reviewed, or pointed somewhere else later. That turned the feature from something I could use occasionally into something that fits an existing daily workflow.

QR codes were the next obvious step. The generator can encode one of the hosted short links or any other text. Choosing a hosted link keeps the useful indirection: the printed QR code continues to point at the same short address even if I change its final destination. The arbitrary-text option works for ordinary URLs, contact details, Wi-Fi notes, or multiline text without saving that content as another record.

The generator produces PNGs in memory and shows a live preview as the form changes. Text input is debounced briefly so typing does not turn into a stream of unnecessary requests, and the page reports the remaining UTF-8 capacity before the server performs the same validation. Opening the generator from an existing short link renders its QR code immediately, while switching sources or appearance options refreshes the preview without requiring a separate button click.

I ended up giving the appearance controls more attention than I expected. There are a few high-contrast presets, square, rounded, and dot module shapes, editable foreground and background colours, and an option for a transparent PNG. Image padding is configurable all the way down to zero, while the page keeps its own preview spacing separate from the downloaded file. The larger finder patterns remain square across the decorative styles because a QR code that looks interesting but scans poorly is not much of a tool.

This was one of those projects where several modest pieces became much more useful together. The shortener gives me stable links I control, ShareX makes them quick to create, visit counts provide a small amount of practical feedback, and the QR generator gives the same links a physical form. None of it needs to be a separate product. It is simply another part of the site that now does a job I previously handed to someone else.

Read full post

Site Update · September 8, 2026

A summer of rebuilding and automating

It has been a busy stretch since I last wrote about turning the blog into a real part of this site. The visible work and the behind-the-scenes work ended up following the same theme: take something I care about, give it a clearer shape, and make it easier to operate with confidence.

The biggest addition visitors can see is a complete Cribbage game. It supports room-code games between two people, several computer-opponent difficulties, automatic scoring, discard and pegging hints, configurable score limits, and a few board styles. Most of the game state lives on the server, so the browser is presenting a shared game instead of trying to be the authority for one.

Cribbage was a much larger project than it first appeared. The basic loop is familiar, but there are a lot of small rules hiding inside dealing, the crib, pegging, go, counting hands, ending a round, and deciding exactly when a game is over. Building it meant separating those rules from the room lifecycle, making reconnects and concurrent actions safe, and then fitting all of that into an interface that still works on a phone. The computer player and hints added another layer: they needed to make useful choices while also being able to explain the scoring behind them.

The larger summer project was rebuilding my homelab. I moved from an older all-in-one setup to a small Proxmox cluster with dedicated Docker runtimes and a Git repository that describes the intended state. Applications, storage mounts, networking, secrets, health checks, and deployment rules now have reviewed definitions instead of living mainly as remembered commands. The transition included this portfolio, Git hosting, databases, monitoring, and the media services I use at home.

That rebuild also changed how this site reaches production. A push to the Portfolio repository runs the test suite, but it does not deploy by itself. Publishing a release builds an immutable container image for the exact commit and hands its digest to the infrastructure workflow. The infrastructure side validates that handoff, records the new image, and performs the deployment. It is more ceremony than a personal site strictly needs, but it gives me a real release path that I can inspect, repeat, and improve.

I put the same effort into seeing what the system is doing. Prometheus, Loki, Grafana, and Alloy now cover service health, capacity, logs, and the automation jobs that move work around the homelab. Actionable alerts go to a private ntfy instance, while application exceptions have their own private error tracker. The goal is not to build a miniature enterprise operations centre. It is to have enough signal that a failure leaves evidence and the useful next question is easier to ask.

Some of the most enjoyable work has been the media automation. I built a "Cartoon TV" system that can assemble and play a living channel from my own library, remember viewing history, and provide a small import queue for filling gaps. The torrent lifecycle around it became more deliberate too: downloads can be routed according to available storage, copies report progress, completed files are verified against their piece hashes, and cleanup waits for the seeding policy instead of guessing that a transfer succeeded. It is a wonderfully over-engineered way to put cartoons on a television, which is exactly why it was fun.

The less visible milestone is recovery. Databases are exported into encrypted backup sets, the Docker virtual machines have scheduled backups, and restore checks run against isolated temporary targets rather than production. I completed restore drills across the application-managed state and fixed several problems that only appeared when the backups were actually consumed. That work reinforced an old lesson: creating a backup is only the first half of the feature; proving that it can restore is what makes it useful.

More recently I added a private location-history service and turned stack upgrades into a release-aware workflow that checks compatibility and rollback assumptions before changing a running application. Those are small examples of where the homelab has ended up. It is still a place to experiment, but the experiments now have clearer boundaries, better records, and a much more reliable path from idea to running service.

There are plenty of rough edges left, and I expect there always will be. What feels different now is that the site and the infrastructure underneath it are no longer separate hobbies. The Portfolio gives me somewhere to build and explain projects; the homelab gives those projects a disciplined place to run; and each round of work makes the next one a little easier to understand.

Read full post

Site Update · June 13, 2026

Making blog posts a real model

One of the quieter changes in this site refresh was moving blog posts out of one-off template edits and into a real BlogPost model. That sounds like a small model update, but it changes the way the site can grow. A post now has a title, slug, status, publish date, body, and timestamps that belong to the application instead of being scattered through static markup.

That move was smart because it gives the writing side of the site the same kind of shape as the rest of the project. Published posts can be queried, sorted, counted, previewed on the home page, and shown on their own stable URLs. Drafts can exist without being public. The admin dashboard can surface the latest publishing activity. Most importantly, adding a post no longer means inventing another little content path by hand.

I also like that the model gives future work somewhere obvious to land. Tags, RSS, better excerpts, scheduled publishing, search, or project-specific post collections can all build on the same table instead of needing a new static convention each time. Even if I never add every one of those ideas, the current shape keeps the door open without making the first version heavy.

The implementation stayed intentionally plain. Markdown is stored as source text, rendered through a CommonMark parser, and cleaned before it reaches the page. The public routes use date pieces plus a slug, so URLs are readable and stable. Publication fields are denormalized just enough to make uniqueness and routing straightforward: the date is available as a real date, and the published day key protects against accidentally publishing two public posts on the same day.

The admin flow was another practical part of the change. I can create, edit, preview, publish, and unpublish posts from the site itself, while older launch posts can still be seeded through migrations when that is the right tool. This entry is doing exactly that, which feels fitting: the blog system is now content, data, and deployment working together instead of a pile of special cases in templates.

Read full post