<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>SpiralOut</title>
    <link>https://spiralnet.io/</link>
    <description>Transmissions from SpiralOut: tools, self-hosting, networking and the small web.</description>
    <language>en</language>
    <lastBuildDate>Sun, 25 Oct 2026 12:00:00 +0000</lastBuildDate>
    <atom:link href="https://spiralnet.io/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>Coolify: Your Own Heroku on a $5 Box</title>
      <link>https://spiralnet.io/blog/coolify.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/coolify.html</guid>
      <pubDate>Sun, 25 Oct 2026 12:00:00 +0000</pubDate>
      <description>A look at Coolify, the open-source, self-hostable alternative to Heroku, Netlify and Vercel that deploys apps, databases and 360+ services to your own servers over SSH.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/coollabsio/coolify" rel="noopener noreferrer">Coolify</a> is an open-source, self-hostable alternative to Heroku, Netlify and Vercel. You get the push-to-deploy convenience of a cloud platform, running on servers you own. In the README's words: "You only need an SSH connection."</p>

					<p>It's on GitHub Trending today with about 64,000 stars. It's licensed under <strong>Apache-2.0</strong>, and the project pledges no features behind a paywall.</p>

					<h2>What it does</h2>
					<p>Point Coolify at a server, whether that's a VPS, a bare-metal box or even a Raspberry Pi, and it becomes a deployment platform. The README's feature list covers most of what you'd pay a PaaS for:</p>
					<ul>
						<li><strong>Deploy any app</strong> from GitHub, GitLab, Bitbucket or Gitea, built with Nixpacks, Railpack, a Dockerfile, Docker Compose or a prebuilt image.</li>
						<li><strong>Run databases and services:</strong> managed databases plus more than 360 one-click services, with persistent storage and generated credentials.</li>
						<li><strong>Automate deployments:</strong> deploy on every Git push, spin up previews for pull requests, trigger builds by webhook, and roll back to earlier images.</li>
						<li><strong>Handle networking:</strong> custom domains, automatic HTTPS, reverse proxying, health checks and container networks.</li>
						<li><strong>Look after it:</strong> backups for databases and storage, scheduled tasks, secrets, notifications, logs, and terminals into running containers.</li>
						<li><strong>Script it:</strong> a dashboard, an API, a CLI, an MCP server for AI agents, and team-based access controls.</li>
					</ul>

					<h2>How it works</h2>
					<p>Coolify is a control panel that manages your servers over SSH and runs your workloads as Docker containers. The recommended setup puts Coolify on one server and deploys your apps to one or more others, so your control plane and your workloads don't compete for resources. A single small server works for tinkering.</p>
					<p>The design choice I like most is that it doesn't lock you in. Your configuration lives on your own servers, and the README says that if you stop using Coolify, "your running resources continue to work and remain manageable." Your apps don't stop when the dashboard goes away.</p>

					<h2>Trying it</h2>
					<p>Installing it on a supported server is one command:</p>
<pre><code>curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash</code></pre>
					<p>The project links straight to the install script so you can read it before piping it into a root shell, which you should. The <a href="https://coolify.io/docs/installation" rel="noopener noreferrer">installation guide</a> covers supported systems and requirements. Once it's running, you open the dashboard in a browser, connect a Git provider or pick a one-click service, and deploy.</p>
					<p>If you'd rather not maintain the Coolify server itself, there's Coolify Cloud, a paid hosted control panel that still deploys to your own servers. It adds high availability, email notifications and extra support.</p>

					<h2>The fine print</h2>
					<ul>
						<li><strong>You're the ops team now.</strong> Coolify takes care of deploys, certificates and backups, but you still own the servers: OS updates, disk space, firewalls and what happens when hardware dies. That's the trade you make for leaving a managed platform.</li>
						<li><strong>Read the install script.</strong> One-line installs are convenient, but the script sets up Docker and system services as root. The README invites you to review it, so take it up on that.</li>
						<li><strong>Donation-funded.</strong> Coolify stays free without paywalled features by relying on sponsors, donations and the Cloud offering. That's worth knowing if you're betting a business on it, and it's a good reason to chip in if you do.</li>
					</ul>

					<h2>Who it's for</h2>
					<p>Coolify is for indie developers tired of PaaS bills that grow faster than their side projects, small teams who want Heroku-style workflows on a couple of cheap VPSes, homelabbers who want one dashboard for everything they run, and anyone who wants their apps to keep working on their own hardware no matter what happens to a vendor.</p>

					<p>The <a href="https://github.com/coollabsio/coolify" rel="noopener noreferrer">GitHub repo</a> and <a href="https://coolify.io/" rel="noopener noreferrer">coolify.io</a> are the places to start, and it's in the <a href="https://spiralnet.io/resources/">resource index</a> now.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>Microduck: A Robot Duck That Learned to Walk</title>
      <link>https://spiralnet.io/blog/microduck.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/microduck.html</guid>
      <pubDate>Sat, 24 Oct 2026 12:00:00 +0000</pubDate>
      <description>A look at Microduck, a tiny open-source biped robot duck from Pollen Robotics that walks on reinforcement learning policies, with a Rust daemon stack and a full simulator.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/pollen-robotics/microduck" rel="noopener noreferrer">Microduck</a> is a tiny biped robot from Pollen Robotics: about 25 cm tall, 800 g, shaped like a duck, and it walks using neural network policies trained in simulation. The repo describes itself as "the duck's brain," meaning all the software that runs on the robot.</p>

					<p>It's on GitHub Trending today with about 10,800 stars, and the software is licensed under <strong>Apache-2.0</strong>.</p>

					<h2>What it does</h2>
					<p>The README lists the tricks:</p>
					<ul>
						<li><strong>It walks.</strong> Pick up a gamepad and drive it around.</li>
						<li><strong>It rolls.</strong> Put wheels on, ask for roller mode, and it loads a different policy.</li>
						<li><strong>It picks things up.</strong> Beak to the floor, one button.</li>
						<li><strong>It gets back up.</strong> Knock it over and it stands itself up.</li>
					</ul>
					<p>It also sits, kicks a ball, and quacks "in a voice that is its own." The command-line cheat sheet even lists "chorale" and "theremin" commands, which tells you something about the people who built it.</p>

					<h2>How it works</h2>
					<p>The movement comes from <strong>reinforcement learning</strong>. Instead of hand-coding each joint for a walking gait, Pollen trains policies in the MuJoCo physics simulator with PPO, a standard RL algorithm. It uses domain randomisation, meaning the simulated physics is varied during training so the policy holds up against the messiness of a real robot. The trained policy is then exported to ONNX and loaded onto the duck. That training pipeline lives in a sister repo, <a href="https://github.com/pollen-robotics/microduck_rl" rel="noopener noreferrer">microduck_rl</a>.</p>
					<p>On the robot itself, a Rockchip RK3566 runs a <strong>50 Hz control loop</strong> that drives fifteen servos from those policies. The software is written in Rust, with no framework, as a set of small daemons, each owning one job:</p>
					<ul>
						<li><code>robotd</code> owns the control loop and the motor bus.</li>
						<li><code>updaterd</code> installs signed releases and rolls them back if the robot comes up unhealthy.</li>
						<li><code>configd</code> handles wifi and identity, <code>btd</code> is the Bluetooth path a phone uses, and <code>padd</code> reads the gamepad.</li>
						<li><code>mediad</code> streams the camera over WebRTC, and <code>tofd</code> serves the depth sensor.</li>
					</ul>
					<p>They all speak one JSON-RPC contract over Unix sockets. The phone app, the console, the gamepad and your own scripts all send exactly the same calls, so anything the app can do, a script can too.</p>
					<p>The update system deserves a mention. The README says every update is "verified, health-gated and reversible," which is exactly what you want from a small robot that would otherwise be one bad firmware flash away from a paperweight.</p>

					<h2>Trying it</h2>
					<p>The robot itself is sold through Pollen's <a href="https://pollen-robotics.com/microduck" rel="noopener noreferrer">Microduck page</a>. If you have one, <code>robotctl</code> is the main tool, and the cheat sheet in the repo covers driving, configuration, voice, wifi, updates and logs. There's also <code>duckctl</code>, which drives the robot from a laptop over Bluetooth, with no network or ssh needed.</p>
					<p>You don't need a duck to start, though. The repo includes a simulated duck that runs the real robot daemons against a body in MuJoCo:</p>
<pre><code>scripts/duck-sim</code></pre>
					<p>It runs either one duck in a window or four as separate machines you can log into. That's a neat way to hack on the software, or just to see how a real robot stack is put together. The docs also cover setting up a dev board from scratch and pushing your own branch builds to a robot in about a minute.</p>

					<h2>The fine print</h2>
					<ul>
						<li><strong>This repo is the software.</strong> It's the brain, not a parts list, and the hardware comes from Pollen. The simulator lets you work on the code without buying anything.</li>
						<li><strong>Training lives elsewhere.</strong> If you want to teach the duck new moves, you'll be working in microduck_rl with MuJoCo and PPO. That means real compute and some reinforcement learning background.</li>
						<li><strong>It's honest about its rough edges.</strong> The repo keeps a <code>docs/project/</code> folder of "what has gone wrong and what would close it," alongside design notes explaining why things are the way they are. That's rare and useful.</li>
					</ul>

					<h2>Who it's for</h2>
					<p>Microduck is for robotics tinkerers, anyone curious how sim-to-real reinforcement learning works in practice, Rust developers who want to see a clean embedded daemon architecture, and frankly anyone who has wanted a small robot duck that gets back up when you knock it over. Even if you never buy one, the design docs and the simulator make it one of the more approachable real robot codebases out there.</p>

					<p>The <a href="https://github.com/pollen-robotics/microduck" rel="noopener noreferrer">GitHub repo</a> and the <a href="https://pollen-robotics.com/microduck" rel="noopener noreferrer">Microduck page</a> are the places to start, and it's in the <a href="https://spiralnet.io/resources/">resource index</a> now.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>OpenShell: A Sandbox With a Bouncer for Your AI Agents</title>
      <link>https://spiralnet.io/blog/openshell.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/openshell.html</guid>
      <pubDate>Fri, 23 Oct 2026 12:00:00 +0000</pubDate>
      <description>A look at NVIDIA OpenShell, an open-source runtime that sandboxes AI agents with kernel-level policy enforcement and formally verified permission changes.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/NVIDIA/OpenShell" rel="noopener noreferrer">OpenShell</a> is NVIDIA's open-source runtime for running AI agents without handing them the keys to everything. The README sums up the problem: agents are most useful when they can "read files, install packages, call APIs, and use credentials," and that's exactly what makes them dangerous. OpenShell lets you write down what each agent may touch, then enforces it.</p>

					<p>It's on GitHub Trending today with about 18,700 stars, and it's licensed under <strong>Apache-2.0</strong>.</p>

					<h2>What it does</h2>
					<p>Every agent runs in its own isolated sandbox. A policy you write decides which files it can read and write, which processes and system calls it can use, and which network hosts and APIs it can reach. When an agent wants something new, say a package registry it hasn't touched before, the change goes through an approval step instead of just happening.</p>
					<p>The part I like best is how it handles credentials. Agents never see your real API keys or tokens. OpenShell adds credentials to outgoing requests only when those requests are headed for endpoints you've approved. An agent that gets talked into sending its secrets somewhere has no secrets to send.</p>

					<h2>How it works</h2>
					<p>The README describes two layers.</p>
					<p><strong>Kernel-level enforcement.</strong> OpenShell instruments the kernel to check every file access, system call and network connection at runtime. Network traffic passes through a policy check before it leaves the sandbox. These rules aren't suggestions in the agent's system prompt. They're enforced whether the agent cooperates or not.</p>
					<p><strong>Formally verified policy changes.</strong> When a policy changes, OpenShell uses formal verification to work out what new access the change would actually grant, flagging things like "this lets the agent reach a new host with credentials" or "this allows a new API method." Risky changes wait for a human to review them. The docs call these tools the policy advisor and the prover.</p>
					<p>Around that sits a <strong>gateway</strong>, the control plane that manages sandboxes, policies and access. It can run locally or on Kubernetes via Helm. Sandboxes run under Docker, Podman or host virtualization, and the docs cover custom images, GPUs and lifecycle management. Credentials are handled by "providers," which also route model inference traffic.</p>

					<h2>Trying it</h2>
					<p>You need Linux, macOS on Apple Silicon, or Windows with WSL 2 (experimental), plus Docker, Podman or host virtualization. Then:</p>
<pre><code>curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
openshell sandbox create --name demo</code></pre>
					<p>That installs the CLI and a local gateway, then creates a sandbox. The default image is a minimal Ubuntu with no agent inside, so it's an empty room until you put something in it. The "Run Your First Agent" guide runs the OpenCode coding agent against a free OpenRouter model and walks through approving new access as the agent asks for it.</p>
					<p>There are SDKs for driving a gateway from your own code in Python (<code>uv add openshell</code>), TypeScript, Go and Rust. There's also a set of agent skills that teach coding assistants to use the CLI and write policies:</p>
<pre><code>npx skills add NVIDIA/OpenShell</code></pre>

					<h2>The fine print</h2>
					<ul>
						<li><strong>It's young.</strong> The 0.1.x series just introduced a stable release cadence, new isolation primitives and new APIs, with an upgrade guide for people coming from earlier builds. Expect things to keep shifting.</li>
						<li><strong>Telemetry is on by default.</strong> The project says it collects only anonymous operational counts, with no prompts, paths, hostnames, credentials or model names. You can turn it off with <code>OPENSHELL_TELEMETRY_ENABLED=false</code> on the gateway, or compile it out entirely.</li>
						<li><strong>Kubernetes needs real network policy.</strong> If you deploy the gateway on a cluster, your CNI has to enforce <code>NetworkPolicy</code>, or the network rules don't mean much.</li>
						<li><strong>A sandbox is only as good as its policy.</strong> OpenShell gives you the enforcement and the review tooling, but deciding what an agent should be allowed to do is still your job.</li>
					</ul>

					<h2>Who it's for</h2>
					<p>OpenShell is for anyone letting AI agents run unattended: teams running coding agents in CI, companies running fleets of agents that touch internal systems, and individuals who want to try an agent without giving it their home directory and every API key on the machine. If you've ever approved an agent's shell command and hoped for the best, this replaces hope with a policy.</p>

					<p>The <a href="https://github.com/NVIDIA/OpenShell" rel="noopener noreferrer">GitHub repo</a> and the <a href="https://docs.nvidia.com/openshell/latest/" rel="noopener noreferrer">documentation</a> are the places to start, and it's in the <a href="https://spiralnet.io/resources/">resource index</a> now.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>Twenty: A CRM You Version Like Code</title>
      <link>https://spiralnet.io/blog/twenty.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/twenty.html</guid>
      <pubDate>Wed, 21 Oct 2026 12:00:00 +0000</pubDate>
      <description>A look at Twenty, the open-source, self-hostable CRM that lets you define objects, views and workflows as code.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/twentyhq/twenty" rel="noopener noreferrer">Twenty</a> bills itself as "the open alternative to Salesforce, designed for AI." It's a CRM for tracking contacts, companies, deals and everything else a sales or ops team keeps tabs on. The difference is that you can host it yourself and extend it with code that lives in your own repo.</p>

					<p>It's on GitHub Trending today with about 59,000 stars and nearly 800 contributors. It ships fast, too: v2.50.0 landed yesterday. The core is <strong>AGPL-3.0</strong>, with some important exceptions covered in the fine print below.</p>

					<h2>What it does</h2>
					<p>Twenty gives you the usual CRM building blocks: records for people, companies and opportunities, customizable views like tables and kanban boards, workflows that automate the repetitive parts, and AI agents that work inside the data. The README's pitch is that it's "the CRM you build, ship, and version like the rest of your stack."</p>
					<p>That last part is the real idea. Most CRMs are customized by clicking around an admin panel until nobody remembers why a field exists. Twenty lets you define your data model and logic as code, review it in a pull request, and deploy it like any other software.</p>

					<h2>How it works</h2>
					<p>Customizations are packaged as "apps." You scaffold one with the Twenty CLI, then declare objects, fields and views in TypeScript. This example from the README adds a <code>deal</code> object with name, amount and close-date fields:</p>
<pre><code>import { defineObject, FieldType } from 'twenty-sdk/define';

export default defineObject({
  nameSingular: 'deal',
  namePlural: 'deals',
  labelSingular: 'Deal',
  labelPlural: 'Deals',
  fields: [
    { name: 'name', label: 'Name', type: FieldType.TEXT },
    { name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
    { name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
  ],
});</code></pre>
					<p>Then you publish it to your workspace from the command line. Apps can also contain views, agents and "logic functions," which are small pieces of code the platform runs for you. There are official agent skills too, so coding assistants like Claude Code, Codex or Cursor can build Twenty apps for you.</p>
					<p>Under the hood it's a TypeScript monorepo: a NestJS backend on PostgreSQL and Redis with BullMQ for background jobs, a React front end, and REST and GraphQL APIs plus webhooks for integration.</p>

					<h2>Trying it</h2>
					<p>The fastest way is the hosted version at twenty.com. To self-host, the docs walk through Docker Compose. Grab the example environment file and compose file:</p>
<pre><code>curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
curl -o docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/docker-compose.yml</code></pre>
					<p>Fill in the secrets in <code>.env</code>, then start it with <code>docker compose up -d</code>. The stack runs a server, a background worker, PostgreSQL 16 and Redis. The server applies its own database migrations on startup. The docs also offer a one-line install script, and an upgrade guide that's worth reading before your first version bump. Back up the database first.</p>
					<p>To build your own app on top:</p>
<pre><code>npx create-twenty-app my-app
npx twenty app:publish --private</code></pre>

					<h2>The fine print</h2>
					<ul>
						<li><strong>It's mostly AGPL, not all AGPL.</strong> The licence file is worth reading. Files marked <code>/* @license Enterprise */</code> are under a commercial licence and need a paid Enterprise Edition subscription for production use. You can still use them for development and testing. The app toolkit (the CLI, SDKs, <code>create-twenty-app</code>) and the UI component library are MIT.</li>
						<li><strong>Your apps can stay proprietary.</strong> A special "Twenty Application Exception" to the AGPL says apps that talk to Twenty only through its published APIs and SDKs aren't covered by the AGPL, so you can license your own customizations however you like. Modifying Twenty itself still triggers the AGPL in full, including the clause requiring you to share source with users of a network service.</li>
						<li><strong>Self-hosting means running a real stack.</strong> Postgres, Redis, a worker process, encryption keys and regular upgrades are all on you. It's well-documented, but it isn't a single binary.</li>
						<li><strong>It moves fast.</strong> 133 releases so far. That's great for features, but it means upgrades are routine maintenance.</li>
					</ul>

					<h2>Who it's for</h2>
					<p>Twenty suits technical teams who've outgrown spreadsheets but don't want to rent Salesforce, plus companies that need their customer data on their own servers and developers who want their CRM schema under version control next to the code that uses it. If you'd rather click than code, the hosted version and the built-in views cover the basics without writing a line.</p>

					<p>The <a href="https://github.com/twentyhq/twenty" rel="noopener noreferrer">GitHub repo</a> and the <a href="https://docs.twenty.com/" rel="noopener noreferrer">documentation</a> are the places to start, and it's in the <a href="https://spiralnet.io/resources/">resource index</a> now.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>ComfyUI: Wiring Up AI Models Like a Modular Synth</title>
      <link>https://spiralnet.io/blog/comfyui.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/comfyui.html</guid>
      <pubDate>Tue, 20 Oct 2026 12:00:00 +0000</pubDate>
      <description>A look at ComfyUI, the open-source node-graph engine for running image, video, audio and 3D AI models on your own hardware.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/Comfy-Org/ComfyUI" rel="noopener noreferrer">ComfyUI</a> is a node-graph interface and inference engine for generative AI models. You connect boxes with wires to build a pipeline, then run it on your own hardware. It started out as a Stable Diffusion front end and now covers images, video, audio, 3D and text.</p>

					<p>It's back on GitHub Trending today, with around 140,000 stars. It's licensed under <strong>GPL-3.0</strong>.</p>

					<h2>What it does</h2>
					<p>Most image-generation tools give you a prompt box and a Generate button. ComfyUI gives you the plumbing instead. A workflow is a graph: one node loads a model, another encodes your prompt, another samples, another decodes the result and saves it. You can rearrange any of it, add a ControlNet or a LoRA, branch off an upscaler, or feed one model's output into another.</p>
					<p>The README's model list is long. A few highlights:</p>
					<ul>
						<li><strong>Images:</strong> Stable Diffusion 1.5, SDXL and SD3.5, Flux.1 and Flux.2, Qwen Image, HiDream, Chroma and more, plus editing models such as Flux Kontext and Qwen Image Edit.</li>
						<li><strong>Video:</strong> Wan 2.1 and 2.2, LTX-Video, HunyuanVideo 1.5, CogVideoX and Mochi.</li>
						<li><strong>Audio:</strong> ACE-Step 1.5, Stable Audio 3 and others.</li>
						<li><strong>3D and vision:</strong> Hunyuan3D 2.1, SAM 3, Depth Anything 3, SUPIR upscaling.</li>
						<li><strong>Text:</strong> Gemma and Qwen3 models, including multimodal inputs.</li>
					</ul>
					<p>Built-in tools cover inpainting and outpainting, masks and compositing, model merging, upscaling, frame interpolation, segmentation and depth estimation. It also handles 16-bit PNG, 32-bit EXR and HDR video, which matters if you're doing real post-production work.</p>

					<h2>How it works</h2>
					<p>ComfyUI is a Python backend with a browser front end. A few design choices make the node graph more than a pretty diagram:</p>
					<ul>
						<li><strong>It only runs what changed.</strong> Submit the same graph twice and only the first run executes. Change the last node and only that node and its downstream dependencies re-run. Nodes without a complete path to an output don't run at all.</li>
						<li><strong>Workflows travel with their images.</strong> Drag a generated PNG back onto the canvas and ComfyUI rebuilds the entire workflow that made it, seeds included. Workflows also save as plain JSON.</li>
						<li><strong>It's careful with memory.</strong> The README claims it can run "even the biggest open source models on as low as 4GB vram + 8GB ram relatively quickly," using what it calls asynchronous weight streaming, plus model offloading and quantized model support.</li>
					</ul>
					<p>Around the core there's a lot more: reusable subgraphs, templates, an "App Mode" that wraps a complicated workflow in a simple UI, a local API for plugging workflows into other software, and a huge ecosystem of custom nodes, managed with the optional ComfyUI-Manager.</p>

					<h2>Trying it</h2>
					<p>On Windows and macOS the project recommends the desktop app. There's also a portable Windows build for NVIDIA, AMD and Intel GPUs. The quickest command-line route is comfy-cli:</p>
<pre><code>pip install comfy-cli
comfy install</code></pre>
					<p>For a manual install, clone the repo, install the PyTorch build for your GPU (the README has exact commands for NVIDIA CUDA, AMD ROCm, Intel XPU and Apple Silicon), then:</p>
<pre><code>pip install -r requirements.txt
python main.py</code></pre>
					<p>Model files go in the matching folders, such as <code>models/checkpoints</code> for checkpoints and <code>models/vae</code> for VAEs. If you already have models downloaded for another UI, <code>extra_model_paths.yaml</code> lets ComfyUI share them instead of keeping duplicates. To add custom nodes, install the manager and launch with it enabled:</p>
<pre><code>pip install -r manager_requirements.txt
python main.py --enable-manager</code></pre>

					<h2>The fine print</h2>
					<ul>
						<li><strong>Offline by default, with paid extras.</strong> Core ComfyUI doesn't download anything unless you ask. It also ships optional "partner nodes" for closed models like Nano Banana and Seedance through Comfy's paid API. Launch with <code>--offline</code> to keep everything local, or <code>--disable-partner-nodes</code> to switch off just those nodes.</li>
						<li><strong>Custom nodes are other people's code.</strong> The ecosystem is ComfyUI's superpower, but every custom node is third-party Python running on your machine. Install the ones you trust.</li>
						<li><strong>Master moves fast.</strong> Stable releases land roughly every two weeks, and the README warns that commits between them "may be very unstable and break many custom nodes." Stick to tagged releases if you depend on a particular node pack.</li>
						<li><strong>Model licences are separate.</strong> GPL-3.0 covers ComfyUI itself. Each model you load comes with its own terms.</li>
						<li><strong>There's a learning curve.</strong> The node graph is the point, but if you just want to type a prompt and get a picture, it's more interface than you need. App Mode and templates soften that.</li>
					</ul>

					<h2>Who it's for</h2>
					<p>ComfyUI is for anyone who wants to see and control every step between a prompt and the output: artists building repeatable pipelines, VFX and video people who need high bit depth and compositing, developers who want a local generation backend with an API, and tinkerers who want to try each new open model the week it drops. If a one-box generator has ever felt like a black box, this is the opposite.</p>

					<p>The <a href="https://github.com/Comfy-Org/ComfyUI" rel="noopener noreferrer">GitHub repo</a> and <a href="https://www.comfy.org/" rel="noopener noreferrer">comfy.org</a> are the places to start, and it's in the <a href="https://spiralnet.io/resources/">resource index</a> now.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>VoiceStudio: An ElevenLabs You Run Yourself</title>
      <link>https://spiralnet.io/blog/voicestudio.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/voicestudio.html</guid>
      <pubDate>Mon, 19 Oct 2026 12:00:00 +0000</pubDate>
      <description>A look at VoiceStudio, an open-source desktop app for local voice cloning, voice design, dubbing, dictation and transcription that runs on your own hardware.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/debpalash/VoiceStudio" rel="noopener noreferrer">VoiceStudio</a> calls itself "the open-source, fully-local ElevenLabs alternative." It's a desktop app for voice cloning, voice design, video dubbing, dictation, transcription and audiobook production, and it claims support for 646 languages. The pitch is that all of it runs on your own hardware.</p>

					<p>It's one of the biggest repos on GitHub Trending right now, with roughly 76,000 stars. The application is licensed under <strong>AGPL-3.0</strong>, with a big caveat about model weights that's covered below.</p>

					<h2>What it does</h2>
					<p>The README groups everything into three columns:</p>
					<ul>
						<li><strong>Create:</strong> clone a voice from a reference recording, or design a new one from a description. A floating dictation widget handles speech-to-text anywhere on your desktop.</li>
						<li><strong>Produce:</strong> dub videos with timed speech, and build stories, audiobooks and batch jobs.</li>
						<li><strong>Connect:</strong> a local API and an MCP server so AI agents can drive it, plus reusable "recipes" and watch folders for automation.</li>
					</ul>
					<p>The feature catalog also lists vocal isolation, speaker diarization (working out who spoke when), a batch queue, GPU auto-detection and an AI watermark option for generated audio.</p>

					<h2>How it works</h2>
					<p>VoiceStudio isn't one model. It's a desktop shell around a large set of open-source speech engines that you install and switch between in a model manager.</p>
					<p><strong>Speech generation</strong> defaults to its own engine, which is powered by k2-fsa's OmniVoice. The catalog lists over a dozen alternatives, including CosyVoice 3, IndexTTS 2.5, KittenTTS, MLX-Audio, VoxCPM2, GPT-SoVITS, sherpa-onnx and MOSS-TTS, several with their own setup guides.</p>
					<p><strong>Transcription</strong> defaults to WhisperX, with Faster-Whisper, MLX Whisper, NVIDIA's Parakeet, Moonshine and FunASR as options. Live dictation uses sherpa-onnx. You can also point it at any OpenAI-compatible server if you'd rather not run speech-to-text locally.</p>
					<p>The front end is an Electron app with a Python backend. (Version 0.5.3 was the last release built on Tauri. Electron is now the only desktop app, so Tauri users have to install it separately.) The project says local workflows run entirely on your machine, remote services are optional, and usage analytics only run if you consent.</p>

					<h2>Hardware</h2>
					<p>The README is straightforward about what runs where:</p>
					<ul>
						<li><strong>NVIDIA GPU</strong> on Windows or Linux gets CUDA acceleration.</li>
						<li><strong>Apple Silicon</strong> gets Metal acceleration.</li>
						<li><strong>No dedicated GPU:</strong> it runs on the CPU, more slowly, using a small CPU build of PyTorch that needs about 5 GB of free disk.</li>
						<li><strong>Windows on ARM</strong> is experimental and CPU-only, with the Python backend running under x64 emulation.</li>
						<li><strong>Intel Macs</strong> get the UI only and have to connect to a backend running on another machine.</li>
					</ul>
					<p>Requirements vary a lot by engine. The project keeps separate performance and benchmark docs.</p>

					<h2>Trying it</h2>
					<p>On macOS or Linux it's a one-line install:</p>
<pre><code>curl -fsSL https://voicestudio.sh/install | sh</code></pre>
					<p>On Windows, in PowerShell:</p>
<pre><code>irm https://voicestudio.sh/install | iex</code></pre>
					<p>The installer keeps your settings, projects and models across upgrades, and <code>--uninstall</code> removes the app while leaving your data. If you'd rather not pipe a script into your shell, there are <code>.dmg</code>, <code>.exe</code>, <code>.AppImage</code> and <code>.deb</code> downloads on the releases page, plus a Docker guide. To run from source:</p>
<pre><code>git clone https://github.com/debpalash/VoiceStudio.git
cd VoiceStudio
bun install
bun run setup:api
bun run dev</code></pre>
					<p>Once it's running, open <strong>Voice cloning</strong>, pick a voice or add a clean reference recording, type your text and generate. The app prompts you to install the model it needs.</p>

					<h2>The fine print</h2>
					<ul>
						<li><strong>The licence covers the app, not the models.</strong> AGPL-3.0 applies to VoiceStudio's own code. Model weights, tokenizers, voices and any conditions on generated output are separate, and the README says outright that even a paid app licence doesn't grant those rights. The model manager shows each model's source notices and flags gaps in the licence review, but the project says this doesn't certify commercial use. If you plan to sell anything you make with it, check each model's terms yourself.</li>
						<li><strong>Consent matters.</strong> "Clone voices only with permission," says the README. The optional AI watermark helps mark generated audio as synthetic.</li>
						<li><strong>It's a big, fast-moving app.</strong> A dozen-plus engines, an Electron shell, a Python backend, and a recent switch from Tauri make for a lot of moving parts. Expect some setup friction, especially on less common hardware.</li>
					</ul>

					<h2>Who it's for</h2>
					<p>VoiceStudio fits people who want ElevenLabs-style tools without sending their audio to someone else's servers: podcasters and video makers dubbing their own content, writers producing audiobooks, anyone who wants offline dictation, and tinkerers who want to compare speech models side by side. The local API and MCP server also make it a handy speech backend for automation and agent setups.</p>

					<p>The <a href="https://github.com/debpalash/VoiceStudio" rel="noopener noreferrer">GitHub repo</a> and <a href="https://voicestudio.sh/" rel="noopener noreferrer">voicestudio.sh</a> are the places to start, and it's in the <a href="https://spiralnet.io/resources/">resource index</a> now.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>BRUTAL: The Framework That Got RimWorld Off Unity</title>
      <link>https://spiralnet.io/blog/brutal.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/brutal.html</guid>
      <pubDate>Fri, 16 Oct 2026 12:00:00 +0000</pubDate>
      <description>A closer look at BRUTAL, Dean Hall's open-source C# game framework: what's in it, how its licence works, and how Ludeon used it to move RimWorld off Unity.</description>
      <content:encoded><![CDATA[<p>DayZ creator Dean Hall has open-sourced <a href="https://github.com/ahwootech/BRUTAL" rel="noopener noreferrer">BRUTAL</a>, a C# framework for making games, or for making your own game engine. <a href="https://www.gamesindustry.biz/dayz-creator-dean-hall-releases-open-source-game-development-framework-brutal" rel="noopener noreferrer">GamesIndustry.biz covered the announcement</a> mostly through Hall's argument against Unity and Unreal. This post covers the rest: what's actually in the repo, how the licence works, and what it took to move RimWorld off Unity.</p>

					<p>BRUTAL is developed by Ahwoo in collaboration with RocketWerkz, both of them Hall's Auckland companies. Work started in 2022, and it's free to use in commercial games with no royalties.</p>

					<h2>A framework, not an engine</h2>
					<p>The README is blunt about what BRUTAL isn't: "There is no editor, no scene graph and no renderer, and we're not adding them." The team says it will reject pull requests that try to grow it in that direction, because "adding everything is how the industry ended up with the kind of engines we don't want to use."</p>
					<p>What you get instead is a foundation. The project compares it to the Linux kernel versus a distro like Ubuntu: BRUTAL handles the low-level layer and you build the game-specific systems on top. RocketWerkz built <a href="https://ahwoo.com/app/100000/kitten-space-agency" rel="noopener noreferrer">Kitten Space Agency</a> directly on it. Ludeon Studios built a whole engine on it instead.</p>

					<h2>What's in the box</h2>
					<p>The core is <code>Brutal.Interop</code>, a custom interop layer that gives modern C# and .NET direct access to native APIs and C/C++ libraries. Around that sit 48 libraries, each its own NuGet package, so you reference only what you need:</p>
					<ul>
						<li><strong>Foundations:</strong> unmanaged memory and allocators, collections and string handling that don't allocate, vector and matrix maths, a job system, logging and localisation.</li>
						<li><strong>Graphics:</strong> Vulkan bindings plus the Vulkan Memory Allocator, OpenGL, shader compilation through ShaderC and Slang, and texture, KTX, glTF and mesh-optimisation loaders.</li>
						<li><strong>Windowing and UI:</strong> GLFW and SDL (with SDL's image, mixer and font add-ons), Dear ImGui on the docking branch, and ImPlot.</li>
						<li><strong>Everything else:</strong> Jolt and Box2D for 3D and 2D physics, FMOD and OpenAL for audio, RakNet for networking, RenderDoc capture integration, native file dialogs, and even NotCurses for terminal UIs.</li>
					</ul>
					<p>The design goal running through all of it is what Hall calls an "obsessive focus on reducing allocations and unnecessary overhead." BRUTAL uses unmanaged memory and unsafe code wherever it makes sense, which keeps the .NET garbage collector out of the hot path.</p>

					<h2>The RimWorld test</h2>
					<p>The strongest argument for BRUTAL is RimWorld. On October 12, Ludeon <a href="https://ludeon.com/blog/2026/10/announcing-the-waylith-engine-and-update-1-7/" rel="noopener noreferrer">announced update 1.7</a>, which moves the game from Unity to <strong>Waylith</strong>, Ludeon's own engine, built with BRUTAL as its foundation. Ludeon's numbers:</p>
					<ul>
						<li><strong>2 to 4 times</strong> better performance overall in their tests.</li>
						<li><strong>2.2 times</strong> better on a stress-tested late-game colony compared with 1.6.</li>
						<li>A <strong>1,500-mod pack</strong> went from 14 minutes to 6 minutes to load.</li>
					</ul>
					<p>The bigger achievement is what didn't break. Ludeon says the game looks and plays exactly the same, 1.6 saves load in 1.7 (and back again), and "the vast majority of mods work on 1.7 without any changes." For a game whose community runs thousands of mods, that's the hard part. Modders also get the latest C# language features. The one casualty: PCs more than about ten years old may not run 1.7. It's on Steam's unstable beta branch now.</p>
					<p>Ludeon also says Waylith will power its next, unannounced game.</p>

					<h2>Built for a solar system at real scale</h2>
					<p>BRUTAL grew out of Kitten Space Agency, a free pre-alpha spaceflight sim that runs a solar system at real scale. A game like that needs to track a spacecraft precisely as it crosses interplanetary distances, then just as precisely as it docks with another ship, and general-purpose engines aren't built around that. BRUTAL let RocketWerkz design positions, coordinates and rendering around the simulation and dig into performance problems directly. KSA won Most Innovative Hi-Tech Creative Technology Solution at the 2026 NZ Hi-Tech Awards.</p>
					<p>Felipe Falanghe, who created Kerbal Space Program, collaborates on KSA and is building his next game on BRUTAL too. "Coming from Unity to working in BRUTAL, initially it was a bit daunting," he said. "But once you're there, you own everything that you've created."</p>
					<p>The README lists six projects so far: RimWorld via Waylith, plus Kitten Space Agency, Torpedia, Art of the Rail and Gruntz from RocketWerkz, and Afterskies from Floating Origin Interactive.</p>

					<h2>Trying it</h2>
					<p>You need the .NET 10 SDK, Windows or Linux, and a Vulkan 1.3 GPU if you're rendering. (Ahwoo's site lists macOS as a target too.) The README's hello-world just opens a window:</p>
<pre><code>dotnet new console -n HelloBrutal
cd HelloBrutal
dotnet add package Ahwoo.Brutal.Core
dotnet add package Ahwoo.Brutal.Glfw</code></pre>
<pre><code>using Brutal.GlfwApi;
using Brutal.Numerics;

if (!Glfw.Init())
    throw new Exception("Failed to initialize GLFW");

Glfw.WindowHint(GlfwWindowHint.ClientApi, 0);
var window = Glfw.CreateWindow(new GlfwWindow.CreateInfo
{
    Title = "Hello BRUTAL",
    Size = new int2(1280, 720),
});

while (!window.ShouldClose)
    Glfw.PollEvents();

Glfw.Terminate();</code></pre>
					<p>That's a window with no graphics API attached. Everything after that is up to you: add the Vulkan packages to write a renderer, ImGui for debug UI, the job system for parallelism, FMOD and RakNet for audio and networking.</p>

					<h2>The licence, read closely</h2>
					<p>Headlines say BRUTAL only asks for a credits line. The actual licence asks for a little more. It's MIT-based with an attribution clause, and a game built with it has to:</p>
					<ol>
						<li>include the BRUTAL copyright notice in its credits screen or equivalent,</li>
						<li>show the BRUTAL logo before gameplay starts, and</li>
						<li>ship <code>NOTICE.txt</code> with the product.</li>
					</ol>
					<p>The logo requirement is waived for non-profit projects, and Ahwoo will arrange a separate commercial licence if you don't want to show it. The licence isn't OSI-approved yet. The team says it simply hasn't submitted it, and plans to refine it with community feedback.</p>

					<h2>The fine print</h2>
					<ul>
						<li><strong>The name is a warning.</strong> "We called it BRUTAL because it can be brutal to use at times," says Ahwoo's head of engineering, Rian Drake. "There are no guardrails." It's aimed at experienced programmers who want to make their own architecture decisions.</li>
						<li><strong>It's a day-one open-source project.</strong> The repo has no build scripts, project files or solution yet, so for now you use the NuGet packages and read or fork the source. Early forum posts also pointed out that the linked API docs weren't public at launch.</li>
						<li><strong>Contributions are scoped tightly.</strong> Commits need a sign-off, AI-assisted contributions have their own rules, and anything that drifts from the "few low-level things done well" mission gets turned away.</li>
					</ul>

					<h2>Why Hall did it</h2>
					<p>Part of the motivation is personal. In May 2025, Unity sent RocketWerkz a notice threatening to revoke its licences, and Hall said the evidence included two people who had never worked there. He told PC Gamer the episode confirmed the studio's existing move away from Unity for new projects.</p>
					<p>The broader argument is about dependence. Hall told GamesIndustry.biz he worries that as AI coding tools erode software companies' competitive moats, engine vendors will try to squeeze more out of developers who feel they can't leave, "whether through royalties, subscriptions, or more restrictive licensing." He's careful not to tell anyone to drop Unity or Unreal tomorrow: "For many projects, they're absolutely the right choice." The point is having a way out. The README puts it in one line: "You are only as independent as your tools are."</p>
					<p>He also expects more frameworks like BRUTAL to appear as the cost of building alternatives falls, and he's given his own an exit hatch: "If one day we stop being accountable to developers ourselves, they should have the ability to fork the framework and go their own way."</p>

					<h2>Who it's for</h2>
					<p>BRUTAL isn't the place to make your first game. It's for teams whose game doesn't fit a general-purpose engine, like real-scale simulations or colony sims with thousands of mods, and for studios that want to own their technology all the way down. RimWorld showed a successful game can make that move without breaking its community. If your game is hard to make, BRUTAL is worth a look.</p>

					<p>The <a href="https://github.com/ahwootech/BRUTAL" rel="noopener noreferrer">GitHub repo</a>, the <a href="https://ahwoo.com/brutal" rel="noopener noreferrer">BRUTAL site</a> and the <a href="https://forums.ahwoo.com/forums/brutal/" rel="noopener noreferrer">forums</a> are the places to start, and it's in the <a href="https://spiralnet.io/resources/">resource index</a> now.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>xtool: Shipping iOS Apps Without a Mac</title>
      <link>https://spiralnet.io/blog/xtool.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/xtool.html</guid>
      <pubDate>Tue, 13 Oct 2026 12:00:00 +0000</pubDate>
      <description>A look at xtool, an open-source, cross-platform Xcode replacement that builds, signs and installs iOS apps from Linux, Windows (WSL) and macOS using SwiftPM.</description>
      <content:encoded><![CDATA[<p>The iPhone has had an SDK for as long as it's had apps, and the rule has always been the same: to build an iOS app you need a Mac running Xcode. <a href="https://github.com/xtool-org/xtool" rel="noopener noreferrer">xtool</a> breaks that rule. It calls itself a <strong>cross-platform Xcode replacement</strong> that builds and deploys iOS apps with Swift Package Manager on Linux, Windows (through WSL) and macOS.</p>

					<p>It was on GitHub Trending today, picking up more than a thousand stars in a day. It's written in Swift and licensed under <strong>MIT</strong>.</p>

					<h2>What it does</h2>
					<p>xtool reimplements the parts of Xcode you need to get an app onto a phone, using open standards instead of Xcode's project format. The project lists three jobs:</p>
					<ul>
						<li><strong>Build a SwiftPM package into an iOS app.</strong> No <code>.xcodeproj</code>, just a <code>Package.swift</code>.</li>
						<li><strong>Sign and install iOS apps</strong> on a real device.</li>
						<li><strong>Talk to Apple Developer Services from code</strong>: certificates, provisioning profiles, app IDs.</li>
					</ul>
					<p>It also ships a library called <strong>XKit</strong>, so your own Swift programs can talk to Apple Developer Services and iOS devices the same way xtool does:</p>
<pre><code>// package dependency:
.package(url: "https://github.com/xtool-org/xtool", .upToNextMinor(from: "1.2.0"))
// target dependency:
.product(name: "XKit", package: "xtool")</code></pre>

					<h2>How it pulls it off</h2>
					<p>xtool doesn't pretend Apple's SDK doesn't exist. It uses the SDK, just without Xcode around it. Four pieces fit together:</p>
					<p><strong>The Swift toolchain.</strong> You install the official open-source Swift toolchain for Linux from swift.org. That gives you the compiler and SwiftPM.</p>
					<p><strong>A Darwin Swift SDK built from Xcode.xip.</strong> You download Xcode's <code>.xip</code> archive from Apple's developer site, but you never run it. During setup, xtool unpacks it and generates an iOS <em>Swift SDK</em>, the cross-compilation bundle SwiftPM uses to target another platform. Afterwards <code>swift sdk list</code> shows a <code>darwin</code> entry.</p>
					<p><strong>usbmuxd for the device.</strong> On Linux, xtool talks to your iPhone over USB through <a href="https://github.com/libimobiledevice/usbmuxd" rel="noopener noreferrer">usbmuxd</a>, the open-source daemon behind the libimobiledevice tools. On Windows you pass the USB device through to WSL with Microsoft's USBIPD.</p>
					<p><strong>Apple Developer Services.</strong> Signing means talking to Apple's servers. You can log in with an App Store Connect <strong>API key</strong>, which needs a paid Developer Program membership, or with an <strong>Apple ID and password</strong>, which works with any account but uses private APIs. The docs say your credentials only go to Apple.</p>
					<p>With those in place, xtool compiles your package against the Darwin SDK, wraps the result in an app bundle, provisions and signs it, and installs it on the phone.</p>

					<h2>Trying it</h2>
					<p>The <a href="https://xtool.sh/documentation/xtool/installation-linux/" rel="noopener noreferrer">Linux/Windows install guide</a> has the full steps. In short:</p>
<pre><code># prerequisites: Swift toolchain (swift.org), usbmuxd, and Xcode.xip from Apple
sudo apt-get install usbmuxd

# install xtool (it ships as an AppImage)
curl -fL \
  "https://github.com/xtool-org/xtool/releases/latest/download/xtool-$(uname -m).AppImage" \
  -o xtool
chmod +x xtool
sudo mv xtool /usr/local/bin/

# one-time setup: log in, then point it at Xcode.xip to build the SDK
xtool setup</code></pre>
					<p>After that, the command line will feel familiar if you've used <code>cargo</code> or <code>npm</code>:</p>
<pre><code>xtool new       # create a new xtool SwiftPM project
xtool dev       # build and run it
xtool devices   # list connected devices
xtool install   # install an .ipa on your device
xtool launch    # launch an installed app
xtool ds        # interact with Apple Developer Services</code></pre>
					<p>The README's screenshot shows the whole loop running from VS Code, which makes sense as the editor once Xcode is out of the picture. The <a href="https://xtool.sh/tutorials/xtool/first-app" rel="noopener noreferrer">first-app tutorial</a> walks you from an empty folder to an app on your phone.</p>

					<h2>The fine print</h2>
					<ul>
						<li><strong>You still need Apple's SDK.</strong> xtool replaces the Xcode app, not Apple's SDK. You download Xcode.xip with an Apple ID, and Apple's license terms for it still apply.</li>
						<li><strong>Signing still goes through Apple.</strong> Free Apple IDs come with Apple's usual limits on sideloaded apps, and the password login relies on private APIs that Apple could change at any time.</li>
						<li><strong>It builds and deploys. That's all.</strong> There's no Interface Builder, Instruments or Simulator in its feature list, and App Store submission isn't a listed feature either.</li>
						<li><strong>It's SwiftPM-first.</strong> Your app is defined as a Swift package. That's cleaner, but an existing <code>.xcodeproj</code> app needs restructuring first.</li>
					</ul>

					<h2>Who it's for</h2>
					<ul>
						<li>Swift developers on <strong>Linux or Windows</strong> who want an app on their own iPhone without buying a Mac.</li>
						<li><strong>CI setups</strong> that would rather build iOS artifacts on cheap Linux runners than on macOS ones.</li>
						<li>Mac users who are tired of <code>.xcodeproj</code> merge conflicts and want a <strong>declarative, package-based</strong> workflow from the terminal.</li>
						<li>Tool builders who want to script Apple Developer Services and devices with <strong>XKit</strong>.</li>
					</ul>

					<p>xtool won't retire Xcode overnight. What it does is turn "building an iOS app requires a Mac" into "building an iOS app requires a Swift toolchain and a USB cable." That opens iOS development to a lot more people.</p>

					<p>The <a href="https://github.com/xtool-org/xtool" rel="noopener noreferrer">GitHub repo</a> and the <a href="https://xtool.sh/" rel="noopener noreferrer">docs at xtool.sh</a> are the places to start, and it's in the <a href="https://spiralnet.io/resources/">resource index</a> now.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>AnyPS5: Porting PS5 Games Without an Emulator</title>
      <link>https://spiralnet.io/blog/anyps5.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/anyps5.html</guid>
      <pubDate>Sun, 11 Oct 2026 12:00:00 +0000</pubDate>
      <description>A look at AnyPS5, an open-source tool that converts PS5 executables into native Linux and Windows programs instead of emulating the console.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/boykopovar/AnyPS5" rel="noopener noreferrer">AnyPS5</a> takes a PS5 executable and turns it into a native Linux or Windows program. Not an emulator wrapped around the game: the output is an ordinary <code>.elf</code> or <code>.exe</code> that your operating system loads and runs like anything else you installed.</p>

					<p>It's one of the busiest repositories in the console-preservation scene right now, with tens of thousands of stars and thousands of commits, and it's licensed under the <strong>GPL-2.0</strong>.</p>

					<h2>Emulation vs. porting</h2>
					<p>An emulator pretends to be the console. It simulates the hardware and the operating system every frame while the game runs, and that pretending costs a lot of CPU. AnyPS5 does the work once, ahead of time:</p>
					<ul>
						<li>It <strong>converts the executable</strong> into the native format of the target system.</li>
						<li>It <strong>replaces the console's system libraries</strong> with native shared libraries that your OS loader links in, the same way any other program pulls in a <code>.so</code> or <code>.dll</code>.</li>
						<li>It <strong>translates the GPU shaders</strong> into SPIR-V so they run on Vulkan, and caches the results to disk.</li>
					</ul>
					<p>There's no separate runtime process sitting between the game and your hardware. Once converted, it's just a program.</p>

					<h2>The three moving parts</h2>
					<p><strong>The relinker.</strong> This is the command-line tool you actually run. It reads the executable's imports, checks its system calls, filters out functions the game never uses, converts any bundled modules that ship with it, and writes out a Linux ELF or a Windows PE. An optional <code>--to-intel</code> pass rewrites AMD-only CPU instructions so the result runs on Intel chips too.</p>

					<p><strong>The system libraries.</strong> PS5 games call into the console's own libraries by numeric ID rather than by name. AnyPS5 reimplements those libraries from scratch as native shared libraries and labels each function with the ID the game expects, so the OS loader can match them up without knowing anything about PlayStation. The project tracks how much of each library it has implemented on its <a href="https://boykopovar.github.io/AnyPS5/" rel="noopener noreferrer">progress page</a>.</p>

					<p><strong>The shader recompiler.</strong> The PS5's GPU is AMD RDNA, and its shaders are compiled for that hardware. The recompiler decodes them, rebuilds their control flow, converts them to an intermediate form, optimizes it and emits SPIR-V for Vulkan. Compiled shaders are cached in memory and on disk, so the stutter of compiling one only happens the first time.</p>

					<h2>Where it stands</h2>
					<p>Early, and honest about it. The <a href="https://github.com/boykopovar/AnyPS5/blob/main/docs/user/COMPATIBILITY.md" rel="noopener noreferrer">compatibility list</a> has one verified game so far: <em>Dreaming Sarah</em>, a 2D platformer, playable on Windows. It holds a steady 60 fps on a GTX 1050 Ti with an i5-7500, and around 36 fps on Intel HD 620 laptop graphics. Linux status for it is still marked unknown.</p>

					<p>The project also fails loudly on purpose. Anything it doesn't support yet throws an error, prints what went wrong and exits, rather than limping along with glitches. For a project at this stage that's the right call: crashes with a clear message are much easier to fix than mystery bugs.</p>

					<p>Controllers work through SDL, including analog sticks and triggers, and keyboard and mouse can be mapped with an <code>anyps5-input.ini</code> file. The <a href="https://github.com/boykopovar/AnyPS5/blob/main/docs/user/INPUT_MAPPING.md" rel="noopener noreferrer">input mapping doc</a> has the format.</p>

					<h2>Trying it</h2>
					<p>There are no prebuilt releases yet, so you build it yourself. The <a href="https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/BUILD.md" rel="noopener noreferrer">build guide</a> covers it, but in short you need:</p>
					<ul>
						<li>An x86-64 machine with Git, CMake 3.22.1 or newer, Ninja and a C++20 compiler.</li>
						<li>On Linux, GCC and binutils.</li>
						<li>On Windows, one specific MinGW-w64 build: WinLibs GCC 15.2.0 (UCRT, POSIX threads). Other compilers aren't supported.</li>
						<li>A GPU with Vulkan support to actually run the result.</li>
					</ul>
					<p>Building produces the relinker plus the replacement system libraries. You point the relinker at a game's executable and pick Linux or Windows output, and it lays out a folder with the converted program, a <code>libs/</code> directory of system libraries and the game's own resources. The <a href="https://github.com/boykopovar/AnyPS5/blob/main/docs/user/USAGE.md" rel="noopener noreferrer">usage guide</a> walks through the options.</p>

					<h2>The legal part</h2>
					<p>AnyPS5 ships no games, no console firmware, no encryption keys and none of Sony's libraries. Everything it provides is written from scratch. The project frames itself as an interoperability, research and preservation tool, and it puts the responsibility on you: any game you feed it has to be one you obtained and use legally.</p>

					<h2>Why it's worth watching</h2>
					<p>Most console preservation work has meant emulators, and emulators for modern consoles are hard: they need beefy hardware and years of work before anything big runs well. Converting the game into a native program sidesteps a lot of that overhead. It's a long road from one 2D platformer to the rest of the library, but the architecture is clean, the documentation is unusually good for a project this young, and the pace of commits is fast.</p>

					<p>If you want to follow along, the <a href="https://github.com/boykopovar/AnyPS5" rel="noopener noreferrer">GitHub repo</a> is the place to start, and it's in the <a href="https://spiralnet.io/resources/">resource index</a> now. The <a href="https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/ARCHITECTURE.md" rel="noopener noreferrer">architecture doc</a> is a good read even if you never build it.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>Paperless-ngx: Scan It, Search It, Shred It</title>
      <link>https://spiralnet.io/blog/paperless-ngx.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/paperless-ngx.html</guid>
      <pubDate>Wed, 07 Oct 2026 12:00:00 +0000</pubDate>
      <description>A detailed guide to Paperless-ngx, the self-hosted document archive: OCR, automatic tagging, Docker install, getting paper in, matching rules, workflows, backups and how it compares to Mayan EDMS.</description>
      <content:encoded><![CDATA[<p>Every household has the drawer. Tax letters, warranty cards, the lease, a pay stub from three jobs ago, the vet's invoice you'll need exactly once. It all goes in the drawer, and finding anything means digging through all of it.</p>
					<p><a href="https://github.com/paperless-ngx/paperless-ngx" rel="noopener noreferrer">Paperless-ngx</a> replaces the drawer. Scan a document, or drop in a PDF, and it reads the text with OCR, figures out who sent it and what it is, tags it, and files it into a searchable archive on your own server. It's one of the most popular self-hosted apps there is, with around 45,000 stars on GitHub, and once it's running it quietly changes how you deal with paper.</p>

					<h2>Where it came from</h2>
					<p>Paperless-ngx is the third generation of the idea. The original <em>Paperless</em> was followed by <em>Paperless-ng</em>, a rewrite with a modern interface. When that project's single maintainer stepped back, the community forked it as Paperless-ngx so that a <strong>team</strong> would carry it forward instead of one person. It's GPL-3.0 licensed, built on Django and Angular, and on the 3.x release line.</p>

					<h2>What it does</h2>
					<ul>
						<li><strong>OCR on everything.</strong> It uses the open-source Tesseract engine to read more than 100 languages, adding searchable, selectable text even to image-only scans.</li>
						<li><strong>Archival PDFs.</strong> Each document is stored as a PDF/A, the format built for long-term storage, next to the untouched original.</li>
						<li><strong>Automatic filing.</strong> A machine-learning classifier trained on your own documents suggests and assigns tags, correspondents and document types.</li>
						<li><strong>Full-text search</strong> with autocomplete, relevance ranking, highlighted matches and "more like this".</li>
						<li><strong>Many file types.</strong> PDFs, images and plain text work out of the box. Word, Excel, PowerPoint and LibreOffice files work with an optional add-on.</li>
						<li><strong>Email import.</strong> It watches mail accounts and pulls in attachments according to rules you define.</li>
						<li><strong>Workflows</strong> that assign metadata, send emails, call webhooks and more, based on triggers.</li>
						<li><strong>Multiple users</strong> with global and per-document permissions, two-factor login, and share links that can expire.</li>
						<li><strong>Your files stay plain.</strong> Documents are ordinary files on disk, in a folder layout you control. Nothing is locked inside a database.</li>
					</ul>

					<h2>The four things every document has</h2>
					<p>Paperless doesn't use folders. Everything is organized with four kinds of metadata, and understanding them is the key to the whole app:</p>
					<pre><code>CORRESPONDENT   who it's from or to      Electric Co, IRS, Dr. Patel
DOCUMENT TYPE   what kind of thing it is  invoice, contract, statement
TAGS            any labels you like       taxes-2026, car, paid, inbox
STORAGE PATH    where the file lands      {{ correspondent }}/{{ created_year }}/{{ title }}</code></pre>
					<p>Tags are the clever part. A document can carry many tags, so "car insurance renewal" can live under <code>car</code>, <code>insurance</code> and <code>taxes-2026</code> at the same time. A folder can't do that. Add <strong>custom fields</strong> (amounts, due dates, policy numbers) and an <strong>archive serial number</strong> that matches the label on the physical copy in your binder, and you can find anything, digital or paper.</p>

					<h2>Install it with Docker</h2>
					<p>The quickest route is the official install script. It asks a few questions, writes the compose files, starts the containers and creates your admin account:</p>
					<pre><code>bash -c "$(curl --location --silent --show-error \
  https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"</code></pre>
					<p>If you prefer to see every piece, set it up by hand. The project ships compose files for SQLite, PostgreSQL and MariaDB, each with an optional <code>-tika</code> variant that adds Office document and email support. PostgreSQL is the recommended choice for new installs. Trimmed down, the Postgres version looks like this:</p>
					<pre><code>services:
  broker:
    image: docker.io/valkey/valkey:9-alpine
    restart: unless-stopped
    volumes:
      - redisdata:/data
  db:
    image: docker.io/library/postgres:18
    restart: unless-stopped
    volumes:
      - pgdata:/var/lib/postgresql
    environment:
      POSTGRES_DB: paperless
      POSTGRES_USER: paperless
      POSTGRES_PASSWORD: paperless
  webserver:
    image: ghcr.io/paperless-ngx/paperless-ngx:latest
    restart: unless-stopped
    depends_on: [db, broker]
    ports:
      - "8000:8000"
    volumes:
      - data:/usr/src/paperless/data
      - media:/usr/src/paperless/media
      - ./export:/usr/src/paperless/export
      - ./consume:/usr/src/paperless/consume
    env_file: docker-compose.env
    environment:
      PAPERLESS_REDIS: redis://broker:6379
      PAPERLESS_DBHOST: db
      PAPERLESS_DBENGINE: postgresql
volumes:
  data:
  media:
  pgdata:
  redisdata:</code></pre>
					<p>Grab the matching <code>docker-compose.env</code> and <code>.env</code> from the repo's <code>docker/compose</code> folder, then set at least these in <code>docker-compose.env</code>:</p>
					<pre><code># Required. Generate one with:
#   python3 -c "import secrets; print(secrets.token_urlsafe(64))"
PAPERLESS_SECRET_KEY=change-me

PAPERLESS_TIME_ZONE=America/New_York
PAPERLESS_OCR_LANGUAGE=eng

# Your host user's IDs (run: id -u / id -g), so you can
# write into the consume folder
USERMAP_UID=1000
USERMAP_GID=1000

# Only if you'll reach it from a domain
#PAPERLESS_URL=https://paperless.example.com</code></pre>
					<p>Then:</p>
					<pre><code>docker compose pull
docker compose up -d
# open http://localhost:8000 and create your superuser</code></pre>

					<h2>Getting paper in</h2>
					<p>There are four doors into Paperless, and most setups end up using all of them:</p>
					<ul>
						<li><strong>The consume folder.</strong> Anything dropped into <code>./consume</code> is picked up, processed and moved into the archive. Point your scanner's "scan to network folder" at it through a Samba share or FTP, and scanning becomes a one-button job. (The file leaves the consume folder once it's processed. That's expected.)</li>
						<li><strong>Drag and drop.</strong> Drop a file anywhere in the web interface.</li>
						<li><strong>Email.</strong> Add an IMAP account under <em>Settings → Mail</em>, then write rules: which folder to watch, which attachments to take, which tags to apply, and what to do with the message afterwards.</li>
						<li><strong>Phone.</strong> Community mobile apps scan and upload straight from your camera. The project wiki keeps a list of them.</li>
					</ul>
					<p><strong>Tip:</strong> if your consume folder lives on an NFS share, set <code>PAPERLESS_CONSUMER_POLLING_INTERVAL</code>. Network file systems don't send the change notifications Paperless listens for, so without polling, new files just sit there.</p>

					<h2>Let it file things for you</h2>
					<p>This is where Paperless earns its keep. Every tag, correspondent, document type and storage path has a <strong>matching rule</strong> that runs against each new document's text:</p>
					<pre><code>Any      any of the words appear        "electric power"
All      every word appears, any order  "invoice 2026"
Exact    the exact phrase appears       "Bank of America"
Regex    a regular expression           Policy\s+No\.\s*\d+
Fuzzy    close-enough partial match     Hydro Quebec
Auto     learn from how you've filed things before</code></pre>
					<p><strong>Auto</strong> is the default, and it gets good fast. File your first few dozen documents by hand and the classifier learns your habits. A new statement from your bank arrives already tagged, typed and assigned. Use the explicit rules for the cases where you want certainty, such as "anything containing my policy number goes under <code>insurance</code>".</p>

					<h3>Workflows</h3>
					<p>Workflows go a step further: <em>when this happens, do that</em>. Triggers include a document starting to be consumed, being added, being updated, or a <strong>scheduled</strong> date. Actions include assigning or removing metadata, sending an email, calling a webhook, removing a PDF password, or moving a document to the trash. Some real-world uses:</p>
					<ul>
						<li>Everything that arrives by email from your accountant gets the <code>taxes</code> tag and is shared with your partner's account.</li>
						<li>Warranty documents trigger a scheduled reminder email 30 days before a custom "warranty ends" date field.</li>
						<li>Bank statements, which always come password-protected, get the password stripped on arrival so they can be OCR'd and searched.</li>
						<li>A webhook pings your home-automation system whenever a new invoice lands.</li>
					</ul>

					<h3>Barcodes and ASNs</h3>
					<p>For heavy paper users: Paperless reads barcodes on PDF pages. Print a sheet of ASN barcode stickers, put one on each paper document before scanning, and the archive serial number is set automatically. To find the original later, read the ASN off the screen and pull that number from your binder. Barcode pages can also split a big batch scan into separate documents.</p>

					<h2>The optional AI features</h2>
					<p>Recent versions add opt-in AI features on top of the classic classifier: LLM-written suggestions for titles, dates and tags, chatting with a single document or your whole archive, and similar-document search. They're <strong>off by default</strong> and work with a local model through Ollama or any OpenAI-compatible API.</p>
					<p>Think before you switch them on. With a remote provider, your document text leaves your server, and that defeats much of the point of self-hosting tax returns and medical letters. If you want these features, a local model keeps everything at home.</p>

					<h2>Backups, and one big warning</h2>
					<p>The project says it plainly: Paperless stores your documents <strong>unencrypted</strong>, and you should never run it on an untrusted host. Run it on your own hardware or a server you control, keep it off the open internet (use a VPN like WireGuard or Tailscale to reach it from outside), and back it up.</p>
					<p>For backups, use the built-in exporter. It writes every document plus all metadata into a folder that can be re-imported into a fresh install:</p>
					<pre><code>docker compose exec -T webserver document_exporter ../export</code></pre>
					<p>Put that on a nightly cron job and copy the <code>export</code> folder off the machine with your usual backup tool. A document archive with no off-site backup is just a fancier drawer that can burn down.</p>

					<h2>Running it on small hardware</h2>
					<p>Paperless runs fine on a Raspberry Pi 4 or a cheap mini PC. On low-power machines:</p>
					<ul>
						<li>Use SQLite instead of Postgres to save memory.</li>
						<li>Set <code>PAPERLESS_OCR_PAGES=1</code>. The first page is usually enough to find a document.</li>
						<li>Lower <code>PAPERLESS_TASK_WORKERS</code> and <code>PAPERLESS_THREADS_PER_WORKER</code>, so a big batch doesn't freeze the machine.</li>
						<li>Set <code>PAPERLESS_WEBSERVER_WORKERS=1</code>.</li>
						<li>Let your scanner do the OCR if it can, and leave <code>PAPERLESS_OCR_MODE</code> on <code>auto</code>.</li>
					</ul>

					<h2>Tips from the trenches</h2>
					<ul>
						<li><strong>Use an inbox tag.</strong> New documents get the <code>inbox</code> tag automatically. Review the inbox once a week, fix anything mis-filed, and remove the tag. Every correction trains the classifier.</li>
						<li><strong>Start with the backlog in batches.</strong> Scanning ten years of paper in one go is overwhelming. Do a box a weekend.</li>
						<li><strong>Keep the paper you're legally required to keep.</strong> A scan isn't always a legal substitute. Put an ASN on those originals and keep them in a binder in number order.</li>
						<li><strong>Try the demo first.</strong> There's a public demo at <a href="https://demo.paperless-ngx.com" rel="noopener noreferrer">demo.paperless-ngx.com</a> (login <code>demo</code> / <code>demo</code>). It resets often, so don't upload anything real.</li>
					</ul>

					<h2>Paperless-ngx or Mayan EDMS?</h2>
					<p>The other big name in self-hosted document management is <a href="https://spiralnet.io/blog/mayan-edms.html">Mayan EDMS</a>, which gets its own transmission today. The short version: Paperless-ngx is built for <strong>a household or small office</strong> that wants paper to vanish into a searchable archive with as little effort as possible. Mayan is built for <strong>organizations</strong> that need formal document types, approval workflows, version control and fine-grained access control, and it asks for more setup and more server in return. For most homelabs, Paperless is the right first stop.</p>

					<h2>Verdict</h2>
					<p>Paperless-ngx is the rare self-hosted app that pays you back in time every week. The setup is a compose file. The first weekend of scanning is a chore. After that, paper stops being a problem: it goes in the scanner, comes out tagged and searchable, and the drawer gets recycled.</p>
					<p>Start with the <a href="https://github.com/paperless-ngx/paperless-ngx" rel="noopener noreferrer">GitHub repo</a> and the <a href="https://docs.paperless-ngx.com" rel="noopener noreferrer">documentation</a>, which is excellent. Links are also in the <a href="https://spiralnet.io/downloads/">downloads directory</a> and the <a href="https://spiralnet.io/resources/">resource index</a>.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>Mayan EDMS: Document Management With a Paper Trail</title>
      <link>https://spiralnet.io/blog/mayan-edms.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/mayan-edms.html</guid>
      <pubDate>Wed, 07 Oct 2026 12:00:00 +0000</pubDate>
      <description>A detailed guide to Mayan EDMS, the open-source enterprise document management system: document types, metadata, indexes, approval workflows, Docker install, and how it compares to Paperless-ngx.</description>
      <content:encoded><![CDATA[<p>Most self-hosted document apps answer one question: <em>where's that letter?</em> <a href="https://gitlab.com/mayan-edms/mayan-edms" rel="noopener noreferrer">Mayan EDMS</a> answers a harder set: who approved this contract, which version did legal sign off on, who's allowed to see it, and when can we legally destroy it?</p>
					<p>Mayan is an <strong>electronic document management system</strong> in the enterprise sense. It has OCR and search like the others, but its real strengths are structure and control: document types with required metadata, multi-step approval workflows, version history, digital signatures and permissions down to a single document. It's free and open source (GPL-2.0), written in Python on Django, has been developed for more than a decade, and is on the 4.12 release line.</p>

					<h2>The mental model</h2>
					<p>Mayan has more moving parts than a home-scanning app, and they make sense once you see how they fit together. Everything hangs off the <strong>document type</strong>:</p>
					<pre><code>DOCUMENT TYPE   "Invoice", "Contract", "HR Record"
  ├─ metadata types   vendor, amount, due date (required or optional)
  ├─ OCR + parsing    on/off, language
  ├─ workflows        which processes this type goes through
  ├─ retention        trash after N days, delete after N more
  └─ antivirus        scan uploads with ClamAV or not

Then, on top of documents:
  TAGS       colored labels
  CABINETS   hand-made folder trees (a document can be in several)
  INDEXES    folder trees Mayan builds for you from templates
  ACLs       who can do what, per document, type or object</code></pre>
					<p>The distinction between <strong>cabinets</strong> and <strong>indexes</strong> is the most important one to grasp. Cabinets are folders you curate by hand. Indexes are generated automatically: you write a template once, and Mayan sorts every document into a virtual folder tree from its metadata, updating it as documents change.</p>

					<h2>Feature tour</h2>
					<ul>
						<li><strong>Metadata.</strong> Typed fields per document type, with defaults, lookups and validation. "Invoice" can require a vendor and an amount, while "Contract" requires parties and an expiry date.</li>
						<li><strong>OCR and parsing.</strong> Tesseract is built in, and the work can be spread across several worker machines. Text from born-digital files is parsed directly. Built-in file metadata (author, camera settings, GPS) is extracted automatically.</li>
						<li><strong>Workflows.</strong> Real state machines with states, transitions, actions on entry and exit, and automatic expiry and escalation. Every change is logged.</li>
						<li><strong>Versions.</strong> Multiple versions of a document, each of which can be downloaded or restored, with an audit trail.</li>
						<li><strong>Check-out and check-in</strong>, so two people don't edit the same document at once.</li>
						<li><strong>Signatures.</strong> Verify embedded cryptographic signatures, attach detached ones, and record captured handwritten signatures.</li>
						<li><strong>Smart links</strong> that connect related documents automatically by their metadata, such as a purchase order and its invoices.</li>
						<li><strong>Duplicate detection</strong>, with exact-file and exact-label matching out of the box.</li>
						<li><strong>Search.</strong> Full-text search over content and metadata. A built-in backend is the default, and Elasticsearch is available for big archives.</li>
						<li><strong>Access control.</strong> Unlimited roles, a permission for every single operation, and ACLs on individual objects, with LDAP, OpenID Connect and other single sign-on options.</li>
						<li><strong>Antivirus.</strong> Optional ClamAV scanning of uploads, switched on per document type.</li>
						<li><strong>A full REST API</strong> with OpenAPI documentation, so other systems can push documents in and pull them out.</li>
					</ul>

					<h2>Install it with Docker</h2>
					<p>The official route is Docker Compose, and getting started really is three commands:</p>
					<pre><code>curl https://gitlab.com/mayan-edms/mayan-edms/-/raw/master/docker/docker-compose.yml -O
curl https://gitlab.com/mayan-edms/mayan-edms/-/raw/master/docker/.env -O
docker compose up --detach</code></pre>
					<p><strong>Read this before you run the last command.</strong> The compose file falls back to default passwords for its database, message queue and cache (<code>mayandbpass</code> and friends). Set your own in <code>.env</code> <em>before</em> the first start, because they're baked into the database when it's created:</p>
					<pre><code># add to .env
MAYAN_DATABASE_PASSWORD=a-long-random-string
MAYAN_RABBITMQ_PASSWORD=another-long-random-string
MAYAN_REDIS_PASSWORD=and-another-one

# the web UI listens on port 80 by default; move it if 80 is taken
MAYAN_FRONTEND_HTTP_PORT=8080</code></pre>
					<p>The stack is assembled with Compose <strong>profiles</strong>. The default in <code>.env</code> is:</p>
					<pre><code>COMPOSE_PROFILES=all_in_one,postgresql,rabbitmq,redis</code></pre>
					<p>That runs the Mayan app (web interface and workers in one container) with PostgreSQL for data, RabbitMQ for the task queue and Redis for locking and caching. Add profiles as you need them:</p>
					<pre><code>elasticsearch       a faster search backend for large archives
traefik             reverse proxy with automatic Let's Encrypt HTTPS
postgresql-backup   scheduled database dumps
index_mirror        expose an index as a read-only folder tree on the host
cabinet_mirror      the same, for a cabinet
extra_worker_*      split workers out to scale up</code></pre>
					<p>On first load, the login page shows <strong>automatically generated admin credentials</strong>. Log in with them, change the password straight away (the notice disappears once you do), and create real user accounts.</p>
					<p>Be realistic about resources. Even the all-in-one setup runs four services, including a database and a message broker. Plan on a few gigabytes of RAM and a few CPU cores, more if you'll OCR a lot. This is a VPS or homelab-server app, not a Raspberry Pi one.</p>

					<h2>A worked example: invoice approval</h2>
					<p>Here's what Mayan is built for. Say invoices arrive by email, need a manager's approval, and must be kept for seven years.</p>

					<h3>1. The document type</h3>
					<p>Create an <code>Invoice</code> document type with required metadata types <code>vendor</code>, <code>amount</code> and <code>due_date</code>. Under its retention settings, keep files for seven years before they move to the trash. Turn on ClamAV for it, since email attachments are where malware arrives.</p>

					<h3>2. The source</h3>
					<p>Add an <strong>email source</strong> (IMAP or POP3) pointed at <code>invoices@</code>, assigned to the <code>Invoice</code> type. Attachments come in typed and ready for metadata. For paper, a <strong>watch folder</strong> that your scanner drops into works the same way, and a <strong>staging folder</strong> lets you preview files before uploading them.</p>

					<h3>3. The workflow</h3>
					<pre><code>[Received] --submit--> [Awaiting approval] --approve--> [Approved] --pay--> [Paid]
                               |
                               +--reject--> [Rejected]

Awaiting approval: escalate to the finance lead if untouched for 3 days
Approved:          action = add tag "approved", email accounts payable</code></pre>
					<p>Each transition can be limited to specific roles: only managers can approve, and only finance can mark something paid. Every step is recorded in the document's workflow log with who did it and when.</p>

					<h3>4. The index</h3>
					<p>Finally, an index that files invoices automatically by year, then vendor. Each level is a small template:</p>
					<pre><code>Level 1:  {{ document.date_added|date:"Y" }}
Level 2:  {{ document.metadata_value_of.vendor }}</code></pre>
					<p>Browse the index and you see <code>2026 / Electric Co / …</code>, rebuilt live as invoices arrive. Turn on the <code>index_mirror</code> profile and the same tree appears as a read-only folder on the server, which is handy for backup tools or a file share.</p>

					<h2>Things to know before you commit</h2>
					<ul>
						<li><strong>There's a learning curve.</strong> Document types, metadata types, indexes, smart links and workflows are each separate screens. Budget an afternoon to model your documents before you upload anything, because restructuring later is much harder.</li>
						<li><strong>The interface is functional, not fancy.</strong> It's a dense admin-style UI built for daily office work, not a slick consumer app.</li>
						<li><strong>Back up two things:</strong> the PostgreSQL database (the <code>postgresql-backup</code> profile helps) and the app volume at <code>/var/lib/mayan</code>, which holds the files themselves. One without the other is useless.</li>
						<li><strong>Keep it private.</strong> Like any archive of sensitive paperwork, run it behind a VPN or an authenticated reverse proxy. The <code>traefik</code> profile handles HTTPS, but HTTPS alone doesn't make it safe to expose to the internet.</li>
						<li><strong>The docs are thorough</strong>, and the project also publishes books and a knowledge base if you're deploying it seriously.</li>
					</ul>

					<h2>Mayan EDMS or Paperless-ngx?</h2>
					<p>I covered <a href="https://spiralnet.io/blog/paperless-ngx.html">Paperless-ngx</a> in a separate transmission today, and they're the two names that come up every time. They solve different problems:</p>
					<pre><code>                    PAPERLESS-NGX            MAYAN EDMS
Built for           homes, small offices     teams, organizations
Organizing          tags, auto-classifier    types, metadata, indexes
Workflows           automation rules         approval state machines
Versions            yes                      yes, with check-out/in
Signatures          no                       verify and capture
Permissions         per user and document    roles and ACLs
Setup effort        an evening               a weekend of planning
Hardware            Raspberry Pi is fine     a real server
License             GPL-3.0                  GPL-2.0</code></pre>
					<p>If your goal is "make the paper pile searchable and stop thinking about it", choose Paperless-ngx. If you're running a small business, a nonprofit or a club, where documents have owners, approvals, versions and retention rules, Mayan is the one built for that, and it's hard to beat for free software.</p>

					<h2>Verdict</h2>
					<p>Mayan EDMS is what you reach for when documents are part of a <em>process</em> rather than just a pile. It asks more of you up front, in planning and in hardware, and gives you something few open-source tools offer in return: a document system with real structure, audit trails and access control, running entirely on your own server.</p>
					<p>Start with the <a href="https://docs.mayan-edms.com" rel="noopener noreferrer">documentation</a> and the <a href="https://gitlab.com/mayan-edms/mayan-edms" rel="noopener noreferrer">GitLab repository</a>. Links are also in the <a href="https://spiralnet.io/downloads/">downloads directory</a> and the <a href="https://spiralnet.io/resources/">resource index</a>.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>Glance: One Page for Everything You Check</title>
      <link>https://spiralnet.io/blog/glance.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/glance.html</guid>
      <pubDate>Mon, 05 Oct 2026 12:00:00 +0000</pubDate>
      <description>A detailed look at Glance, a fast self-hosted dashboard: what it does, how to install it with Docker, how the YAML config works, the best widgets, theming, and how to lock it down.</description>
      <content:encoded><![CDATA[<p>Every morning goes the same way: open the RSS reader, then Hacker News, then a couple of subreddits, then the weather, then a quick look at whether the home server is still alive. Five tabs to answer one question: <em>what's new?</em></p>
					<p><a href="https://github.com/glanceapp/glance" rel="noopener noreferrer">Glance</a> collapses all of that into one page. It's a self-hosted dashboard that pulls your feeds, news, videos, weather, markets and server status into a single, fast, good-looking start page. You describe the page in one YAML file and Glance does the rest. It runs as a single small binary or a Docker container, and it's quietly become one of the most popular self-hosted projects on GitHub, at over 35,000 stars.</p>

					<h2>What it is (and isn't)</h2>
					<p>Glance is a <strong>reading dashboard</strong>. Its job is to put the things you check every day in one place, laid out in columns, so one glance (hence the name) tells you what changed. It's written in Go, licensed AGPL-3.0, and the whole thing ships as a single binary under 20 MB with few dependencies.</p>
					<p>It is <em>not</em> a live monitoring system. This is the single most important thing to understand before you set it up:</p>
					<ul>
						<li>Glance fetches data <strong>when you load the page</strong>, then caches it. Nothing runs in the background.</li>
						<li>The page doesn't update itself. A refresh pulls fresh data, once the cache for that widget has expired.</li>
						<li>Clocks and relative timestamps ("3h ago") do tick on their own, but feeds and stats don't.</li>
					</ul>
					<p>That design is why it's so light. There's no database, no worker process, no polling loop eating CPU at 3 a.m. If you want alerts at 3 a.m., pair it with something like Uptime Kuma. If you want a start page that loads in about a second, this is it.</p>

					<h2>The widgets</h2>
					<p>Everything on a Glance page is a widget. The built-in set covers most of what a self-hoster checks in a day:</p>
					<pre><code>FEEDS &amp; NEWS
  rss              RSS and Atom feeds, several display styles
  hacker-news      Hacker News front page
  lobsters         Lobsters front page
  reddit           any subreddit, with optional thumbnails
  videos           latest uploads from YouTube channels
  twitch-channels  who's live
  releases         new releases from GitHub, GitLab, Docker Hub

HOMELAB
  monitor          is each service up, and how fast did it answer
  docker-containers  status of your containers
  server-stats     CPU, memory and disk usage
  dns-stats        AdGuard Home, Pi-hole or Technitium stats
  repository       stars, issues and PRs for a GitHub repo

EVERYDAY
  weather, calendar, clock, markets, bookmarks, search, to-do

BUILD YOUR OWN
  custom-api       fetch any JSON API and render it with a template
  extension        pull HTML from a URL you control
  html, iframe     drop in your own markup or embed a page

LAYOUT
  group            stack widgets as tabs
  split-column     put widgets side by side</code></pre>
					<p>Every widget accepts a <code>title</code> to rename it, and most accept a <code>cache</code> duration such as <code>30m</code>, <code>12h</code> or <code>1d</code>. The exceptions are weather and calendar, which update hourly on a fixed schedule.</p>

					<h2>Install it with Docker</h2>
					<p>The fastest route is the official compose template, which sets up a sensible folder layout in one command:</p>
					<pre><code>mkdir glance &amp;&amp; cd glance &amp;&amp; curl -sL \
  https://github.com/glanceapp/docker-compose-template/archive/refs/heads/main.tar.gz \
  | tar -xzf - --strip-components 2</code></pre>
					<p>That gives you <code>docker-compose.yml</code>, <code>config/glance.yml</code> and <code>config/home.yml</code>. Edit them, then:</p>
					<pre><code>docker compose up -d</code></pre>
					<p>If you'd rather build it by hand, the compose file is tiny:</p>
					<pre><code>services:
  glance:
    container_name: glance
    image: glanceapp/glance
    restart: unless-stopped
    volumes:
      - ./config:/app/config
    ports:
      - 8080:8080</code></pre>
					<p>Grab the example config to start from, and bring it up:</p>
					<pre><code>mkdir config &amp;&amp; wget -O config/glance.yml \
  https://raw.githubusercontent.com/glanceapp/glance/refs/heads/main/docs/glance.yml
docker compose up -d
# open http://localhost:8080</code></pre>
					<p>No Docker? There are precompiled binaries for Linux, Windows and macOS on x86, x86_64, ARM and ARM64. Drop the binary next to a <code>glance.yml</code> and run it. On Linux it's easy to wrap in a systemd service under <code>/opt/glance/</code>. It also builds from source with Go 1.23 or newer: <code>go build -o build/glance .</code></p>

					<h2>How the config works</h2>
					<p>The whole dashboard is one YAML file, and the structure is three levels deep: <strong>pages</strong> contain <strong>columns</strong>, and columns contain <strong>widgets</strong>.</p>
					<pre><code>pages:
  - name: Home
    columns:
      - size: small
        widgets:
          - type: calendar
      - size: full
        widgets:
          - type: hacker-news
      - size: small
        widgets:
          - type: weather
            location: London, United Kingdom</code></pre>
					<p>Each page becomes a tab across the top, and you can have as many as you like: a Home page, a Homelab page, a Videos page. Columns follow a few strict rules worth knowing up front, because breaking them is the most common first-run error:</p>
					<ul>
						<li>A page has at most <strong>three</strong> columns.</li>
						<li>It must have <strong>one or two</strong> <code>full</code> columns.</li>
						<li><code>small</code> columns are a fixed 300px wide, and <code>full</code> columns share whatever's left.</li>
						<li>Valid shapes: 1 full, 2 full, 1 full + 2 small, or 2 full + 1 small.</li>
					</ul>
					<p>The classic layout is small / full / small: quick-glance stuff on the sides, the main reading feed in the middle.</p>

					<h3>Hot reload</h3>
					<p>Save the file and Glance picks up the change immediately, with no restart. If you save a broken config, it keeps running the last good one and logs the error. One catch: a reload clears the widget caches, so if you save twenty times in five minutes you can trip rate limits on sites like Reddit. Batch your edits.</p>

					<h3>Splitting the file, and keeping secrets out of it</h3>
					<p>Once the config grows, split it up with <code>$include</code>. That's what the compose template does with <code>home.yml</code>:</p>
					<pre><code>pages:
  - $include: home.yml
  - $include: homelab.yml</code></pre>
					<p>Anything sensitive, such as API keys and tokens, can come from environment variables instead of sitting in the file. Glance can also read Docker secrets:</p>
					<pre><code>token: ${GITHUB_TOKEN}
password: ${secret:dashboard_password}</code></pre>

					<h2>A homelab page that earns its tab</h2>
					<p>Here's where Glance stops being a news reader and becomes genuinely useful for anyone running services at home. A second page, three widgets:</p>
					<pre><code>- name: Homelab
  columns:
    - size: full
      widgets:
        - type: monitor
          cache: 1m
          title: Services
          sites:
            - title: Jellyfin
              url: https://jellyfin.yourdomain.com
            - title: AdGuard Home
              url: https://adguard.yourdomain.com

        - type: docker-containers
          hide-by-default: false

    - size: small
      widgets:
        - type: server-stats
          servers:
            - type: local
              name: Services</code></pre>
					<p><strong>monitor</strong> sends a GET request to each URL and shows whether it answered with a 200, plus how many milliseconds it took. <strong>server-stats</strong> shows CPU, memory and disk for the machine Glance runs on.</p>
					<p><strong>docker-containers</strong> needs access to the Docker socket, so add it to the Glance service's volumes:</p>
					<pre><code>    volumes:
      - ./config:/app/config
      - /var/run/docker.sock:/var/run/docker.sock</code></pre>
					<p>Then dress up each container with labels in its own compose file, so it shows a proper name, icon and link:</p>
					<pre><code>  jellyfin:
    image: jellyfin/jellyfin:latest
    labels:
      glance.name: Jellyfin
      glance.icon: si:jellyfin
      glance.url: https://jellyfin.yourdomain.com
      glance.description: Movies &amp; shows</code></pre>
					<p><strong>A word on that socket:</strong> mounting <code>docker.sock</code> gives the container control over Docker on the host, which is effectively root. Glance only reads from it, but if you'd rather not hand it the keys, point <code>sock-path</code> at a read-only proxy such as <a href="https://github.com/Tecnativa/docker-socket-proxy" rel="noopener noreferrer">docker-socket-proxy</a> instead.</p>

					<h2>Build your own widget in five lines</h2>
					<p>The <code>custom-api</code> widget is the secret weapon. Point it at any JSON endpoint and render the result with a Go template:</p>
					<pre><code>- type: custom-api
  title: Random Fact
  cache: 6h
  url: https://uselessfacts.jsph.pl/api/v2/facts/random
  template: |
    &lt;p class="size-h4"&gt;{{ .JSON.String "text" }}&lt;/p&gt;</code></pre>
					<p>Anything with a JSON API becomes a widget: your router, your Home Assistant, a game server, a package tracker. Before writing your own, check the <a href="https://github.com/glanceapp/community-widgets" rel="noopener noreferrer">community-widgets</a> repo. There's a good chance someone already built it.</p>

					<h2>Theming</h2>
					<p>Glance themes are a handful of HSL values (hue, saturation, lightness, space-separated, no percent signs). Change three numbers and the whole dashboard follows. Naturally, here's one in this site's colors:</p>
					<pre><code>theme:
  background-color: 140 30 4
  primary-color: 135 100 60
  contrast-multiplier: 1.2
  presets:
    amber-crt:
      background-color: 30 30 4
      primary-color: 41 100 50
      contrast-multiplier: 1.2</code></pre>
					<p>Presets show up in a theme picker on the page, so you can switch between phosphor green and amber without touching the file. For deeper changes, <code>custom-css-file</code> loads your own stylesheet, and there's a <code>light: true</code> option if you're that kind of person. The repo's <code>docs/themes.md</code> has a gallery of ready-made themes to copy.</p>

					<h2>Locking it down</h2>
					<p>A dashboard that shows your containers and server stats shouldn't be open to the internet. Glance has built-in username and password auth. Generate a secret key and a password hash:</p>
					<pre><code>docker run --rm glanceapp/glance secret:make
docker run --rm glanceapp/glance password:hash mysecretpassword</code></pre>
					<p>Then add them to the top of <code>glance.yml</code>:</p>
					<pre><code>auth:
  secret-key: ${GLANCE_SECRET_KEY}
  users:
    admin:
      password-hash: ${GLANCE_ADMIN_HASH}</code></pre>
					<p>Even with auth on, the safest setup is to keep Glance on your LAN or behind a VPN like WireGuard or Tailscale. If you do expose it, put it behind a reverse proxy with HTTPS.</p>

					<h2>Tips from setting it up</h2>
					<ul>
						<li><strong>Make it your browser's start page.</strong> That's where it pays off. Every new tab is the dashboard.</li>
						<li><strong>Set sensible caches.</strong> <code>1m</code> for monitors, <code>1h</code> or more for feeds, <code>1d</code> for releases. Long caches keep page loads fast and keep you clear of rate limits.</li>
						<li><strong>Use <code>group</code> to save space.</strong> Hacker News and Lobsters in one tabbed widget, or three subreddits in another.</li>
						<li><strong>Feed it the small web.</strong> The rss widget is the heart of Glance. Personal blogs and project feeds are where it shines. This site has <a href="https://spiralnet.io/feed.xml">a feed</a> too, if you want to give it a test.</li>
						<li><strong>Use the <code>releases</code> widget for update-watching.</strong> List the projects you self-host and you'll see new versions the morning they ship.</li>
					</ul>

					<h2>Verdict</h2>
					<p>Glance does one thing and does it well: it turns a morning of tab-hopping into a single page that loads in about a second. Setup is a compose file and a YAML file, the resource footprint is close to nothing, and the custom-api widget means it grows with whatever else you run. If you keep a homelab, or just read a lot of feeds, it's one of the easiest self-hosted wins there is.</p>
					<p>Start with the <a href="https://github.com/glanceapp/glance" rel="noopener noreferrer">GitHub repo</a> and the <a href="https://github.com/glanceapp/glance/blob/main/docs/configuration.md" rel="noopener noreferrer">configuration docs</a>, which cover every widget option in depth. The image is in the <a href="https://spiralnet.io/downloads/">downloads directory</a>, and Glance is in the <a href="https://spiralnet.io/resources/">resource index</a>.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>Windows in a Docker Container</title>
      <link>https://spiralnet.io/blog/dockur-windows.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/dockur-windows.html</guid>
      <pubDate>Sun, 04 Oct 2026 12:00:00 +0000</pubDate>
      <description>A look at dockur/windows, an open-source project that installs and runs Windows inside a Docker container, from Windows 95 to Server 2025.</description>
      <content:encoded><![CDATA[<p><a href="https://github.com/dockur/windows" rel="noopener noreferrer">dockur/windows</a> does something that sounds like it shouldn't work: it runs a full Windows install inside a Docker container. You pick a version, start the container, and it downloads the installer, runs a hands-free setup, and gives you a Windows desktop in your browser. No ISO hunting, no clicking through the installer.</p>

					<h2>How it works</h2>
					<p>It's not Windows running natively in a container. Under the hood the container runs QEMU with KVM acceleration, so Windows gets a real virtual machine with near-native speed. Docker is the packaging: one image, one config file, and a volume that holds the virtual disk. That makes Windows as easy to spin up, back up and throw away as any other container.</p>

					<h2>What it can do</h2>
					<ul>
						<li>Automatic download and unattended install.</li>
						<li>A web viewer on port <code>8006</code>, plus RDP on <code>3389</code> for a proper client.</li>
						<li>Adjustable CPU cores, RAM and disk size.</li>
						<li>A shared folder between host and guest, extra disks, and USB passthrough.</li>
						<li>Custom ISOs, if you'd rather bring your own installer.</li>
						<li>Several networking modes, including giving Windows its own IP on your LAN.</li>
					</ul>

					<h2>Every Windows you can think of</h2>
					<p>The <code>VERSION</code> variable picks the release. A few of the options:</p>
					<pre><code>11      Windows 11 Pro          7.9 GB
11l     Windows 11 LTSC         4.7 GB
10      Windows 10 Pro          5.7 GB
10l     Windows 10 LTSC         4.6 GB
7u      Windows 7 Ultimate      3.1 GB
xp      Windows XP Professional 0.6 GB
98      Windows 98              0.7 GB
95      Windows 95              0.6 GB
2025    Windows Server 2025     7.6 GB
2022    Windows Server 2022     6.0 GB
tiny11  Tiny11                  5.3 GB
reactos ReactOS                 0.1 GB</code></pre>
					<p>The full list is in the project README, and there's a separate <a href="https://github.com/dockur/windows-arm" rel="noopener noreferrer">dockur/windows-arm</a> for ARM64 hosts. Yes, you can boot Windows 95 in a container. Fitting for a site that looks like this one.</p>

					<h2>Quick start</h2>
					<p>Save this as <code>compose.yml</code>:</p>
					<pre><code>services:
  windows:
    image: dockurr/windows
    container_name: windows
    environment:
      VERSION: "11"
    devices:
      - /dev/kvm
      - /dev/net/tun
    cap_add:
      - NET_ADMIN
    ports:
      - 8006:8006
      - 3389:3389/tcp
      - 3389:3389/udp
    volumes:
      - ./windows:/storage
    restart: always
    stop_grace_period: 2m</code></pre>
					<p>Then:</p>
					<pre><code>docker compose up -d
# open http://localhost:8006 and watch it install</code></pre>
					<p>Or as a single command:</p>
					<pre><code>docker run -it --rm --name windows -e "VERSION=11" -p 8006:8006 \
  --device=/dev/kvm --device=/dev/net/tun --cap-add NET_ADMIN \
  -v "${PWD:-.}/windows:/storage" --stop-timeout 120 \
  docker.io/dockurr/windows</code></pre>
					<p>The install takes a while. When the desktop appears, you're in. The default login is <code>Docker</code> / <code>admin</code>. Change it with the <code>USERNAME</code> and <code>PASSWORD</code> variables before the first boot.</p>

					<h2>Tuning it</h2>
					<p>Defaults are 2 cores, 4 GB of RAM and a 64 GB disk. Raise them in the <code>environment</code> block:</p>
					<pre><code>    environment:
      VERSION: "11"
      RAM_SIZE: "8G"
      CPU_CORES: "4"
      DISK_SIZE: "128G"</code></pre>
					<p>To share files, mount a folder at <code>/shared</code> and it shows up as a <strong>Shared</strong> folder on the Windows desktop:</p>
					<pre><code>    volumes:
      - ./windows:/storage
      - ./example:/shared</code></pre>

					<h2>Before you try it</h2>
					<ul>
						<li><strong>You need KVM.</strong> A Linux host with hardware virtualization enabled works. So does Windows 11 with nested virtualization turned on. Docker Desktop on macOS, on Linux and on Windows 10 doesn't expose KVM, so it won't run there.</li>
						<li>Budget at least 2 GB of free RAM and 32 GB of free disk, and more for a modern Windows that you'll actually use.</li>
						<li>Port <code>8006</code> is a full remote desktop. Don't expose it to the internet without something in front of it.</li>
						<li>The project is MIT-licensed and contains only open-source code. It doesn't distribute Windows itself. You're still responsible for having a valid Windows license and following Microsoft's terms.</li>
					</ul>

					<h2>Why it's worth a look</h2>
					<p>Needing Windows for one app, testing something on an old release, or keeping a throwaway Windows box in the homelab used to mean setting up a VM by hand. Here it's a config file and one command. With over 50,000 stars on GitHub, plenty of people agree.</p>

					<p>The links are in the <a href="https://spiralnet.io/downloads/">downloads directory</a> and the <a href="https://spiralnet.io/resources/">resource index</a>. Start with the <a href="https://github.com/dockur/windows#readme" rel="noopener noreferrer">project README</a>, which covers the FAQ in depth.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>Where the Terminal Goes Next</title>
      <link>https://spiralnet.io/blog/whats-next.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/whats-next.html</guid>
      <pubDate>Sat, 03 Oct 2026 12:00:00 +0000</pubDate>
      <description>Ideas for growing the terminal theme, and what a site that looks like a BBS is actually good for.</description>
      <content:encoded><![CDATA[<p>The CRT look is in place: one stylesheet, scanlines, a blinking cursor and a slowly turning sigil. A theme is only half a website, though. This transmission is a scratchpad of where the look could go next, and, more importantly, what a site dressed like an old bulletin board is actually good for.</p>

					<h2>Ideas for the theme itself</h2>
					<p>Ground rule first: the site is fast because it's plain HTML and CSS. Anything below that needs JavaScript should be an extra layer, so the page still works, and still looks right, with scripts turned off.</p>

					<h3>Phosphor switcher</h3>
					<p>Real terminals came in green, amber and white phosphor. Because every color is a CSS variable, a <code>[GREEN] [AMBER] [WHITE]</code> toggle in the footer is a few lines: swap a class on <code>&lt;html&gt;</code> and remember the choice in <code>localStorage</code>.</p>
					<pre><code>html.amber {
  --fg: #ffb000;
  --fg-dim: #805800;
  --fg-bright: #ffe0a0;
}</code></pre>

					<h3>A real command line</h3>
					<p>The prompts are decoration today. A small script could make one of them live: type <code>ls</code> to list posts, <code>cat sniffnet</code> to open one, <code>help</code> for commands. The normal nav links stay as the dependable route, and the prompt is the fun one.</p>

					<h3>Boot sequence</h3>
					<p>A one-time startup screen on the first visit, with a memory check, a modem handshake and <code>CONNECT 2400</code>, before the page draws in. It needs a skip key and must respect <code>prefers-reduced-motion</code>, and after the first visit it shouldn't play again.</p>

					<h3>Keyboard navigation</h3>
					<p>Number keys for menu items, <code>j</code>/<code>k</code> to move through the post list, <code>q</code> to go back. It suits the aesthetic and makes the site genuinely quicker to use for people who live in a terminal.</p>

					<h3>Smaller touches</h3>
					<ul>
						<li>A faint screen flicker or phosphor glow, offered as an option rather than forced on everyone.</li>
						<li>Self-hosting the IBM Plex Mono font, so the site makes no third-party requests at all.</li>
						<li>ANSI-colored art for post headers, rendered with spans rather than images.</li>
						<li>A printable stylesheet that turns any post into a clean, plain printout.</li>
					</ul>

					<h2>What to use the site for</h2>
					<p>The look sets expectations: technical, personal, a little nostalgic, built by hand. Content that fits that tends to feel at home here.</p>

					<h3>A homelab and project log</h3>
					<p>Write-ups of builds, server setups and things that broke, posted as dated transmissions. The <code>ls -lt</code> post list already reads like a changelog, which is exactly what a project log is.</p>

					<h3>Tool spotlights</h3>
					<p>Short posts like the <a href="https://spiralnet.io/blog/sniffnet.html">Sniffnet</a> one: what a tool does, why it's worth trying, and a link in the downloads directory. Over time that adds up to a curated software shelf.</p>

					<h3>A microblog</h3>
					<p>Not everything deserves a full post. A <code>/notes</code> feed of one-to-three-line dated entries ("rebuilt the NAS, 40 minutes, no data lost") would keep the site alive between longer pieces. Add an RSS feed alongside it so people can follow without checking in.</p>

					<h3>Guides and tutorials</h3>
					<p>The <a href="https://spiralnet.io/blog/terminal-theme.html">theme tutorial</a> showed the format works: step-by-step, copyable code, nothing hidden. Networking, Linux, self-hosting and security basics all read naturally in monospace.</p>

					<h3>A link directory and webring</h3>
					<p>The resource index could grow into a hand-picked directory of the small web: personal sites, useful tools, good reading. Joining or starting a webring of similar retro and personal sites fits the BBS spirit of linking to your neighbors.</p>

					<h3>A status board</h3>
					<p>The System Status section on the home page could report real things: uptime of self-hosted services, the latest post date, the last deploy. A static site can still show it if a small scheduled job rewrites the numbers.</p>

					<h3>Community</h3>
					<p>The <code>info@</code> mailbox is open now. A guestbook page, or later a forum, would close the loop and turn a site you read into a place you visit.</p>

					<h2>What to keep</h2>
					<p>Whatever gets built, a few things stay non-negotiable: pages load instantly, everything works without JavaScript, text stays readable (contrast before atmosphere), and the effects never get in the way of the words.</p>

					<p>Got an idea or a request? Send it to <a href="mailto:info@spiralnet.io">info@spiralnet.io</a>.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>Build Your Own CRT Terminal Theme</title>
      <link>https://spiralnet.io/blog/terminal-theme.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/terminal-theme.html</guid>
      <pubDate>Fri, 02 Oct 2026 12:00:00 +0000</pubDate>
      <description>How to recreate this site's retro CRT terminal look with one stylesheet: colors, scanlines, prompts, cursors and the spinning sigil.</description>
      <content:encoded><![CDATA[<p>This whole site is one stylesheet. No images, no JavaScript, no framework. Here's how each piece of the look works, so you can build your own. If you'd rather start from the finished thing, <a href="https://spiralnet.io/css/crt.css?v=2" download="crt.css">grab crt.css</a> and adapt it.</p>

					<h2>1. One font, a few colors</h2>
					<p>The look comes from restraint: a single monospace face and a short palette defined once as CSS variables. Everything else references the variables, so re-theming means changing a handful of lines.</p>
					<pre><code>:root {
  --bg: #000000;
  --fg: #33ff66;        /* body text */
  --fg-dim: #1a8033;    /* borders, metadata, prompts */
  --fg-bright: #aaffaa; /* headings, links */
  --accent: #ff6633;    /* hover and focus only */
  --warn: #ffcc33;      /* tags */
  --mono: 'IBM Plex Mono', 'Courier New', monospace;
}

html, body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--mono);
  font-size: 15px;
  line-height: 1.5;
}</code></pre>
					<p>Load the font in your <code>&lt;head&gt;</code>:</p>
					<pre><code>&lt;link rel="preconnect" href="https://fonts.googleapis.com"&gt;
&lt;link rel="preconnect" href="https://fonts.gstatic.com" crossorigin&gt;
&lt;link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&amp;display=swap" rel="stylesheet"&gt;</code></pre>
					<p>Want amber instead of green? Try <code>--fg: #ffb000</code>, <code>--fg-dim: #805800</code>, <code>--fg-bright: #ffe0a0</code>. Keep the accent a contrasting hue so hover states stand out.</p>

					<h2>2. Scanlines and vignette</h2>
					<p>The CRT feel is two fixed overlays drawn with gradients on <code>body</code>'s pseudo-elements. A repeating linear gradient makes faint horizontal lines; a radial gradient darkens the corners like a curved tube.</p>
					<pre><code>body::before { /* scanlines */
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    0deg,
    rgba(0,0,0,0) 0px,
    rgba(0,0,0,0) 3px,
    rgba(0,0,0,0.08) 4px,
    rgba(0,0,0,0) 5px
  );
  z-index: 100;
}

body::after { /* vignette */
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(ellipse at center, transparent 50%, rgba(0,0,0,0.4) 100%);
  z-index: 99;
}</code></pre>
					<p><strong>Don't skip <code>pointer-events: none</code>.</strong> Without it the overlays sit on top of the page and swallow every click.</p>

					<h2>3. Shell prompts and a blinking cursor</h2>
					<p>Prompts are added with <code>::before</code>, so the HTML stays clean and screen readers aren't fed decoration. The cursor is a block that blinks with a two-step animation, which gives a hard on/off rather than a fade.</p>
					<pre><code>.prompt::before {
  content: 'you@host:~$ ';
  color: var(--fg-dim);
}

.cursor {
  display: inline-block;
  width: 0.6em;
  background: var(--fg);
  color: var(--fg);
  animation: blink 1.1s steps(2) infinite;
}
@keyframes blink { 50% { opacity: 0; } }</code></pre>
					<pre><code>&lt;p class="prompt"&gt;uptime&lt;/p&gt;
&lt;p&gt;&gt; system online &lt;span class="cursor" aria-hidden="true"&gt;_&lt;/span&gt;&lt;/p&gt;</code></pre>

					<h2>4. Bracketed nav and comment-style headings</h2>
					<p>The same pseudo-element trick dresses up navigation and section titles without extra markup:</p>
					<pre><code>nav a::before { content: '['; color: var(--fg-dim); }
nav a::after  { content: ']'; color: var(--fg-dim); }

.section-title {
  color: var(--fg-bright);
  text-transform: uppercase;
  letter-spacing: 2px;
}
.section-title::before { content: '// '; color: var(--fg-dim); }

a {
  color: var(--fg-bright);
  text-decoration: none;
  border-bottom: 1px dashed var(--fg-dim);
}
a:hover { color: var(--accent); border-bottom-color: var(--accent); }</code></pre>
					<p>Dotted and dashed borders (<code>1px dotted var(--fg-dim)</code>) between list rows finish the printout look.</p>

					<h2>5. The rotating sigil</h2>
					<p>The faint spinning shape behind the page is one <code>div</code> with stacked gradients: three thin rings from radial gradients and six spokes from a conic gradient, turning once every four minutes.</p>
					<pre><code>.sigil {
  position: fixed;
  right: -160px;
  top: 50%;
  width: 520px;
  height: 520px;
  transform: translateY(-50%);
  pointer-events: none;
  opacity: 0.06;
  z-index: 0;
  border-radius: 50%;
  background:
    radial-gradient(circle, transparent 38%, var(--fg) 39%, var(--fg) 40%, transparent 41%),
    radial-gradient(circle, transparent 28%, var(--fg) 29%, var(--fg) 30%, transparent 31%),
    radial-gradient(circle, transparent 18%, var(--fg) 19%, var(--fg) 20%, transparent 21%),
    conic-gradient(var(--fg) 0deg, transparent 2deg 60deg,
                   var(--fg) 62deg, transparent 64deg 120deg,
                   var(--fg) 122deg, transparent 124deg 180deg,
                   var(--fg) 182deg, transparent 184deg 240deg,
                   var(--fg) 242deg, transparent 244deg 300deg,
                   var(--fg) 302deg, transparent 304deg);
  animation: rotate 240s linear infinite;
}
@keyframes rotate { to { transform: translateY(-50%) rotate(360deg); } }</code></pre>
					<p>Put <code>&lt;div class="sigil" aria-hidden="true"&gt;&lt;/div&gt;</code> just inside <code>&lt;body&gt;</code>, and give your content wrapper <code>position: relative; z-index: 1</code> so it sits above the sigil and below the scanlines.</p>

					<h2>6. ASCII art headers</h2>
					<p>The banner on the home page is generated text, not an image. Install <code>figlet</code> (or <code>pip install pyfiglet</code>) and render your name in the <code>smslant</code> font:</p>
					<pre><code>figlet -f smslant YOURNAME</code></pre>
					<p>Paste the output into a <code>&lt;pre&gt;</code> and escape any <code>&lt;</code>, <code>&gt;</code> or <code>&amp;</code>. On small screens, shrink it rather than letting it wrap:</p>
					<pre><code>header.hero pre { font-size: 12px; line-height: 1.1; white-space: pre; overflow-x: auto; }

@media (max-width: 640px) {
  header.hero pre { font-size: 9px; }
  .sigil { display: none; }
}</code></pre>

					<h2>7. Keep it usable</h2>
					<ul>
						<li>Give keyboard users a visible focus ring: <code>:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }</code></li>
						<li>Mark decoration (sigil, cursors) <code>aria-hidden="true"</code>.</li>
						<li>Check contrast on your dim color. Dark green on black is the first thing to become unreadable. Use it for borders and metadata, never for body text.</li>
						<li>If motion bothers you or your readers, wrap the blink and rotate animations in <code>@media (prefers-reduced-motion: no-preference)</code>.</li>
					</ul>

					<p>That's the whole trick. The full stylesheet, including post and download-table styles, is <a href="https://spiralnet.io/css/crt.css?v=2" download="crt.css">crt.css</a>. Take it, change the colors, make it yours.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>Sniffnet: Watching the Wire</title>
      <link>https://spiralnet.io/blog/sniffnet.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/sniffnet.html</guid>
      <pubDate>Wed, 30 Sep 2026 12:00:00 +0000</pubDate>
      <description>A quick look at Sniffnet, a free and open-source network traffic monitor.</description>
      <content:encoded><![CDATA[<p><a href="https://sniffnet.app/" rel="noopener noreferrer">Sniffnet</a> is a network monitoring app for keeping an eye on your Internet traffic. Pick a network adapter and it shows you, in real time, how much data is moving and who it's going to.</p>

					<h2>What it does</h2>
					<ul>
						<li>Live charts and statistics for your traffic.</li>
						<li>Domain name, ASN and geographic location of the hosts you're talking to.</li>
						<li>Which programs on your machine are using bandwidth.</li>
						<li>Recognition of 6000+ upper-layer services and protocols.</li>
						<li>Filters, custom notifications and imported IP blacklists.</li>
						<li>Import and export of captures as <code>PCAP</code> files.</li>
					</ul>

					<h2>Why it's worth a look</h2>
					<p>Most packet analyzers assume you already know what you're looking at. Sniffnet is built to be approachable instead, and it's translated into 26 languages. It's written in Rust, it's <strong>free and open source</strong> (MIT or Apache-2.0), and it runs on Windows, macOS and Linux.</p>

					<p>Installers are in the <a href="https://spiralnet.io/downloads/">downloads directory</a>. The <a href="https://github.com/GyulyVGC/sniffnet/wiki" rel="noopener noreferrer">Sniffnet wiki</a> covers setup and the more advanced features.</p>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
    <item>
      <title>First Transmission</title>
      <link>https://spiralnet.io/blog/first-transmission.html</link>
      <guid isPermaLink="true">https://spiralnet.io/blog/first-transmission.html</guid>
      <pubDate>Wed, 30 Sep 2026 12:00:00 +0000</pubDate>
      <description>First transmission from SpiralOut.</description>
      <content:encoded><![CDATA[
					<p>Carrier detected. This is the first transmission from <strong>SpiralOut</strong>.</p>

					<h2>What this is</h2>
					<p>A small, static corner of the web: plain HTML and one stylesheet, no build step and no JavaScript.</p>

					<h2>What's next</h2>
					<ul>
						<li>Transmissions land in the <a href="https://spiralnet.io/blog/">log</a>.</li>
						<li>Useful links collect in the <a href="https://spiralnet.io/resources/">resource index</a>.</li>
						<li>Files go in the <a href="https://spiralnet.io/downloads/">downloads directory</a>.</li>
					</ul>

					<blockquote><p>End of transmission.</p></blockquote>]]></content:encoded>
    </item>
  </channel>
</rss>
