diff --git a/framework/mdx-hack.tsx b/framework/mdx-hack.tsx
index 927416e6e57dfadea28f4540a996db8a239461cf..ffbe656306588a39ed90348050099dccca0a2088 100644
--- a/framework/mdx-hack.tsx
+++ b/framework/mdx-hack.tsx
@@ -3,7 +3,7 @@ export function useMDXComponents() {
pre: async (
{ filename, whitespace, tint, content: [, , { className, content }] },
) => {
- const lang = className.split("-")?.[1] ?? null;
+ const lang = className?.split("-")?.[1] ?? null;
const code = ["tsx", "jsx", "json", "html", "ts", "js"].includes(lang)
? await highlight.highlightCode(
content.trim(),
diff --git a/package-lock.json b/package-lock.json
index 4c8a6423c0be2cd6103a593308313280daae0dfb..e5e8344f03d06c86bbd8c97a499f55d88dd7a16f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -25,6 +25,7 @@
},
"devDependencies": {
"@types/node": "24.0.10",
+ "baseline-browser-mapping": "^2.9.14",
"typescript": "5.8.3"
}
},
@@ -1454,9 +1455,9 @@
}
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.10",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz",
- "integrity": "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==",
+ "version": "2.9.14",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
+ "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
diff --git a/package.json b/package.json
index 969236b5906e184ba0fef59bb1ebb9df50bbbf77..e9f9eba755cc2610f54e21cc837c4dcdaebdc7a9 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
},
"devDependencies": {
"@types/node": "24.0.10",
+ "baseline-browser-mapping": "^2.9.14",
"typescript": "5.8.3"
},
"imports": {
diff --git a/src/pages/index.marko b/src/pages/index.marko
index fff9561d9678931f2309d2a2f267094a2bd2dc19..35db1efdc1a97160c5f3e4db23844da5724186d2 100644
--- a/src/pages/index.marko
+++ b/src/pages/index.marko
@@ -26,12 +26,13 @@ export const meta = {
song: waterfalls (2025-01-01)
things
questions and answers
+ recent activity
file browser
mailing list
links
-
+
paper clover
diff --git a/src/pages/log.css b/src/pages/log.css
new file mode 100644
index 0000000000000000000000000000000000000000..0f17f9eb7446ad8be34b04b77ce1641272536775
--- /dev/null
+++ b/src/pages/log.css
@@ -0,0 +1,16 @@
+h1 {
+ margin-top: 0;
+}
+body {
+ background-color: #ccddee;
+}
+pre:has(code) {
+ border: 1px solid black;
+ border-radius: 4px;
+ padding: 1rem;
+ overflow-x: auto;
+}
+blockquote {
+ border-left: 4px solid black;
+ padding-left: 8px;
+}
diff --git a/src/pages/log.mdx b/src/pages/log.mdx
new file mode 100644
index 0000000000000000000000000000000000000000..4158f82557c6a032c237f95f307fd7e083a706ce
--- /dev/null
+++ b/src/pages/log.mdx
@@ -0,0 +1,195 @@
+import './log.css';
+export default ({ content }) =>
{content}
+export const meta = { title: 'clover\'s log' };
+
+[go back to the home page](/)
+
+[git]: https://git.paperclover.net/
+[home infra]: https://git.paperclover.net/clo/home-infra
+[name paint bot]: https://git.paperclover.net/clo/discord-name-painter
+[todo tracker]: https://git.paperclover.net/clo/todo-tracker
+[ts lie detector]: https://git.paperclover.net/clo/ts-lie-detector
+[progress.ts]: https://jsr.io/@clo/lib/doc/progress.ts
+
+# clover's log
+
+## 2026-01-12
+
+tags: [album]
+
+i started more music work. i've gotten better at lyric writing, phrasing this
+new song as a sort of "adventure". felt for one of the first times that i was
+doing worldbuilding in a song. the imagery is that good.
+
+## 2026-01-11
+
+tags: [progress.ts], [todo tracker]
+
+i finished streaming io on the @clo/lib/[progress.ts]. very proud of it. my git
+commits describe the tech better than me reiterating.
+
+> ## feat(lib/progress): implement streaming wire protocol
+> resolves `#47`
+>
+> `encodeByteStream` converts these events into a `ReadableStream`. by
+> batching events together, the stream contents remain small, that way the
+> code that constructs progress nodes do not have to worry about calling
+> many setters at once, it gets debounced be the serializer. stream
+> backpressure causes larger time-gaps to be batched (smaller). this
+> enables servers to respond with rich progress.
+>
+> ```
+> const root = new progress.Root();
+> doActionWithProgress(root).then(root.end, root.error);
+> // streaming clients indicate a header
+> if (req.headers.get("Accept")?.includes(progress.contentType))
+> return new Response(progress.encodeByteStream(root), {
+> headers: { 'Content-Type': progress.contentType },
+> });
+> // to support non-streaming clients
+> return Response.json(await root.asPromise());
+> ```
+>
+> and `decodeByteStream` on the client:
+>
+> ```
+> const output = document.getElementById("output");
+> const res = await fetch(...);
+> if (!res.ok) throw ...;
+> const root = new progress.Root();
+> root.on("change", (active) => {
+> output.innerText = ansi.strip(progress.formatAnsi(
+> performance.now(),
+> active,
+> ));
+> });
+> const result = await progress.decodeByteStream(res.body, root);
+> output.innerText = JSON.stringify(result);
+> ```
+>
+> there is currently no document bindings, but i plan to. additionally, a
+> React hook is very trivial to implement for this -- but that is
+> unplanned for this repository. for transports that require JSON or
+> UTF-8, there is `encodeEventStream` which returns a `ReadableStream` of
+> JSON objects which can be compressed at the developer's discretion.
+
+> ## feat(lib/progress): headless rendering + time estimation
+> node signaling is done by providing a `progress.Root` to every node,
+> dispatching events to it when the node changes. the root is connected to
+> an observer to construct a UI out of it. there are two apis planned:
+>
+> - `attachToScreen` binds a root to a TTY screen (via the log.Widget API).
+> the primary use of this is to implement the top level `progress.start`.
+>
+> - a serialization system that allows transmitting a `Root` over a wire.
+> this commit was going to include this but it is an unexpectedly large
+> component.
+>
+> - potentially a browser binding like `attachToDocument`. this will not
+> be added in this patch.
+>
+> additionally, resolves #33 by implementing `estimatedTime`
+
+i also did a large part of the work to create a "code todo tracking" tool. i
+would say it's about half done, since the second half is simply fixing all of
+the little bugs there are. most of this code is currently ai-generated, but
+with me manually coming in to write interfaces and the modular program
+architecture. then i synthesize the code and the tests. this was basically just
+going on ambiently while [progress.ts] was in progress.
+
+## 2026-01-09
+
+tags: [home infra]
+
+finished SSO sub-project. im happy with the setup i used to protect internal
+services, such as pgadmin and qbittorrent. it's a caddy snippet that i can
+re-use very easily.
+
+```
+(reverse_proxy_auth) {
+ handle /snow.oauth2/* {
+ reverse_proxy "http://forward-auth" {
+ header_up X-Real-IP {remote_host}
+ header_up X-Forwarded-Uri {uri}
+ }
+ }
+ handle {
+ forward_auth "http://forward-auth" {
+ uri /snow.oauth2/auth
+ header_up X-Real-IP {remote_host}
+ @error status 401
+ handle_response @error {
+ redir * /snow.oauth2/sign_in?rd={scheme}://{host}{uri}
+ }
+ @valid_group header X-Auth-Request-Groups *role:{args[1]}*
+ handle_response @valid_group {
+ method {method}
+ rewrite {uri}
+ reverse_proxy {args[0]} {
+ header_up Cookie ([^;]*?)\s*_oauth2_proxy_\d=[^;]*(;?.*) "$1$2"
+ {block}
+ }
+ }
+ handle_response {
+ rewrite /403.html
+ file_server {
+ status 403
+ root /etc/caddy
+ }
+ }
+ }
+ }
+}
+
+# usage
+pg.{$HOME_DOMAIN} {
+ import reverse_proxy_auth "http://pgadmin" admin
+}
+qbt.{$HOME_DOMAIN} {
+ import reverse_proxy_auth "http://qbittorrent" media-manage
+}
+```
+
+
+## 2026-01-04
+
+tags: [home infra]
+
+working on SSO for my internal services. for context, i have about 12
+self-hosted services running, half of which i allow my friends to access.
+currently, this is done through manually creating an account on such service
+(jellyfin, forgejo), but many are done through a caddy rule. in the interest of
+making my password manager less confused (ip vs domain, subdomain etc), i'm
+slowly reducing this setup to a single sign in page.
+
+to do this, i am using https://keycloak.org, which supports openid connect
+(how i will configure forgejo and jellyfin), as well as a separate service to
+provide forward auth proxying (how i protect services like copyparty,
+syncthing, pgadmin, and many more). i tried authelia beforehand, but i really
+do not recommend them due to how hard it is to configure, passkeys being
+annoying to setup, and limited themes. i also dont recommend authentik, but i
+couldnt figure out how to even start using it after i installed it.
+
+keycloak is a bit stupid on config. as all the config lies in the postgres
+database, i can't use a config file to setup the primary realm. so instead, i
+have this huge python script to use the API to upsert the configuration in.
+this works pretty well, and means that for locally running the infrastructure
+for testing, i can get the config to be the same (useful if you brick
+keycloak, which is pretty easy to do).
+
+## 2026-01-02
+
+tags: [home infra], [git], [name paint bot] show
+
+i deleted all my github repositories except four: my "readme", a bug
+reproduction repo, the mirror for [ts lie detector], and a shared private repo
+with someone that is load bearing. in this process, i've moved all the projects
+to my [forgejo instance][git].
+
+with this, [name paint bot], one of my few remaining projects that is still
+active, moves to that forgejo instance using their github migrator. some of my
+private projects, like my pet scripting language, were migrated as well. it
+feels more alive on my site because of the theming and per-repo icons.
+
+after a year of forgejo, i am really happy with how it treats me.
+