cdn.si

rendered markdown titpetric

Development

Screenshots

Every view, in both themes and on a phone, is in docs/screenshots.md; the pictures are under docs/assets/.

scripts/demo.sh regenerates them: it serves a copy of testdata/demo, sets the instance up through the API with curl, starts real runs with scripts/run.sh, walks every link of every page with scripts/crawl.sh and captures each view with headless chromium through scripts/chromium.sh, the phone views included. A run opens in its window from a link: /runs/<id>#run=<id>; #start=<folder>/<job> starts one; #menu opens the drawer; #actions=<n> opens the ⋯ menu of row n.

Each browser runs in a systemd scope capped at TASK_UI_CHROMIUM_MEM (default 1G) with swap off, and a shot that runs away dies alone. At most TASK_UI_CHROMIUM_PARALLEL (default 2) browsers run at once across all callers; without systemd there is no cgroup cap and only the chromium flags bound the browser. TASK_UI_SHOT_FILTER takes only the shots whose names match the ERE and replaces only their files under docs/assets/.

Under 860px the sidebar is a drawer behind the menu button, with the folders opening into their jobs, and the tables of jobs and runs fold into cards.

Row actions

A row of a table shows its main action as a button and the rest behind its ⋯ button, the same menu on every page: users, keys, secrets, projects, workspaces, schedules, workflows, runs, notes and prompts, artifacts, a folder’s Jobs and Files, and the inventory’s hosts.

A template draws one with the menu template and the functions of the same names:

{{ template "menu" (menu (print "Actions on " .Name)
	(detail "Fingerprint" .Fingerprint "mono" "")
	(item "edit" "Edit links" "href" (print "/settings/keys/" .Name))
	(item "copy" "Copy public key" "data-copy" .PublicKey)
	sep
	(and ($.Can "key/delete") (danger "trash" "Remove" "data-delete" (print "/api/keys/" .Name) "data-confirm" "Remove it?" "data-next" "reload"))) }}

item and danger take an icon of the sprite, a label and pairs of an attribute and its value: href, download, note, disabled (its value the note) and any data-*, aria-* or title, the values escaped; an unknown attribute fails the render. detail takes a label, a value and title or mono. sep ends a group. (and $cond (item ...)) leaves an item out while the condition is empty, and a menu with nothing in it draws no button. menuHTML in task-ui.js draws the same for the rows a script adds. The CSS is in css/menus.css.

A keys table has the same columns on every page, the Keys page, the Instance page, a workspace’s page, Your SSH keys and a user’s edit page: Name, as wide as the names, with the encryption key marked, Algorithm, Size in bits, Public key filling the rest, cut with an ellipsis, with a copy button for the whole line, and the ⋯ menu with the fingerprint, the dates, the purpose and the links in its details.

Lists and actions

Sitemap lists every page with its lists, its head actions, its row actions, its forms and its empty lines. The pages follow one set of rules:

head takes the title, a note, the empty line or a false value, and buttons made with item, primary or danger, a false value left out. A list a filter empties, runs or artifacts, keeps its table and an empty row. - Copy: a value to copy is the copyable template, (list value label [shown [fill]]): the value in code and the copy button right after it, for a public key, an ssh login, a link or a command; fill makes it take the width it has, the value cut with an ellipsis and whole in the tooltip. The same value may also have Copy in the row’s menu. - One template per entity: keys list through keyhead and keyrow, projects through projecttable, a user through user, a row’s menu through menu. A page that lists them again uses those. - Forms: a label above each field, its help in faint text beside the label, Cancel and the primary submit at the bottom right in form-foot. A page of two or more forms or lists puts each in a block with a heading: a workspace’s Settings, Projects and Keys.

Prose

task lint:docs, the atkins job lint:docs, reports what vale finds: every markdown file whole, and the comments out of the Go, PHP, JavaScript and CSS. The style is llm-slop, thirty rules vendored under .vale/styles, so it needs no network. .vale.ini declares the globs and switches the style off for the two vendored trees, the testdata fixtures and the four generated documents.

It is run by name and never from default or lint. The tree is not at zero, so a job that fails on every run gates nothing; the count comes down on its own schedule.

git decides which files vale reads, git ls-files --cached --others --exclude-standard, because .gitignore already names the composer vendor/ trees and the agent worktrees.

Layout

Packages are the bounded contexts, one per domain, singular. The dependency runs one way: app to everything, api and frontend to the domains, the domains to model and storage, model to nothing of ours.

Package Holds
schema the SQL migrations, the source of truth of the data model (mig)
model the mig generated types and the rules on them
storage CRUD per table over pdo, one in-memory database per test
detector the conventions: a Detector per file kind, returning a Source of Targets
layout where a folder ID lives on disk and back: <ws>/home and <ws>/src/<project> under the root, home and src/<project> for internal; the only place that maps IDs to directories, and the layout migration
folder the catalogue: a Folder with its Jobs, discovery under the root, the project configuration
runner sessions: a Process on a pty, the terminal Frame, subscribers, artifacts
scheduler cron Expressions and the Loop that starts due schedules
hours the office hours Rules the runs of a workspace may start in
recording the asciicast files
access who may do what: the password file, Sessions, tokens, the Guard
secret the secrets, encrypted at rest with the instance’s encryption key through its Vault, their scopes and their read log
keystore every key of the instance in one flat folder, what each is for and whose runs get it
envfile the dotenv files of older instances: a workspace bundle carries them, and an import writes them into the folder
sshkey, git, dotenv the key pairs, the checkouts, the dotenv lines it writes
permission what a user may do, from config/permissions.yml and the policies of the user and the role
config the embedded configuration: permissions.yml and context.md
note the notes board: messages, replies and the notes that hand someone an instruction
workflow workflows: named chains of jobs, started one after another as ordinary runs
workspace what a workspace holds beside its checkout: its keys, and its export bundle
agent the host commands of the Claude built-ins, `task-ui agent note
claude Claude Code for task-ui’s own uses, such as the summary of a run’s output
integration the settings of the services task-ui talks to, encrypted with the instance key
beszel the client of a Beszel hub, for the host inventory
forgejo the client of a Forgejo instance and the sync of its organisations and users into workspaces and projects, with the push hook’s signature
artifactstore the files runs leave, as one tree with its history in git
retention the Data retention rules and task-ui cleanup, which applies them
channel task-ui channel, the Claude Code channel of the notes that name a user
event the in-memory bus the pages follow, by topic
sshagent an ssh agent on a socket of its own, serving keys from memory to a run
sshprobe who task-ui signs in to a host as over ssh
hostrun the runs of @host:<host>, a command on a host of the inventory
hostinfo a host’s filesystems and its containers’ sizes over its ssh login, for the expanded row of the inventory
hostpath the paths of the container task-ui runs in as its docker host sees them, for a run’s docker run -v
skills asks atkins what runs in a directory: its pipeline and the skills that apply
tooling asks atkins what the tree has been running, every command with its time, the Report behind the panel on the runs page
markdown markdown rendered as HTML
highlight the coloured HTML of the files a pipeline reads
diskusage how much space a directory takes, files and git history apart
argv the arguments a user types for a job, split as a shell splits words
internal/rulelint matches the permission checks of the code against config/permissions.yml
phpscript the plugin boundary: the Task\Module and Task:: bindings, discovery of a modinfo.php, the runtime pool and the handler that serves /m/<vendor>/<name> (Modules)
phpscripts the modules that ship in the binary, embedded, extracted to the data folder on first start
api the JSON endpoints and the attach socket
mcp the MCP server: JSON-RPC tools that call the API as the token’s user
frontend the pages; frontend/view renders them from embedded templates and assets
app the composition root: configuration, wiring, serving

A convention is a Detector in the detector package: one type that reads a folder and returns a Source, added to the Detectors list. The rest of task-ui runs on sources and jobs and never asks which kind they are.

A module’s PHP is held to the pipeline it ships: phpscript fmt -l reports what the formatter would rewrite and phpscript lint reports undefined names, unreachable code and unused variables. A path of ./... makes either one recursive and a bare . reads one directory; both skip a directory named vendor. Neither reads a sentence, so vale reads the comments instead.

splint ./... (task lint) is the bar: every file has a test beside it, every exported symbol a test that reaches it, every symbol a file named for it, and every dependency one import per package. Tests are black box, parallel, and share nothing. go test ./... runs the unit tests alone; the tests that run processes, open a database or use the network sit behind the integration build tag, see Testing. A test takes under a second, and scripts/test.sh is what says so: it runs the packages a change touched and the packages that import them, then fails on anything over the budget. scripts/test-integration.sh runs the tagged suite one package at a time and splits a package of more than twenty tests into batches of twenty, because the frontend package needs more than the memory cap when its tests run together; it reports the tests over five seconds the same way. Every batch, and every other package, is green before a release. go run ./internal/cmd/rulelint (task lint too) matches the permission checks against config/permissions.yml: every rule a check names is in the file, and every rule the file holds has a check. A check names its rule as a string literal, h.require(w, r, "secret/edit") in Go and {{ if .Can "secret/edit" }} in a template, where the lint finds it. config/context.md tells an agent what task-ui is, area by area, for the MCP instructions and every Claude run; keep it in step with the features. Its test fails when a section or permission of config/permissions.yml, an MCP tool or a built-in job has no line there.


Contents · Running the server · API