authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-04 11:44:54-08:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-01-14 01:08:17-08:00
log3d5576a112f56848e5f3600a7101d9a4bbc9574f
tree75cdf3067a12d43526a795590afbcd4fcd205c27
parenta3a4ad4523a80f22cd8457091a6f2be771ea782e
signature Commit is signed but in an unrecognized format.

feat: add "activity log" page


6 files changed, 220 insertions(+), 6 deletions(-)

framework/mdx-hack.tsx+1-1
......@@ -3,7 +3,7 @@ export function useMDXComponents() {
33 pre: async (
44 { filename, whitespace, tint, content: [, , { className, content }] },
55 ) => {
6 const lang = className.split("-")?.[1] ?? null;
6 const lang = className?.split("-")?.[1] ?? null;
77 const code = ["tsx", "jsx", "json", "html", "ts", "js"].includes(lang)
88 ? await highlight.highlightCode(
99 content.trim(),
package-lock.json+4-3
......@@ -25,6 +25,7 @@
2525 },
2626 "devDependencies": {
2727 "@types/node": "24.0.10",
28 "baseline-browser-mapping": "^2.9.14",
2829 "typescript": "5.8.3"
2930 }
3031 },
......@@ -1454,9 +1455,9 @@
14541455 }
14551456 },
14561457 "node_modules/baseline-browser-mapping": {
1457 "version": "2.8.10",
1458 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.10.tgz",
1459 "integrity": "sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==",
1458 "version": "2.9.14",
1459 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
1460 "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
14601461 "license": "Apache-2.0",
14611462 "bin": {
14621463 "baseline-browser-mapping": "dist/cli.js"
package.json+1
......@@ -21,6 +21,7 @@
2121 },
2222 "devDependencies": {
2323 "@types/node": "24.0.10",
24 "baseline-browser-mapping": "^2.9.14",
2425 "typescript": "5.8.3"
2526 },
2627 "imports": {
src/pages/index.marko+3-2
......@@ -26,12 +26,13 @@ export const meta = {
2626 <p>song: <a href="/waterfalls">waterfalls</a> (2025-01-01)</p>
2727 <h2>things</h2>
2828 <p><a href="/q+a">questions and answers</a></p>
29 <p><a href="/log">recent activity</a></p>
2930 <p><a href="/file">file browser</a></p>
3031 <p><a href="/subscribe">mailing list</a></p>
3132 <h2>links</h2>
32 <div style="display:flex;gap:1rem;position:relative;z-index:2">
33 <div style="display:flex;gap:1.5rem;position:relative;z-index:2">
3334 <a href="mailto:hello@paperclover.net">email me</a>
34 <a href="/rss.xml">rss feed</a>
35 <!-- <a href="/rss.xml">rss feed</a> -->
3536 <a href="https://ko-fi.com/paper_clover">donate</a>
3637 </div>
3738 <h1>paper clover</h1>
src/pages/log.css created+16
......@@ -0,0 +1,16 @@
1h1 {
2 margin-top: 0;
3}
4body {
5 background-color: #ccddee;
6}
7pre:has(code) {
8 border: 1px solid black;
9 border-radius: 4px;
10 padding: 1rem;
11 overflow-x: auto;
12}
13blockquote {
14 border-left: 4px solid black;
15 padding-left: 8px;
16}
src/pages/log.mdx created+195
......@@ -0,0 +1,195 @@
1import './log.css';
2export default ({ content }) => <main>{content}</main>
3export const meta = { title: 'clover\'s log' };
4
5[go back to the home page](/)
6
7[git]: https://git.paperclover.net/
8[home infra]: https://git.paperclover.net/clo/home-infra
9[name paint bot]: https://git.paperclover.net/clo/discord-name-painter
10[todo tracker]: https://git.paperclover.net/clo/todo-tracker
11[ts lie detector]: https://git.paperclover.net/clo/ts-lie-detector
12[progress.ts]: https://jsr.io/@clo/lib/doc/progress.ts
13
14# clover's log
15
16## 2026-01-12
17
18tags: [album]
19
20i started more music work. i've gotten better at lyric writing, phrasing this
21new song as a sort of "adventure". felt for one of the first times that i was
22doing worldbuilding in a song. the imagery is that good.
23
24## 2026-01-11
25
26tags: [progress.ts], [todo tracker]
27
28i finished streaming io on the @clo/lib/[progress.ts]. very proud of it. my git
29commits describe the tech better than me reiterating.
30
31> ## feat(lib/progress): implement streaming wire protocol
32> resolves `#47`
33>
34> `encodeByteStream` converts these events into a `ReadableStream`. by
35> batching events together, the stream contents remain small, that way the
36> code that constructs progress nodes do not have to worry about calling
37> many setters at once, it gets debounced be the serializer. stream
38> backpressure causes larger time-gaps to be batched (smaller). this
39> enables servers to respond with rich progress.
40>
41> ```
42> const root = new progress.Root();
43> doActionWithProgress(root).then(root.end, root.error);
44> // streaming clients indicate a header
45> if (req.headers.get("Accept")?.includes(progress.contentType))
46> return new Response(progress.encodeByteStream(root), {
47> headers: { 'Content-Type': progress.contentType },
48> });
49> // to support non-streaming clients
50> return Response.json(await root.asPromise());
51> ```
52>
53> and `decodeByteStream` on the client:
54>
55> ```
56> const output = document.getElementById("output");
57> const res = await fetch(...);
58> if (!res.ok) throw ...;
59> const root = new progress.Root();
60> root.on("change", (active) => {
61> output.innerText = ansi.strip(progress.formatAnsi(
62> performance.now(),
63> active,
64> ));
65> });
66> const result = await progress.decodeByteStream(res.body, root);
67> output.innerText = JSON.stringify(result);
68> ```
69>
70> there is currently no document bindings, but i plan to. additionally, a
71> React hook is very trivial to implement for this -- but that is
72> unplanned for this repository. for transports that require JSON or
73> UTF-8, there is `encodeEventStream` which returns a `ReadableStream` of
74> JSON objects which can be compressed at the developer's discretion.
75
76> ## feat(lib/progress): headless rendering + time estimation
77> node signaling is done by providing a `progress.Root` to every node,
78> dispatching events to it when the node changes. the root is connected to
79> an observer to construct a UI out of it. there are two apis planned:
80>
81> - `attachToScreen` binds a root to a TTY screen (via the log.Widget API).
82> the primary use of this is to implement the top level `progress.start`.
83>
84> - a serialization system that allows transmitting a `Root` over a wire.
85> this commit was going to include this but it is an unexpectedly large
86> component.
87>
88> - potentially a browser binding like `attachToDocument`. this will not
89> be added in this patch.
90>
91> additionally, resolves #33 by implementing `estimatedTime`
92
93i also did a large part of the work to create a "code todo tracking" tool. i
94would say it's about half done, since the second half is simply fixing all of
95the little bugs there are. most of this code is currently ai-generated, but
96with me manually coming in to write interfaces and the modular program
97architecture. then i synthesize the code and the tests. this was basically just
98going on ambiently while [progress.ts] was in progress.
99
100## 2026-01-09
101
102tags: [home infra]
103
104finished SSO sub-project. im happy with the setup i used to protect internal
105services, such as pgadmin and qbittorrent. it's a caddy snippet that i can
106re-use very easily.
107
108```
109(reverse_proxy_auth) {
110 handle /snow.oauth2/* {
111 reverse_proxy "http://forward-auth" {
112 header_up X-Real-IP {remote_host}
113 header_up X-Forwarded-Uri {uri}
114 }
115 }
116 handle {
117 forward_auth "http://forward-auth" {
118 uri /snow.oauth2/auth
119 header_up X-Real-IP {remote_host}
120 @error status 401
121 handle_response @error {
122 redir * /snow.oauth2/sign_in?rd={scheme}://{host}{uri}
123 }
124 @valid_group header X-Auth-Request-Groups *role:{args[1]}*
125 handle_response @valid_group {
126 method {method}
127 rewrite {uri}
128 reverse_proxy {args[0]} {
129 header_up Cookie ([^;]*?)\s*_oauth2_proxy_\d=[^;]*(;?.*) "$1$2"
130 {block}
131 }
132 }
133 handle_response {
134 rewrite /403.html
135 file_server {
136 status 403
137 root /etc/caddy
138 }
139 }
140 }
141 }
142}
143
144# usage
145pg.{$HOME_DOMAIN} {
146 import reverse_proxy_auth "http://pgadmin" admin
147}
148qbt.{$HOME_DOMAIN} {
149 import reverse_proxy_auth "http://qbittorrent" media-manage
150}
151```
152
153
154## 2026-01-04
155
156tags: [home infra]
157
158working on SSO for my internal services. for context, i have about 12
159self-hosted services running, half of which i allow my friends to access.
160currently, this is done through manually creating an account on such service
161(jellyfin, forgejo), but many are done through a caddy rule. in the interest of
162making my password manager less confused (ip vs domain, subdomain etc), i'm
163slowly reducing this setup to a single sign in page.
164
165to do this, i am using https://keycloak.org, which supports openid connect
166(how i will configure forgejo and jellyfin), as well as a separate service to
167provide forward auth proxying (how i protect services like copyparty,
168syncthing, pgadmin, and many more). i tried authelia beforehand, but i really
169do not recommend them due to how hard it is to configure, passkeys being
170annoying to setup, and limited themes. i also dont recommend authentik, but i
171couldnt figure out how to even start using it after i installed it.
172
173keycloak is a bit stupid on config. as all the config lies in the postgres
174database, i can't use a config file to setup the primary realm. so instead, i
175have this huge python script to use the API to upsert the configuration in.
176this works pretty well, and means that for locally running the infrastructure
177for testing, i can get the config to be the same (useful if you brick
178keycloak, which is pretty easy to do).
179
180## 2026-01-02
181
182tags: [home infra], [git], [name paint bot] show
183
184i deleted all my github repositories except four: my "readme", a bug
185reproduction repo, the mirror for [ts lie detector], and a shared private repo
186with someone that is load bearing. in this process, i've moved all the projects
187to my [forgejo instance][git].
188
189with this, [name paint bot], one of my few remaining projects that is still
190active, moves to that forgejo instance using their github migrator. some of my
191private projects, like my pet scripting language, were migrated as well. it
192feels more alive on my site because of the theming and per-repo icons.
193
194after a year of forgejo, i am really happy with how it treats me.
195