2026-09-21 22:21:27 UTC

iBoard.cc

The Programming Kitchen (a dev only blog)

iBoard - ABOUT

System
#iboard

iBoard is a little practicing project, figuring out how Elixir-development and AI comes together. So, I recently wrote this Blog-server. The content here doesn’t really matter! The project is not public (yet) but will be published at GitHub once it fulfills some basic quality standards ;-)

The iBoard Project This web application is written in Elixir with Phoenix, LiveView, TailwindCSS, DaisyUI, Ecto/Postgres, to name the most important.

ExDocs

Documentation

The full dependency list reads like:

:bcrypt_elixir, "~> 3.0"
:phoenix, "~> 1.8.3"
:phoenix_ecto, "~> 4.5"
:ecto_sql, "~> 3.13"
:postgrex, ">= 0.0.0"
:phoenix_html, "~> 4.1"
:phoenix_live_reload, "~> 1.2", only: :dev
:phoenix_live_view, "~> 1.1.0"
:lazy_html, ">= 0.1.0", only: :test
:phoenix_live_dashboard, "~> 0.8.3"
:esbuild, "~> 0.10", runtime: Mix.env() == :dev
:tailwind, "~> 0.3", runtime: Mix.env() == :dev
:heroicons, github: "tailwindlabs/heroicons", tag: "v2.2.0"
:gen_smtp, "~> 1.2"
:swoosh, "~> 1.5"
:castore, "~> 1.0"
:req, "~> 0.5"
:telemetry_metrics, "~> 1.0"
:telemetry_poller, "~> 1.0"
:gettext, "~> 1.0"
:gettext_sigils, "~> 0.1.0"
:jason, "~> 1.2"
:dns_cluster, "~> 0.2.0"
:bandit, "~> 1.5"
:tzdata, "~> 1.1"
:boundary, "~> 0.10", runtime: false
:ex_doc, "~> 0.31", only: [:dev, :prod], runtime: false
:earmark, "~> 1.4"

With a big hug to this gorgeous community!

Follow the tag #iboard if you’re interested in the faith of this project.

Features so far (Buzzwords)

  • Accounts, Users (phx.gen.auth)
  • Posts, Drafts, Authors, Moderators, Likes, Followers
  • User management, invite by e-mail
  • Supports DaisyUI Themes
  • Supports Locale and Timezone
0
2026-04-02 18:52

Release 1.0.0

Avatar Andi
#elixir #api #phoenix #release

Version 1.0.0 is out. It collects a single day of work around one idea: this blog should be writable from the place where articles are actually written — a text editor and a terminal.

What is new

A JSON API. Six endpoints, authenticated with a bearer token, cover the whole life of an article: create it, upload its attachments, publish it, read it back, revise it. A key belongs to exactly one user and acts as that user, so nothing about visibility or ownership changes just because the request came from a script.

A client. post-article takes a Markdown file with a short front matter block and does the rest: create the draft, upload the images, publish on request, notify the followers only when asked. Drafts are the default, because the only step that cannot be undone is the e-mail.

Key management in the browser. Admins mint and revoke keys under API Keys, with a guide on the page itself: where to store the key, which commands to run, what each endpoint does, and where the limits are. A new key is shown exactly once — only its hash is kept.

An audit trail. Every API call is recorded with method, path, status, IP address and the name of the key that made it. Rejected calls are recorded too. Whatever a script did, the audit log knows which key did it.

Downloads. The client and the agent skill can be fetched straight from the documentation, so a new machine needs nothing but a browser.

Smaller repairs along the way

Reading the first API-written article back from the site turned up three things worth fixing:

  • The client split articles at the language separator even when that line sat inside a code example, cutting the text in half.
  • The language switch on an article drew a border around the language you were not reading, which looked like the button to press.
  • Relative timestamps only knew how to phrase the past, so a key valid for another year politely reported that it expired “just now”.

Try it

mix w_app_core.api_token create you@example.com --label laptop
post-article --check
post-article --publish article.md

This post was written in an editor and sent from a terminal.

1
2026-09-21 19:06

Writing posts from the terminal

Avatar Andi
#phoenix #elixir #howto #api

This blog now accepts articles over a JSON API. Write the text in your editor of choice, run one command, and the post lands here — as a draft first, so nothing goes out before you have read it in its final shape.

Screenshot
Screenshot — From the Terminal into the Blog without using a browser.

Why bother

The editor in the browser is fine for a quick note. It is less fine when the text already exists as a file, when the screenshots sit in a folder next to it, or when the article is the last step of a script that has just finished a release. For those cases the blog got an API.

The shape of it

Six endpoints, all authenticated with a bearer token:

GET   /api/me                        the user behind the token
POST  /api/posts                     create a post (draft by default)
GET   /api/posts/:id                 read one of your own posts
PATCH /api/posts/:id                 revise a post, field by field
POST  /api/posts/:id/attachments     upload a file
POST  /api/posts/:id/publish         publish, optionally notify followers

A key belongs to exactly one user and acts as that user, so the same visibility and ownership rules apply as in the browser. Keys are stored hashed, expire after a year, and are minted and revoked by an admin under API Keys.

One command

The interesting part is the client. An article is a Markdown file with a small front matter block:

---
subject: The English headline
subject_de: Die deutsche Überschrift
tags: elixir phoenix
visibility: public
attachments:
  - diagram.png
---
The English body. Embed the image with {diagram.png}.

--- de ---

Der deutsche Text, wieder mit {diagram.png}.

Then:

post-article --publish article.md

The script creates the draft, uploads the attachments, publishes, and prints the URL. Leave off --publish and you get a draft to review. Add --notify and the followers get the usual e-mail — with the images already in place, which is exactly why uploading happens before publishing.

What it does not do

It does not unpublish, it does not delete, and it will not let you touch someone else’s post. Those stay in the browser, where a mis-click is visible and reversible. Revising works, though: a PATCH changes exactly the fields it is given — this very paragraph arrived that way. Every API call is written to the audit log with method, path, status, IP and the name of the key that made it, so it is always clear which script did what.

This article was posted with the command above.

0
2026-09-21 18:02

gitpro — a GitHub project board in the terminal

Avatar Andi
#published #devop #elixir #iboard

I read a project board far more often than I edit one. What is on the board is something I want to know while I am standing in the checkout, working — not after opening a browser, finding the organisation, clicking the right board and hunting down the card.

So: gitpro, version 1.0.0, now on Hex.

cd ~/Projects/some-repo
gitpro

That is the whole invocation. gitpro asks git which repository the working directory belongs to, asks GitHub which Projects v2 boards that repository is linked to, and opens the cards on one of them.

Screenshot
Screenshot — A Screenshot of the TUI of gitpro, showing the startpage of the app, with a project listed.

Typing is searching

There is no mode to enter and no key to press first. The search field has the keyboard from the moment gitpro opens — every printable character is the query’s, and everything else is bound to a key the field does not want. Every word has to match somewhere in the row: its number, title, repository, column, author, milestone, labels, assignees or state.

^F opens the switches: state (open, closed, merged, draft — only the ones the board actually has), the board’s kanban columns, the labels the cards wear and the people they are assigned to, each with an empty bucket of its own. The list narrows behind the popup as the switches are set, so you can see what a switch does before you commit to it.

^I shows a card in full — title, description, state, column, repository, author, labels, assignees, milestone and dates. All of it arrived with the board, so the popup opens instantly and keeps working with the network gone. j and k walk to the next card without closing it: reading through a column is opening the first card and holding j. ^O or Enter opens the card in a browser.

How it is put together

The UI is Atui, a terminal toolkit in the shape of Phoenix LiveView: views with state, a render/2 that returns cells, and a runtime that owns the terminal.

It talks to GitHub through the gh CLI, which means there is no HTTP client, no JSON dependency and no token to configure beyond gh auth login.

Two things are deliberate. Failing happens in the terminal, not in the UI — “not a git repository”, “gh is not installed”, “this repository is not on a project board” are printed on stderr, with a non-zero exit status, before the alternate screen is opened. And everything that decides what is on screen is a pure function over data: the filters, the GraphQL decoders, the row widths, the window onto a list longer than the screen. So the interesting half of the application is tested without a terminal and without a token.

Installing

gh has to be installed and logged in with the project scope:

gh auth login
gh auth refresh -s project      # if 'project' is not already in gh auth status

Then, in one step:

mix escript.install hex gitpro

The escript lands in ~/.mix/escripts, so that wants to be on your PATH.

Not yet

An MVP, and it shows. Next up: comments and sub-issues in the detail popup, moving a card between columns from here rather than in a browser, and remembering the switches per board between runs.

GPL-3.0-or-later.

0
2026-09-19 14:29

What Was That Sound?

Avatar Andi
#devop #omarchy #published #opensource

You know the moment. Something pings. A soft little blip from somewhere in the machine,

Omarchy Sound Source Plugin
Omarchy Sound Source Plugin — Screenshot

and you have no idea what made it. Was that a chat message? A build finishing? A browser tab you forgot you left open three hours ago?

You end up doing the thing we all do: alt-tab through every window hunting for a badge, find nothing, and go back to work slightly annoyed. The sound is gone. Whatever wanted your attention got it, and then declined to say who it was.

So I wrote a small Omarchy shell plugin that just tells you.

Sound Source

🌐 Omarchy Plug Ins - Marketplace

When an app starts playing audio, a short popup appears naming it. That’s the whole idea. A speaker icon sits on the bar; left-click turns announcements on and off, right-click opens a setup dialog.

The dialog is where the interesting bits live. You get the on/off switch again, a per-app Never announce list, and controls for where the popup appears and how long it stays.

One detail there I’m quietly pleased with: the app list isn’t what’s playing right now — it’s everything the plugin has seen play since the shell started. That sounds like a small distinction, but it’s the one that makes the feature usable. By the time you’re irritated enough to go looking for the culprit, the culprit has already stopped making noise. A list of currently playing apps would be empty exactly when you need it. Ignored apps stay in the list too, so you can always take one back off.

New streams, not loud streams

Here’s the part I found genuinely interesting to build.

The obvious way to detect “an app made a sound” is to watch audio levels: attach a meter to everything and announce whenever something goes from silent to loud. It’s the intuitive approach, and it’s the wrong one.

Watch what a discrete sound actually is in the PipeWire graph. An app opens a fresh playback stream, plays its blip, and closes the stream again. One notification sound is one node appearing and then vanishing. That’s a clean, cheap, unambiguous signal — so the plugin watches for new playback streams appearing, reads the app name off the stream, and shows it.

No meters. No polling. Nothing running when nothing is happening.

media.role comes along for free on most streams, so it also picks the glyph — a music note for Music, Video and Game, something quieter for a notification.

The double-popup mystery

Of course it wasn’t that clean at first. Early on, every single sound popped up twice.

The culprit was Omarchy’s parametric EQ. It takes the app’s stream, processes it, and re-publishes it — so the graph gets two new nodes per sound, one from the app and one from the EQ. The fix is to skip anything tagged media.role=DSP, plus nodes whose names start with output.omarchy., input.omarchy. or omarchy_speaker_tuning. That’s plumbing, not an application, and it has no business raising a popup.

A couple of other small guards earned their place the same way. A browser opening three streams at once should announce once, not three times, so there’s a short repeat window (repeatMs, 1500ms by default). And streams that are already playing when the shell loads get recorded silently rather than announced — otherwise every shell restart would fire a volley of popups at you (startupGraceMs).

What it deliberately doesn’t do

An app that holds one long-lived stream open and plays all its sounds through it produces no new node, so nothing gets announced.

Music players work this way, which is harmless — you know you started the music. But a chat app keeping a permanently open output stream would slip right through, and that’s exactly the case you wanted this for.

Catching it means going back to level watching after all: a peak monitor per stream, announcing on silent-to-loud. And that’s a standing cost — a monitor running on every stream for as long as the shell is up, whether or not you ever hear anything. I left it out rather than shipping it disabled behind a setting. If it turns out people hit this often enough, that’s the moment to reconsider, not before.

I’d rather ship something small that’s honest about its edges than something that quietly burns CPU to cover a case most people never meet.

Living on the bar properly

The bar icon went through a revision that taught me something about Omarchy’s shell.

Originally it stayed drawn and just dimmed when announcements were off. That worked, but it sat oddly next to the stock indicators — Dnd, NightLight and friends conceal themselves entirely and collapse their slot until you hover the indicator area. Mine was the one icon that wouldn’t take a hint.

Making it behave turned out to be less obvious than expected. You can’t simply list a plugin in omarchy.indicators‘ own items: that widget resolves every entry to a file in the shell’s packaged indicators directory, so a plugin id there loads exactly nothing. Instead the widget reaches into the bar’s moduleSlots, finds the live omarchy.indicators instance, and reads its reveal state — which lets a plugin join the group from its own slot. Hovering the icon holds that reveal open, so it can’t slide out from under your pointer before the click lands.

It only conceals itself if such a host actually exists, mind. On a bar with no indicators widget there’d be no hover to bring it back, and a switch nothing can click is a switch stuck off.

Getting it

No dependencies beyond Omarchy 4.x and PipeWire. It uses Quickshell’s own Quickshell.Services.Pipewire, so there’s nothing to build and no helper process to keep alive — pure QML and JavaScript.

omarchy plugin add https://github.com/iboard/sound-source.git --enable --yes
omarchy bar put io.github.iboard.sound-source --after omarchy.indicators
omarchy restart shell

Everything is reachable over IPC too, for keybindings or the Omarchy menu:

omarchy-shell sound-source now      # what's playing right now
omarchy-shell sound-source last     # the app announced most recently
omarchy-shell sound-source toggle   # same switch as the bar icon

Plugins land disabled if you drop the --enable, so you can read the code first. Which you should — for this or any other plugin, since they run unsandboxed inside omarchy-shell.

It’s Apache-2.0, and it’s on its way to the Omarchy plugin marketplace. Source, README and the full settings table are on GitHub.

Now go find out what that pinging was.

3
2026-09-08 12:50

The New Audit Log

Avatar Andi
#changelog #security #transparency

More Transparency: The New Audit Log

Starting today, the blog keeps an audit log — a complete, human-readable record of what happens on the platform. From “Andreas created the post ‘Welcome!’” to “Hannah liked your post,” every meaningful action is captured and stays traceable.

Two Views

  • My Activity (/activity): Every signed-in user sees their own timeline — everything you did, or that concerns you (your account, your posts).
  • Audit Log (/admin/audit-log): Administrators see the complete timeline for the entire platform.

Both views update live: new entries appear automatically, no refresh needed. With infinite scrolling and a full-text search across action, person, and subject, you’ll quickly find what you’re after — even in long histories.

Reads or Writes — Your Call

Every entry is tagged as a read or a write. Opening a post, profile, or group counts as a read; everything else counts as a write. The All / Writes / Reads toggle lets you show exactly what matters to you — the total count and live updates follow your selection.

Sessions at a Glance

Sign-ins are part of the timeline too: a guest visit is recorded as “New session from <IP> (<country>),” and logins and logouts as “logged in” and “session ended.” The IP, country, and session ID live in the entry’s metadata.

Since these can pile up fast, there’s a Hide sessions switch: one click and the pure session entries drop out of the list, the count, and the live updates — keeping your focus on the events that actually matter.

Server Restarts, Made Visible

When the server restarts, it writes its own entry: “The server started.” Unlike the other entries, this one is visible to all signed-in users. So you can see at a glance whether and when there was an update or a restart.

Why Bother?

Transparency builds trust. The audit log makes it clear who did what and when — both for you personally and for the people running the platform. No more guesswork, no black box.

Enjoy exploring your timeline!

0
2026-07-26 13:18

Introducing Groups: Share with exactly the people you choose

Avatar Andi
#iboard

Until now, a post here could be public, private, or sent as a direct message. That covered the extremes — everyone, or just one person — but not the middle ground most of us actually live in: this handful of people, and no one else.

Groups fill that gap.

Create a group in seconds

Head to My Groups in the sidebar (or /groups) and hit New group. Give it a name, and start adding people:

  • Friends are added instantly — they’re active right away.
  • Anyone else gets an email invitation. Once they confirm, they join the group and become a mutual friend, so the connection works both ways.

Posts for members only

When you write a post, visibility now has a Specific Groups option. Pick which groups may read it — and, separately, which of those may comment. Only active members of a listed group can see or reply.

Every group-restricted post carries a “Who can read this” section, so it’s always clear exactly who’s in the room. No guessing, no accidental oversharing.

It lands in the inbox

Published group posts show up in each member’s inbox (/dm) right alongside direct messages, complete with a Group badge and per-person read/unread tracking — the same unread counter you already know from the navbar. Nobody misses what’s meant for them.

A home for every group

Each group has its own page (/groups/:id) listing its members and how many posts each has published. Any active member can invite new people; you can remove anyone you added and leave whenever you like; and the owner can manage everyone.

Groups are yours to shape — start one today and share with just the right circle.

0
2026-07-08 16:52

More posts

Browse all posts you can see.

Authors
2
Postings
54
Comments
13
Groups
3
Likes
21
Following
17
Followers
17
Friends
6