Merge branch 'main' of ssh://gitea.apps.sustainabledelivery.com:3022/mostalive/firehose

This commit is contained in:
Firehose Bot
2026-04-01 23:34:24 +01:00
67 changed files with 3708 additions and 24 deletions
@@ -9,17 +9,17 @@
I made a skill for a coding agent to help me get more of my draft blog posts over the line. I enjoy writing, and am somewhat fluent in it. Publishing that writing is more hit and miss, however. I often lose energy just before a piece is finished enough. I want to publish more often, and need to form a more effective habit for it.
# What did I get out if it?
# What did I get out if it?
I got a working agent 'skill' in an hour or so. I like the QWEN models for their no-bullshit approach to feedback. As it turns out, I have about 60 pages with the 'Candidate Blogpost' tag in my notes, but most of them are not more than an idea. Only some of them have enough detail to turn into a post. I am going to keep this around, prune my candidate blogposts, and add my recent clippings to the mix.
I got a working agent 'skill' in an hour or so. I like the QWEN models for their no-bullshit approach to feedback. As it turns out, I have about 60 pages with the 'Candidate Blogpost' tag in my notes, but most of them are not more than an idea. Only some of them have enough detail to turn into a post. I am going to keep this around, prune my candidate blogposts, and add my recent clippings to the mix.
Quite a few of my candidates were 'just links' according to the model, but as I am inspired by [Simon Willison](https://www.simonwillison.net), there is value in sharing links with a brief description on why I think they are relevant. Probably in a different category.
# How did I develop the skill?
# How did I develop the skill?
I was inspired by two writings:
- Jurgen De Smet asking [how do you write long form articles?](https://www.linkedin.com/posts/jurgendesmet_this-is-how-i-write-long-form-articles-these-share-7441394036222935040-JHmv).
- Jurgen De Smet asking [how do you write long form articles?](https://www.linkedin.com/posts/jurgendesmet_this-is-how-i-write-long-form-articles-these-share-7441394036222935040-JHmv).
- Chris Parsons suggested to [brief an agent for daily tasks](https://www.chrismdp.com/stop-prompting-start-briefing/), and use the _backbriefing_ loop from "The Art of Action" to improve them.
I like "The Art of Action" - detailed, yet practical. So I had a chat with a frontier model to develop a skill for a local model to surface notes that are almost finished, with some suggestions to get them over the line.
@@ -31,14 +31,14 @@ This was my initial prompt. Full chat transcript in the Further Reading section.
What I found interesting was that, maybe because I mentioned the links were in an sqlite database, claude desktop spontaneously suggested to create a bash script as part of the skill. I used to have a meta-skill to separate the deterministic parts of agent skills into scripts, but that does not seem to be necessary anymore. I prune my agent setups continuously, only keeping what is needed.
# Tradeoffs
# Tradeoffs
Initially I planned to run this as a scheduled job, but from the development chat it emerged that backbriefing (improving the skill as we run it daily) would not work if it runs scheduled.
Initially I planned to run this as a scheduled job, but from the development chat it emerged that backbriefing (improving the skill as we run it daily) would not work if it runs scheduled.
I chose a local coding agent with a local model, because I don't want to share my personal notes with a cloud service, and I thought that a smaller model would be more than powerful enough.
## Further reading
## Further reading
https://claude.ai/share/be0184d9-f2bf-41ba-b2e3-235fe9daf9fd - initial chat do develop the skill
@@ -0,0 +1,8 @@
%{
title: "Future Test Post",
author: "Test Author",
tags: ~w(test),
description: "A post scheduled for the future"
}
---
This is a future test post.
@@ -0,0 +1,72 @@
%{
title: "Scheduled Publishing & Author Dashboard",
author: "Willem van den Ende",
tags: ~w(release features),
description: "Future-dated posts stay hidden until their publish date, authors get a dashboard to track drafts and scheduled content, and registration is locked down to invited emails only."
}
---
Posts in Firehose are markdown files with a date in the filename. Until now, every published post was immediately visible. That changes today: posts with a future date are now hidden from public views until their date arrives.
This was built in a single session using an agentic dev team -- 12 issues tracked in beads, executed in three parallel phases, producing 232 tests across the blogex library and Phoenix app.
## What changed
### Future-dated posts are hidden from public views
The blog index, tag pages, RSS feeds, and Atom feeds now filter out posts where the date is after today. If you schedule a post for next Tuesday, readers won't see it until then.
![Blog index showing only past-dated posts](/images/scheduled-publishing/scheduled-blog-index.png)
But here's the key design choice: **direct URL access still works**. If you know the slug, you can view the post. This lets authors share preview links with reviewers before the publish date.
![Future post accessible by direct URL](/images/scheduled-publishing/scheduled-direct-access.png)
### Status banners for authors
When you're logged in, draft and scheduled posts show a status banner so you always know what state a post is in. Unauthenticated visitors see nothing -- no clue the post isn't "live" yet.
**Scheduled posts** show a blue banner with the target date:
![Scheduled banner showing target publish date](/images/scheduled-publishing/scheduled-banner-future.png)
**Draft posts** (unpublished) show an amber banner:
![Draft banner on unpublished post](/images/scheduled-publishing/scheduled-banner-draft.png)
### Editor dashboard
A new LiveView at `/editor/dashboard` gives authors a unified view of all non-live content across every blog. Two tabs: drafts and scheduled posts. Scheduled posts show a "days until live" countdown.
![Dashboard drafts tab](/images/scheduled-publishing/scheduled-dashboard.png)
![Dashboard scheduled tab with countdown](/images/scheduled-publishing/scheduled-dashboard-scheduled.png)
The dashboard requires authentication -- unauthenticated users are redirected to the login page.
### Authentication and registration gating
We added `mix phx.gen.auth` for session-based authentication with magic links and password login. Login and registration pages are accessible by direct URL only -- they're intentionally not linked from the public navigation.
Registration is gated to a single email via the `ALLOWED_REGISTRATION_EMAIL` environment variable. Anyone else gets a polite rejection:
![Registration rejected for non-matching email](/images/scheduled-publishing/scheduled-registration-rejected.png)
When the environment variable isn't set, registration is disabled entirely. A demo user (`demo@example.com`) is seeded in dev for local testing.
## How it was built
The feature was planned as an [Allium specification](https://github.com/your-org/allium) with surfaces, rules, and domain entities, then broken into 12 beads (issues) across three phases:
1. **Scheduled posts** (5 beads): date filtering in blogex, unfiltered direct access, feed/router verification
2. **Authentication** (3 beads): phx.gen.auth scaffolding, registration gating, dev seed
3. **Dashboard** (4 beads): post visibility helpers, unfiltered registry access, LiveView dashboard, status banners
All 12 beads were executed with parallel agentic workers in isolated git worktrees, then merged and integrated on main. The demo caught one bug (auth check using `current_user` instead of `current_scope`) which was fixed before this post.
## By the numbers
- **232 tests** passing (89 blogex + 143 Phoenix app)
- **12 beads** planned, executed, and closed
- **3 phases** run with parallel workers
- **0 compiler warnings**
@@ -0,0 +1,30 @@
defmodule Firehose.Repo.Migrations.CreateUsersAuthTables do
use Ecto.Migration
def change do
execute "CREATE EXTENSION IF NOT EXISTS citext", ""
create table(:users) do
add :email, :citext, null: false
add :hashed_password, :string
add :confirmed_at, :utc_datetime
timestamps(type: :utc_datetime)
end
create unique_index(:users, [:email])
create table(:users_tokens) do
add :user_id, references(:users, on_delete: :delete_all), null: false
add :token, :binary, null: false
add :context, :string, null: false
add :sent_to, :string
add :authenticated_at, :utc_datetime
timestamps(type: :utc_datetime, updated_at: false)
end
create index(:users_tokens, [:user_id])
create unique_index(:users_tokens, [:context, :token])
end
end
+10
View File
@@ -9,3 +9,13 @@
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.
if Mix.env() == :dev do
alias Firehose.Accounts
# Create demo user if not already present
unless Accounts.get_user_by_email("demo@example.com") do
{:ok, user} = Accounts.register_user(%{email: "demo@example.com"})
{:ok, {_user, _tokens}} = Accounts.update_user_password(user, %{password: "password123!"})
end
end
Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB