I don't recommend this for actual servers it prob has 10,000 backdoors cuz i made it with claude bc idk how to code fabric mods
This mod includes an operator bypass function which was meant specifically for our server, which automatically allows a UUID full OP access, which was obviously made with the intention of only local use.
A Fabric 26.2 mod that adds in-game server administration tools: a live console viewer,
a file browser, a file editor, uploads/downloads, display-name masks and a self-updater —
all driven from /almin, with the sensitive parts gated behind a hardcoded UUID allowlist.
NOT MEANT FOR USAGE IN PUBLIC SERVERS
COMES WITH ABSOLUTELY NO WARRANTY
AUTHOR IS NOT RESPONSIBLE FOR ANY DAMAGES CAUSED AS A RESULT OF THE USAGE OF THIS PROGRAM
Both sides update themselves, and both apply the update at the next start — a jar cannot be swapped underneath a running game.
The server checks GitHub on boot (update-check-on-boot) and, with
auto-update on, downloads the new version and restarts to apply it. When
auto-update-when-empty is on (the default), an update found while people are
playing is queued until the last player leaves. A slow download is staged as a
non-jar and the player count is checked again before anything is replaced, so
somebody joining mid-download is not kicked. The running jar is replaced at
the same pathname, so background mod scanners such as BlueMap never retain a
versioned path that disappears during startup. Almin starts the server again
itself afterwards — see
Restarting — so the update completes without anything else
having to notice.
The web panel is part of that. It is served out of the mod jar, so an
update replaces it too. A browser tab left open notices that the address is
answering with a different version and reloads itself onto the new panel; the
page is sent with no-store so it gets the new one rather than a cached old
one. Pressing Download & install in Settings does the whole thing: install,
restart, and the page comes back on the new version by itself.
The client checks GitHub shortly after launch and every few hours after
that, downloads a newer release into its own mods folder, and uses it from
the next launch. It also matches the version of any Almin server it joins.
Downloads only ever come from this project's own releases — a server supplies
a version number and nothing else, never a URL.
It writes to your mods folder without asking, which not everyone wants. Turn
it off in config/almin-client.json:
{ "auto-update": false, "check-hours": 3 }Almin serves a web panel. It works out of the box — a reverse proxy is optional, and only worth setting up when you want HTTPS.
1. Set a password. Nobody can log in until you do. In game or at the server console:
/almin op web password your-password-here
Or open /almin and use the Web tab, which has a password field — better
than the command, since the command puts your password in chat and in the log.
2. Find the address. The port is picked randomly on first start and written
to config/almin/config.json as web-ui-port. To see it:
/almin op web
3. Open it. http://your-server-address:<port>/
That's the whole setup. There is no step involving Caddy.
Signing in lasts. Logins are kept in config/almin/sessions.json, so restarting
the server — or updating it, which restarts it — does not sign anybody out, and
the cookie has a lifetime, so closing the browser does not lose it either. Both
of those used to happen, and both were consequences of where sessions were kept
rather than decisions about who should be signed in.
web-session-minutes is how long a login is remembered while it is not being
used. Using the panel pushes it out, so it is an idle window rather than a
stopwatch: 30 days by default, and a login you use every week never runs out.
Servers that had the old two-hour default are moved to the new one once; if you
had deliberately set something else, that is left alone.
Three things end a login before the window does:
- Logging out.
- Changing the password behind it. Each session carries a stamp taken from
the account's stored hash, checked on every request — so changing a password
ends that account's logins whether it was changed from the panel, from
/almin op web password, or by editing the file, and whether the server was running at the time or not. Changing your own account's password signs out your other tabs and nobody else's; changing the owner password signs out everyone. - Deleting the account.
The file holds a SHA-256 of each session id rather than the id, the way a
password file holds a hash: somebody who can read it cannot paste its contents
into a cookie and be signed in. It is written rw------- where the filesystem
supports it.
The panel is on by default, and it says so on the server console at startup —
either the address it came up on, or why it didn't. Everything else Almin logs
goes to config/almin/almin.log and never to the console; the panel is the
exception, because a panel that quietly failed to start looks exactly like one
nobody switched on.
If the port it wants is already taken it waits a couple of seconds and tries again, then falls back to a nearby one for that run only — the configured port is never overwritten, so the address you bookmarked comes back as soon as whatever was holding it lets go.
Fixed in 2.8.0, and worth knowing what it was. HttpServer.start() spawns its
own dispatcher thread, outside the pool Almin configures, and that thread takes
its daemon flag from whoever called start() — the Minecraft server thread,
which is not a daemon. So when the game stopped without running its shutdown
hooks, that one thread kept the whole JVM alive: the port stayed bound in front
of a panel with a dead server behind it, and nothing watching for the process to
exit ever saw it. The next start then found its own corpse on the port.
Almin now binds from a thread whose daemon flag matches web-supervisor, so the
website deliberately remains available when Minecraft stops unless you opt out.
On top of that, a restart Almin itself asked for — an auto-update, /almin op restart, or the panel's Restart — arms a watchdog that forces the old process
to exit if it is still running a minute later, because a restart only works if
the old process actually hands over.
And if one is left behind anyway, the next start takes the port back rather than
moving aside. Almin leaves a note in config/almin/web.lock saying which process
holds the port; when a start finds the port busy and waiting has not helped, it
reads that note and ends the process it names — but only when every one of these
holds: Almin wrote the note, it names this exact port, the pid is alive and is
not us, that live process started at the instant recorded in the note (so a
recycled pid cannot match), it is a java process, and it was running from this
same server directory. A port taken by someone else's server is never killed
for it; the panel moves aside there, which is the right answer. Within one
directory there is no ambiguity to begin with — Minecraft's own world lock means
a second live server here is impossible, so anything still holding the port is a
leftover by definition.
If a NAS panel, SSH session or process wrapper disconnects stdin instead, Java
can report Exception handling console input with java.io.IOException: Input/output error. That exception comes from Minecraft's console-reader
thread, not its main server thread. Almin turns the dead pipe into a clean EOF,
so the server and website continue without the stack trace; console commands
remain available through Almin's website even when the host terminal is gone.
To check and fix from in game, /almin op web reports the reason, and:
/almin op web start
stop and restart work the same way, and none of them restarts Minecraft.
/almin → Web has all of it on one screen: Start / Stop / Restart, the
address, whether a password is set, and toggles for the settings worth changing
from a game client.
| Control | Setting | What it does |
|---|---|---|
| Enabled | web-ui-enabled |
serves the panel, now and on every future start |
| Public metrics | web-public-metrics |
the small no-login view |
| HTTPS only | web-require-secure |
refuse admin login over plain HTTP |
| Outlive server | web-supervisor |
keep the panel up while the server is stopped |
| port / bind / mins | web-ui-port, web-ui-bind, web-session-minutes |
edit and press Apply |
Changes take effect immediately — turning Enabled off stops the panel there and then, and a changed port or address restarts the listener on its own.
web-start-command is deliberately not editable from the tab or the web
panel — see Restarting. You should not need to set it at all.
- Without logging in: basic metrics only — versions, uptime, a player count, TPS. No names, no console, no files, no settings.
- After logging in: eight tabs, covering what the in-game admin UI does.
| Tab | What's on it |
|---|---|
| Overview | live metrics, TPS trend, the dashboard rows |
| Console | the server log, tailing, with a command box under it |
| Load | what the server is spending its time on, and where |
| Files | a full-width folder browser — right-click anything for what you can do with it |
| Activity | a timeline map of everyone, and what players have been doing |
| Players | who's online, who's been on before, and display-name masks |
| Mods | one list of the mods advertised to joining players, with the icons and settings behind it |
| Settings | every Almin setting, the admin password, and the update check |
The browser takes the whole page. Each row carries what a folder view is for — the kind of file, its size or how many things are in it, and when it last changed — and every row says whether Almin may write to it, because writes are limited to the configured roots and finding that out by being refused is no way to learn it.
Right-click is the way in. On a file: edit, download, rename, delete, copy its path. On a folder: open, rename, delete. Anything the write rules forbid is shown greyed with the reason rather than offered and then refused, and a jar or a region file is not offered to a text editor at all.
Right-clicking anywhere else — or the + New button — is about the folder you are looking at: upload files, download a link straight to the server, a new file, a new folder. The editor is an overlay now rather than a permanent second column, so it appears when there is something to edit and the browser gets the full width the rest of the time.
The Settings tab has two halves. Almin is everything above; Minecraft
server is server.properties — the game's file, not Almin's — with a typed
control per row (a dropdown for a true/false, a number box for a number), a
filter, and an Undo on anything you have touched.
Editing it here rather than in the file browser is the point: the browser would
let you do it anyway, so doing it properly is strictly better. Values are
written straight away, on the line the key is already on, so the comments
and the order in the file survive — which is more than Properties.store would
have left of them.
Almost everything in that file is read when the server boots and kept in memory
after that, so changes land at the next restart, and the panel says so
where you press Save. Anything whose name says password comes back masked and
is only written if you type a new one: the panel is behind a login, but a
credential that renders into a page is a credential in a screenshot.
The Players and Activity lists show each player's face, cropped out of their skin. Someone who is connected costs nothing — the skin is already in their profile from the login handshake. Anyone who has left, and everyone on an offline-mode server, means asking Mojang; both answers are cached, misses included, so a long history is not a long list of requests.
web-player-heads false turns the whole thing off: no faces, no requests to
Mojang, and the lists draw a coloured initial instead. That fallback is also
what you get for any player whose skin cannot be found, so a cracked server
without the setting changed simply shows initials.
On both maps a player is drawn as their head at the size you actually look for
one, with their name written across the bottom of it rather than on a label
beside it — at six people crossing the same clearing, labels stop lining up
with the faces they belong to. The frame around the head is that player's
colour, so it survives a face that will not load or a server with
web-player-heads off.
Anything that is not simply "standing there" greys the face and puts a short caption on the corner, so it reads without hovering:
| Caption | What it means |
|---|---|
| (none) | Here, and moving. |
afk 10m |
Here, and has not moved for ten minutes — activity-afk-seconds sets the threshold. Nobody is sampled while they stand still, so this stays true when you scrub back rather than only describing right now. |
→ Nether |
They went through a portal. A path is drawn one dimension at a time, so the last point in this one is where they left from, not where they are. |
left 30m |
Off the server, half an hour ago. Where they were is still worth knowing; bluemap-left-player-hours says for how long. |
Hovering any of them gives the whole sentence, with the clock time as well as the "ago" — three hours ago is a number you have to do arithmetic on before you can compare it to anything else you know.
With the 3D map connected, Almin draws every one of these faces itself and asks BlueMap not to draw its own, which otherwise put a second copy of everybody at wherever they are standing this second — beside the one Almin drew where they actually were. That includes the people Almin never records: a trusted UUID or an operator is not in the activity log or in any path, so their face comes straight from the live player list. Turning Players off in the map's own options hands the job back to BlueMap.
Almin's own overlays are kept out of BlueMap's way rather than placed by guess. The bridge measures where the app's interface actually is in the window — what is held against the top corners, which is its menu at one end and its row of map buttons at the other — and the row of who-is-online chips is put below and inside that. It only ever moves the chips further out of the way: on a version whose interface is somewhere unexpected, or before the page has finished arriving, they sit where they always did. Previously the two numbers had been measured once by hand, so on a wider window the chips came down on the buttons, and a chip over a button is a button nobody can press.
The map's own options panel keeps its place while the map refreshes under it. The panel is drawn with the map and the map redraws itself every few seconds, so a rebuilt list used to start at the top roughly as often as you could reach the bottom of it.
Every row in the Players list carries a badge saying whether that client has the Almin mod — the same test the join handler uses to decide whether to send it anything, so it is the truth rather than a guess.
Client on a row that has one opens what it reported: the mod list, the Minecraft and loader versions, the launcher's own name for itself, and the shape of the machine — operating system, version, architecture, processors, and the heap Java was given. That is what a support question needs.
The list is ordered so that what changed does not need reading. Present mods
are alphabetical; ones that arrived since the last join are green with a +
and stay in that alphabetical order; ones that have gone sit at the bottom,
struck through, with the date, for client-mod-history-days (a week by
default) and are then forgotten. A mod that comes back clears its own removal,
so toggling a profile does not fill the list with ghosts, and a version change
is an upgrade rather than an install.
Everything here is self-reported. A modified client can put anything it likes in that packet. It is a support tool and a house rule; nothing that treats it as proof is right about what it has.
What is deliberately not collected: no machine model, no serial number, no
username, no file paths, no addresses. Reading a Mac's model would mean running
sysctl on somebody's computer, and a mod that shells out on a player's
machine to report what it found is a different kind of program from this one.
client-report false turns the whole thing off, and then the client sends
nothing at all.
| Setting | Default | Meaning |
|---|---|---|
client-report |
true |
let the client mod report its mods and machine |
client-mod-history-days |
7 |
how long a removed mod stays listed as recently removed |
Mods → Settings → Restricted mods takes a list of mod ids players are asked
not to run — xaerominimap, as the loader spells it, not the display name.
Every client's report is checked at join: a hit is logged and, with
mods-restricted-kick on, disconnects them with the names in the message.
The section is hidden until Almin required to play is on, and that is not
tidiness. Without the client mod there is no mod list to check, so the rule
would only ever land on the players honest enough to be visible — which is the
wrong half of them. mods-show-restricted puts it back for anyone who wants it
regardless.
Like everything else here it is self-reported, so it is a house rule and not an anti-cheat.
| Setting | Default | Meaning |
|---|---|---|
mods-restricted |
(empty) | comma-separated mod ids to restrict |
mods-show-restricted |
false |
show the section without require-client-mod |
mods-restricted-kick |
false |
disconnect a player running one, rather than only logging it |
Every row in the Players list carries three more things.
A masked player gets a small second row underneath with the face and name of the account they appear as. A mask is another player's name as far as everyone else is concerned, and the useful question — "who does this look like" — is answered by the face rather than by the string. It is a bare name with no UUID behind it, so that face is looked up by name; a mask naming no real account falls back to an initial, which is itself the answer.
Beside it, what they did: one icon per kind of action with how many times in the corner, so a glance says "mostly breaking blocks, one death" without opening anything. For somebody who is connected that means this session — "what have they been doing" asked about a player who is here is a question about now, not about last week — and it says which nothing it is when there is none.
And where they went: their path over the ground it was walked on, framed to fit whatever they actually did. For somebody who is offline that is their last visit rather than their whole week — the week is a scribble, and the walk they took before logging off is a thing you can read. The visit is found from the samples themselves, since a gap longer than twenty minutes is where they were not here; join and leave rows would only work for players who were being recorded at the time. Their action strip follows the same window — someone who spent the day in one room and someone who walked to the badlands both get a picture that fills the box, with a scale bar underneath saying which is which. Without the bar the two would look identical, which would be worse than no map at all.
Activity on any row — and the button on the corner of the little map — opens that player on the big map with everyone else filtered out.
Everything above answers "what has everyone been doing". The other question an admin asks is about one person, and it used to be spread across three menus and a filter: who they are in Players, what they did in Activity, how long they have played nowhere at all.
A player's name is now a way in to them, wherever it appears. The Players list, every row of the activity log, the list beside the map, the legends under both maps, the bar of who is online, the card a cluster of marks opens, the colour key on a picture of a build, and any name the AI menu writes into an answer. A name is drawn with a dotted underline and pressing it opens a sheet about that player. Where a name already had a job — the chips that filter the map to one person — the chip still filters and the name inside it opens; on the flat map's heads and the figures in a build, which are places before they are names, the player is on the second mouse button.
The sheet holds:
- The account. How long Almin has watched them play, how many times they joined, when they were first and last seen. If they are connected: this session, where they are, health and food, level and game mode, and their coordinates unless this account is not allowed to see coordinates.
- Days. One bar per day they did anything, oldest on the left. The shape is the point — a fortnight of evenings looks nothing like one very long night.
- What Almin saw. Their last rows out of the activity log, through the same filters as everything else: an account restricted to its own player can open itself and nobody else, and one that cannot see coordinates does not get them here either.
- The game's own numbers. Minecraft keeps a statistics file per player, and it reaches back past the day Almin was installed. Time played, deaths, kills, distance walked, damage; then what they have broken, used, made and killed most of. Ticks, centimetres and half-hearts are turned into hours, kilometres and hearts.
- What they are carrying — see below.
- Who has looked in their inventory.
Nothing on the sheet is written anywhere. It reads Almin's log, the world's
stats/<uuid>.json, and — only when asked — playerdata/<uuid>.dat.
This is the most intrusive thing the panel can do, so it is built to be difficult to do quietly.
It is hidden until you ask. The sheet shows a button and a sentence, not an item list; no poll and no redraw fetches it. The item list is a POST of its own, behind the Players menu and a session good enough to change something — an account with read-only access to Players can see the button and cannot press it.
Pressing it is written down, before the read happens, in
config/almin/inventory-looks.json: when, which panel account, and whose
inventory. Never what was in it — a copy of somebody's things, kept forever in
a second file, would be a worse problem than the one this is here to solve.
There is no account this is off for and no setting that turns it off, including for the owner of the panel. It is deliberately not the panel audit log, which records watched accounts and never records the owner, and it is not the activity log, which is about the world and is thrown away on the activity retention clock. It is its own file, it holds the last 600 looks, and the record is shown at the top of the same sheet the looking happens in — so the next person to open that player sees who has been there. A record nobody reads protects nobody.
The console says it too: [almin] <account> looked in <player>'s inventory.
A world reset clears the record, because the record is about that world.
What comes back is drawn as the inventory screen, not as a table of item names: the same grey panel, the same sunken squares, armour up the left with the off hand beside the boots, the pack, a gap, and the hotbar. Empty squares are drawn too — the shape of what is missing is half of what an inventory says. The ender chest is a second panel because it is a second container, and anything whose slot number the game does not place lands in a third, so nothing is dropped for not fitting the picture.
Item pictures come from whatever resource pack the server has (drop one into
resourcepacks/ or config/almin/textures.zip); a square with no texture for
it shows the item's initial instead, and hovering any square names the thing
and its id. A block held in a hand has no icon of its own in the game — that
picture is built from the block model — so one face of the block is used, which
is not the same picture but is recognisably the same material.
When the player is connected the panel shows what they have on them right now, including armour, off hand and ender chest. When they are not, it comes from their save file and says how old that is — the server writes it out on its own schedule, so what an offline player is "carrying" is whatever was last saved.
The header carries Stop, Restart and Start for the Minecraft server itself. Stop means stop. Restart genuinely restarts: Almin stops the server and then starts it again from this machine, without needing a wrapper script to notice the exit. The page goes quiet while the server boots and comes back on its own. Start is for a server that is down while the panel is still up. Both are covered in Restarting.
Two settings are deliberately not editable from the web panel:
web-admin-password-hash, which has its own field that hashes what you type,
and web-start-command, which becomes a command on the host OS.
What an admin login is worth. It can write to your mods/ folder and
restart the server, so it is equivalent to running code on the machine. That is
inherent in what the panel is for, not a gap to be plugged — treat the password
accordingly, and don't put the panel on the internet without TLS.
A restart is two things: stopping the server, and starting it again. Almin used to do only the first, and trust that something outside — a wrapper script, a systemd unit, a host panel's auto-restart — was watching for the exit and would do the second. On a server where nothing is watching, every feature that "restarts" simply stopped the server and left it stopped, and nothing anywhere said that was what had happened.
Almin can do the second half itself, and you do not have to configure how. This JVM already knows the command line it was launched with, so running that again is a faithful restart: same java binary, same heap flags, same jar, same arguments, same environment, same working directory. The panel's Settings tab shows the exact command it would run. Self-relaunch is on by default so a directly launched server actually comes back; hosted or container installs whose own supervisor handles restarts should switch it off.
The wait has a clock on it. Pressing Restart or Start opens the same countdown an update opens: a number, a bar, and a line saying what is being waited for. It closes itself the moment the server is back, and closing it early costs nothing — the count carries on in the banner behind it and the page reconnects on its own either way. The two waits differ in one thing, which is what running out means: an update replaces the panel as well as the server, so the page reloads onto the new one; a restart leaves the panel exactly where it was, so nothing is reloaded and the dialog simply says it is still waiting.
This is what Restart and Start in the panel do, what /almin op restart
does, and how an auto-update applies itself. The order matters and is deliberate:
the new server is started first, and only once it is genuinely on its way
does the old process give up its port and exit. If the launch fails there is no
handover at all — the panel stays up saying why, which beats exiting into a
server that is down with nothing left to bring it back.
| Setting | Default | Meaning |
|---|---|---|
web-supervisor |
true |
keep the entire website available while stopped |
web-restart-relaunch |
true |
handle Almin restarts here without keeping a stopped-server website permanently |
web-start-command |
(blank) | run this instead of re-running this server's own command line |
Turn web-restart-relaunch off when a wrapper, container policy, host panel,
or service already restarts this server. Otherwise both can start one, and
the second to reach the world loses to Minecraft's own session.lock.
The two switches are deliberately independent, as they were in the original
restart system. web-supervisor only preserves the website. It does not make
Almin take restart ownership away from a host wrapper. All seven authenticated
pages stay reachable after Minecraft stops when it is enabled; actions that
need a live server say that it is stopped, while Start server remains
available without a timeout.
Restart and Start use the original launch-first handoff: the replacement is
successfully created before the old website releases its port and exits. The
replacement must now reach Minecraft's SERVER_STARTED event too; a Java
process that spawns and then dies during mod or world loading is still a failed
launch. The current website remains untouched, reports the failure, and allows
Start to be tried again. Slow servers are allowed up to ten minutes to finish
startup before the attempted child is stopped and reported as failed.
Turn web-supervisor off only when an external wrapper must see the Almin JVM
exit after an ordinary server stop.
Only a stop Almin was asked for becomes a restart. An ordinary /stop, a
crash, or the machine shutting down are not restarts and are never turned into
one.
web-start-command is deliberately not editable from the web panel or the
in-game Web tab: it is the one setting that becomes a command on the host OS, so
it stays in config/almin/config.json and /almin config. You should not need
it — it exists for a server whose real start procedure is more than its own
command line, and for platforms that will not report a process's arguments.
The panel speaks plain HTTP. On a home or private network that is usually fine. Over the internet it is not: your password and everything you do crosses the network in the clear.
Two settings control this, and neither gets in your way by default:
| Setting | Default | Meaning |
|---|---|---|
web-ui-bind |
0.0.0.0 |
reachable from other machines. Set to 127.0.0.1 to restrict it to the server itself |
web-require-secure |
false |
when true, admin login is refused unless the connection is HTTPS (via a proxy) or from the server itself |
Leave both alone and it just works. Once you have TLS in front, set
web-require-secure true and the panel will stop accepting plaintext logins.
Only if you want encryption. A starter config/almin/Caddyfile is written on
first start. Point it at your domain, then:
caddy run --config config/almin/Caddyfile
It is deliberately scoped so it cannot collide with anything else on the
machine: it never binds port 80 or 443 (it publishes on its own HTTPS port and
gets certificates over DNS, which needs no inbound port), and it keeps all its
state inside config/almin/.
With the proxy running, tighten things up:
/almin config web-ui-bind 127.0.0.1
/almin config web-require-secure true
That makes the panel reachable only through the proxy, over HTTPS.
Security note: an admin login is remote control of the server — a terminal and file writes. Use a strong password, and don't expose the panel to the internet without TLS in front of it.
Everything /almin does has a screen on a modded client. Vanilla clients and the
server console get the same information as chat, so nothing is only reachable
one way.
| Screen | Command | What you can do there |
|---|---|---|
| Dashboard | /almin |
metrics, and the way through to everything else |
| Console | /almin op console |
the live server log |
| Files | /almin op dir |
browse, edit, download, upload, rename, delete |
| Web | /almin op web |
run the web panel and set its password |
| Activity | /almin op activity |
a timeline map, and what players have been doing |
| Shared | /almin files |
the shared folder, one click to download |
| Mods | /almin mods |
advertised mods, required toggle, remove |
| Masks | /almin mask |
set and clear display names, and op players |
| Config | /almin config |
every setting, typed |
| Updates | /almin update |
version, check, install |
A tab strip along the bottom moves between them, so no screen is a dead end. The strip drops its lowest-priority tabs on a narrow window rather than running off the edge.
The five list screens are all one screen underneath. The server sends rows and the command each button should run; the client renders them and re-issues the command, which is re-checked exactly as if it had been typed. That is why a whole set of admin surfaces needs no permission logic of its own — there is nothing to get wrong, because the buttons are only ever commands you could have typed yourself.
A Backups menu that copies the world into a zip, on a clock or when you press the button, and deletes the old ones so the folder does not grow forever.
It is off until you turn it on. Every other default in Almin costs a few
kilobytes; this one writes a copy of your entire world to a disk Almin knows
nothing about the size of. That is not a decision a mod should make on your
behalf the first time it boots, so backup-enabled starts false. Manual
backups work regardless — the button is always there.
A backup is the world, plus the files that decide who may join it. The world
folder, server.properties, ops.json, whitelist.json and the two ban lists.
Not the mods, not the configs, not Almin's own folder: those are the things you
can put back by reinstalling, and the world is the thing you cannot. A backup
that took an hour and filled the disk because it also copied every jar is a
backup you keep too few of.
It is taken the way you would take one by hand. Autosave is turned off and
every level is flushed to disk first, so what gets copied is a world that has
finished being written; the copying then happens off the server thread, so the
game keeps running while it does; autosave goes back on afterwards whether the
copy worked or not, because it is in a finally. The zip is written under a
.part name and moved into place only when it is complete — a half-written file
that looks like a backup is worse than no backup at all, and a failed run
deletes its own wreckage.
Three rules decide what gets deleted, applied after every new backup:
| Setting | Default | What it does |
|---|---|---|
backup-enabled |
false |
Take one on a clock at all |
backup-interval-hours |
6 |
How often |
backup-keep |
8 |
How many to keep |
backup-keep-days |
14 |
Delete anything older (0 = no age limit) |
backup-max-gb |
20 |
Ceiling on the whole folder (0 = no size limit) |
backup-skip-unchanged |
true |
Skip the automatic one when nothing has happened |
backup-folder |
backups |
Where they go; absolute paths are honoured |
The newest is never deleted by any of them. Set backup-keep to 1 and you
get one backup, not none; a folder where everything is older than
backup-keep-days still keeps the newest; a single backup larger than
backup-max-gb is still kept. A retention rule that can empty the folder is not
a policy, it is the bug you discover on the day you need the file that is gone.
Deleting the last one is something only a person can do, from the menu, after a
confirmation that says which one it is.
backup-folder takes an absolute path, and putting it on a different disk is
the arrangement that actually survives the failure backups exist for. If you
point it inside the world folder, backups are not copied into backups.
A quiet server does not fill the folder with identical copies. With
backup-skip-unchanged on, an automatic backup is skipped when nothing has
happened since the last one — otherwise a server nobody plays on spends a week
pushing its real backups off the end of the retention rules with copies of the
same untouched world. Pressing the button is never skipped.
There is no restore button, and the menu says so rather than hiding it. Putting a world back means stopping the server, moving the live world out of the way and unpacking over it. A browser button that does that is one misclick from deleting exactly what these files exist to protect. The zips are ordinary zips: download one, stop the server, unpack it yourself.
Backups are their own menu in the account system, so an account can be given them read-only — able to see what exists and download it, but not to make or delete one. Every download is written to the Almin log with the account that asked for it.
The Overview tab says how the server is doing: ticks are taking 71ms, memory is at 84%. Neither number tells anybody what to do about it. The reason a Minecraft server is slow is almost never mysterious — it is nine thousand items on a hopper floor, or a villager hall somebody left running, or a chunk loader in the Nether that has been ticking a mob farm for a week with nobody near it — but finding out which of those it is means running commands and guessing at coordinates.
The Load tab samples the running server and reports the things you can actually go and change. It opens on a sentence rather than a number:
working hard — There is headroom, but not much. A busy moment will be felt. The busiest ground is Overworld around -211, 908 — 6,120 entities and 1,802 ticking blocks in about 9 chunks. 12 chunks are force-loaded, so they keep ticking whether anybody is there or not.
Under it: tick time against the budget the server actually has (a server on a non-default tickrate is judged against its own), the slowest of the last hundred ticks, memory with its collection count, and what is loaded. Then five lists — the busiest patches of ground, what there is most of, which blocks are ticking, how the worlds compare, and what is loaded around each player, which is the closest thing there is to a per-person bill.
Every hotspot is somewhere. Click one and the Activity map opens on it. "4,812 items" is a fact; "4,812 items at -211, 908 in the overworld" is a decision.
Nothing, until somebody looks. A sample is one pass over every entity and every ticking chunk, which is not free, so it does not happen unless the Load tab is open — opening it is what wakes the sampler, and it goes back to sleep on its own about a minute after the last person closes it. While awake it samples at most once every four seconds, however often the panel refreshes. The footer says how long the last pass took.
Sampling runs on the server thread, because entities and chunks belong to it, and the panel reads whatever the last pass produced rather than blocking on one.
There is deliberately no per-entity timing. Attributing milliseconds to individual entities means instrumenting the tick loop, which costs something on every tick forever in exchange for a number that is only read for a minute at a time. Counts and positions answer the same question.
Load is its own menu, granted separately like every other. An account given Activity to read the log is not thereby told what the server is doing now. Accounts that existed before this menu did start without it — a new menu is off for everybody but the owner until somebody grants it, which is the same rule every other menu has always followed.
Positions are the one part of a sample that is about somebody's build rather than about the server, so an account that is not shown coordinates gets the counts and not the hotspots — "there are nine thousand items" says nothing about where anybody lives.
The AI menu gets the same thing as a tool, server_load, behind the same
menu and the same coordinate rule.
Almin keeps three logs. Two are about the server — Minecraft's own console, and
config/almin/almin.log for what admins do. The third is about players.
/almin op activity in game, or the web panel's Activity tab, shows what
ordinary players have been doing: joins and leaves, chat, commands, blocks
placed and broken, containers opened, PvP hits and deaths.
A masked player joining is announced where only admins see it: the server
console, almin.log, and the activity log. Never in chat — a mask exists so
other players see the other name, and "X is really Y" in chat would undo it.
Every admin surface shows the real name first with the mask beside it.
By default it records nobody who can read it. A player is skipped entirely if their UUID is on the trusted allowlist, or if they hold moderator permission or above — which is every vanilla op. So out of the box it is a record of the unprivileged, kept by the privileged, and never a record of the people keeping it.
That default is the point rather than a limitation, but it is not a rule. Turn
activity-include-admins on and everyone is recorded, ops included — for an
audit, or a server whose staff have agreed to it.
More often what you actually want is this afternoon, not forever, so there is a second form that forgets by itself:
/almin op activity admins temp on
That overrides the setting until the next restart and is never written to
config.json — a switch you have to remember to turn back off is a switch that
stays on. admins temp off excludes admins on a server that has the setting on;
admins temp clear hands control back to the setting; /almin op activity admins on its own reports which of the two is deciding. The Activity tab in
both UIs has the same two controls, and says which one is in force.
Rows expire. This is data about named people, so it has a deliberate shelf
life rather than accumulating: five days by default, from memory and from
config/almin/activity.log alike. Five is long enough that "what happened over
the weekend" is still answerable and short enough that this stays a working
record rather than an archive; activity-retention-minutes moves it anywhere
from five minutes to thirty days.
A server that was already running gets the new default too. Every setting is
written to config.json on first start, so a changed default would otherwise
only ever reach a fresh install — the file carries a version, and the one
change to five days is applied once, and only where the value is still sitting
on the old one-day default.
| Setting | Default | Meaning |
|---|---|---|
activity-log |
true |
record at all |
activity-include-admins |
false |
record ops and trusted UUIDs too |
activity-retention-minutes |
7200 |
how long a row survives — 5 minutes to 30 days |
activity-max-entries |
20000 |
ceiling on the log; oldest rows drop first |
activity-blocks |
true |
blocks placed, blocks broken, blocks used |
activity-combat |
true |
damage taken, hits landed, deaths |
activity-items |
true |
item use, entity interaction, containers |
activity-track-seconds |
5 |
position sampling for the map; 0 turns it off |
web-player-heads |
true |
player faces in the panel's lists and on the map; off means Almin never asks Mojang |
activity-afk-seconds |
20 |
seconds of not moving before a player counts as away; 0 turns it off |
Recorded: joins and leaves, chat, commands, blocks placed, blocks broken, blocks used, containers opened, item use, entity interaction, hits landed, damage taken and by what, deaths, and respawns.
Placing is its own action. It used to be filed as a use — dirt placed on dirt read as "used Dirt on Dirt" — because Fabric has no placement event and the one that does exist fires before the interaction resolves, when putting a block down and opening a chest still look identical. The row now waits until the end of the tick: if a block actually went down it becomes a placement, and otherwise it is written out as the use it always was. One right-click is still one row.
The web Activity tab has two renderers. With no extra mod it uses Almin's recorded 2D ground snapshots described below. Install 3D world map in the Activity heading installs BlueMap as a separate optional Fabric mod; after one restart its full rendered world becomes the main canvas, while Legacy 2D always remains beside it. Timeline/playback, filters, paths, faces, clusters, fading, sequences, coordinate grid, side list and fullscreen work with either renderer. In the 3D world, build scenes expand at their real X/Y/Z coordinates instead of opening over the map. See the BlueMap integration boundary for installation, security, removal and the files it owns.
Travelled player paths fade segment by segment on the same clock as activity marks in both renderers; during playback, the faint guide is only the path ahead.
Almin neither compiles against nor redistributes BlueMap. Its integrated web server is forced onto an instance-local loopback port and reached only through the authenticated Almin origin, including through the existing Caddy proxy.
Both Activity tabs open on a map of everyone, on one clock: each tracked player's path from above — X across, Z down, the way Minecraft's own maps read — with the things they did marked along it. Drag the timeline to move through the period, or press Play to watch it. Paths draw up to the cursor and recent marks stand out, but nothing disappears: scrub to a quiet minute and you still see where everything happened. It answers the question you usually start with, which is not "where has this person been" but "what happened here, and who was around".
Underneath it, the web panel still has the single-player view: pick a name for that player's path on its own, and hover any marker for what happened and when.
Dimensions are drawn separately, because overworld and nether coordinates share numbers without sharing places. A labelled switcher under the timeline names them the way people do — Overworld, Nether, The End — and lists any dimension there is either activity or a picture of the ground for, so somewhere nobody has been since the log rolled over is still somewhere you can look at. In game, click the map to cycle.
Every action has its own shape, so a glance says what happened rather than only that something did. A block put down is a solid square; one taken away is the outline it left. Attacks are crossed swords, a hit taken is a burst, chat is a speech bubble, a container is a chest, arriving and leaving are arrows in and out. They are drawn, not fetched: the panel has to work on a server with no way out to the internet.
The map is a viewer, not a picture. Scroll to zoom about the pointer — the block under the cursor stays under it — and drag to move. The buttons in the corner zoom in, zoom out, and put the framing back to everything in view.
Every player's face is drawn where they were at the cursor, so scrubbing moves them along their paths. Somebody who has logged off by then is drawn smaller, greyed, with a small arrow on the corner saying they left there — a path ends where somebody logs off, and a face left standing at full size says they are still there, which after an evening of people coming and going is a map full of players who all went home hours ago. Rejoining undoes it. A face also goes grey once nobody has moved it. Nobody is sampled while they stand still, so the gap between the cursor and their last sample is exactly how long they have not moved — which makes the grey right when you scrub back, rather than only describing right now. Clicking a face — or a name in the legend, or anyone in the online strip — focuses that player: the map, the timeline ticks and the side list all drop everyone else. Click again to get them back.
A coordinate grid sits under everything, on round block coordinates chosen for the zoom and labelled with them, with x=0 and z=0 brighter than the rest. It replaces four lines at quarters of the screen, which moved when you panned and stood for no coordinate at all — a world people navigate by numbers should show the numbers. The cog turns it off.
Marks keep their size on the screen rather than in the map's own coordinates, so making the map bigger shows more map instead of bigger marks. Without that, fullscreen made a mark that read well in the panel fill a building.
On a wide enough screen a panel opens beside the map with what happened, chat included, newest at the cursor first. Clicking a line takes the map to that moment and that place. Above the map, a strip shows who is online now, with anyone who has stopped moving greyed out.
Marks that crowd become one box with a number on it. Zoom out over an
evening's mining and forty overlapping shapes are four boxes; zoom in and they
separate again, because the grouping is by distance on screen rather than by
distance in the world. Clicking a box lists what is inside it, and identical
things fold with a count — fifty rows of "broke Stone" is one line saying
break ×50, which is both shorter and more informative than fifty lines.
Clicking one of those lines takes the timeline to it, and the box stays put
through a repaint, a playback frame or a live refresh, following its marks as
you pan and zoom.
Fullscreen hands the whole window to the map, with the timeline, the
controls, the side list and the legend floating over it instead of sitting
around it. They are the same elements in the same order — only positioned
differently — so going in and out cannot lose where you were looking. Esc
comes back.
Marks fade with recency out of the box — what happened just now stands out and everything else stays legible, with a high floor, because on a long period almost everything is old and fading those away would empty the map of the marks it exists to show.
Fade old marks away is a different thing and is off until you ask for it:
past its window a mark is not drawn at all. It is set per group — the world,
fighting, talking, coming and going, things, and sequences — because "stop
showing me week-old chat" and "stop showing me week-old block edits" are
separate wishes. A box of grouped marks is as visible as the freshest thing in
it and goes once everything in it has. None of this deletes anything: it is
about what is drawn, and what is kept is activity-retention-minutes.
Filter opens the tree of what is actually in the period: kinds of thing
grouped into the world, fighting, talking, coming and going, and things — with
a count beside each — and under place, break and the rest, the particular
blocks and items, also with counts. Ticking a kind shows that kind; expanding
it and ticking Oak Log narrows to that. Unticking the kind lets go of the
things under it, so the map never hides rows for a reason that is no longer on
screen.
Sequences are in the same tree. Ticking fight shows everything that player
did while the fight was going on rather than only the swings, which is the
honest reading of the question. The filter reaches the map, the timeline ticks
and the side list together, so all three agree about what is being shown.
When a model is enabled, the question box above the map has two separate
actions. Ask answers from the selected Everything/player/area scope and the
visible timeline window without moving the map. Find on map also applies
the supporting player, action, item and episode filters so the answer can be
inspected. Both are grounded in the server's own Activity rows; chat text is
withheld when ai-send-chat is off.
A stretch of work gets a badge with the tool on it — a pickaxe for a shaft, an axe for felling trees, a hammer for a build, a sword for a fight, boots for a journey — worked out from what was actually broken, so digging through sand gets a shovel and digging through stone gets a pickaxe. Where the server has textures they are the game's own iron tools; where it has none they are drawn, which is the same fallback the ground has. Point at one and it says what it was — and what the model said about it, once one is connected. It waits for the pointer on purpose: a dozen sentences drawn across the map cover the map they are describing, and the badge already says what kind of work it was, which is what a glance needs. Clicking one opens it.
The map answers where; a mark from above cannot answer what shape. The log knows every block that went down and every one that came up, with its height, so the shape is recoverable — and 3D on a build, a shaft, a tunnel or a clearing draws it in isometric: placed blocks solid and yellow, broken blocks as red outlines, because what is being shown there is an absence. Anything hit during it is a red burst where it happened.
Blocks are drawn in the colour they actually are — the server knows, from its own registry and from the texture where there is one — with the yellow or red on the outline. Two questions, two channels: a wall of solid yellow only said "somebody put blocks here", which the sentence above the picture had already said.
The ground around it is the world itself, in three dimensions: snapshots carry a height for every column beside its colour, so the land is built out of the same blocks the build is — a top face per column at its own height and a side face wherever the ground next to it is lower. A hillside is a hillside rather than a picture of one. Ground turns it off.
Heights are stored beside each whole picture, not each difference: ground moves slowly, and the shape from the nearest keyframe is the same shape. A snapshot taken before this existed has no heights, and the scene is then the blocks alone rather than an error.
Turn it a quarter at a time to see round the back, and drag the slider to watch it go up in the order it actually went up. The block size and the framing come from what is in the scene, so it fits its window rather than running off the edge of it.
An episode is cut by time and by distance from a running centre, so a player who dug a hole, walked thirty blocks and dug another lands both in one run — and two heaps thirty blocks apart drawn in one picture are two pictures. Blocks within a dozen of each other are one piece of work; the rest is left out, and the count under the picture says how much. It is clamped to 64 blocks across, and only what changed is in it.
The cog in the top corner opens the adjustments that are about your eyes rather than about the server, and they are there rather than in Settings because they are the things you change while looking at the thing they change: ground darkness, path width, marker size, face size, whether marks are coloured by what they were or by who did them, whether faces, paths, grouping, sequence badges and the side panels are drawn at all, how often the live map refreshes, and whether old marks fade away. Faces have their own size because they are the thing you look for: tying them to the marker size meant making them readable made everything else shout. They are remembered in the browser, so two admins looking at the same map are allowed to disagree about how dark the ground should be.
Two strips. The thin one on top is the whole period with the visible slice marked; the one below is that slice, drawn large. Scroll it to zoom about the pointer, drag it to scrub, and drag the top strip to move the window. Back to live puts the whole period back in view.
The period is exactly what is saved, end to end, so zooming out stops at the
oldest and newest rows there are. It used to run to the clock instead, which on
a server nobody had touched since yesterday meant most of the timeline was
empty and there was nothing to zoom out to but blank. Live still means the
newest thing there is, and says live · nothing since 3h ago when that is
older than a couple of minutes, rather than implying the map is showing now.
It opens live. "What is happening" is the question you arrive with;
"what happened at four o'clock" is the one you come to second. So the cursor
follows the clock, the bar under the timeline says LIVE where Play usually is,
and the playback controls are not there — live, there is no speed, no direction
and no position to set. Touch the timeline and they all come back, along with
Back to live. The map refreshes itself every ten seconds while live, keeping
wherever you have panned and zoomed to.
The period runs to the clock rather than to the last thing anyone did. On a quiet server those are an hour apart, and a timeline that stopped at the last row would say the map was showing an hour ago.
Stretches when nobody was on are hatched and labelled, and Skip quiet time makes playback jump over them — an empty map is not worth watching in real time. Playback loops: reaching the end of the visible slice sends it back to the start of that slice, so zooming the timeline picks what to watch.
Speed is a real multiple of recorded time — at 60× one second on screen is a
minute that was lived, and the readout beside the buttons says so. It follows
the clock rather than the frame rate, so a busy page or a background tab
changes how smooth it looks and not how fast it runs.
A player counts as away after activity-afk-seconds (twenty by default) of not
moving. Standing still is the only signal a server can be sure of: a client
that has stopped sending movement is indistinguishable from a player who has
stopped moving, which is what AFK means. Going away is recorded as an action
like any other, so it shows on the map and in the log.
The web map is drawn over a picture of the actual ground, and that picture changes with the timeline — scrub forward and a build appears.
Almin takes them itself, on a timer: a top-down raster of the loaded area around whoever is playing. They are kept with a timestamp, and the map shows the newest one taken at or before wherever the cursor is.
It is the same idea as a vanilla map but shaded the way the web world maps are. Vanilla gives every block one flat colour out of a small palette and picks between three brightnesses, which is why a vanilla map reads as blotches: a beach and a desert are the same yellow, and a hillside only shows at all if it happens to face north. Here relief comes from the slope in both directions, so a hill is a hill whichever way it faces; and water is darkened and blued by how deep it is, so a coastline and a shelf are visible.
And where it can, it uses the game's own textures. At one pixel per block there is exactly enough room for one texel, so a block takes its texture's colour nudged by the texel that falls at that position — which means a sand field comes out grainy the way sand actually is, a floor of oak planks comes out striped the way planks actually are, and sandstone stops being the same yellow as sand. It tiles the real 16×16 file across the world, so it is the game's grain rather than a rule somebody wrote about sand.
Textures do not come from the server: a dedicated server jar ships none,
because nothing on a server ever draws a block. So Almin looks for them, in
order, in config/almin/textures.zip, config/almin/textures/, any
resourcepacks/*.zip or unpacked pack folder, and finally its own classpath
(which has them in a development run). Any resource pack will do — drop one
into resourcepacks/ and the next snapshot is textured. The legend under the
map says which it is using, or map palette if it found none, and finding none
is not an error: the map falls back to the palette with an invented grain, and
looks the way it did before.
A texture is only used where it is a surface: a cross-shaped plant, a pane of glass or a torch is mostly empty, and averaging one into a single pixel gives a colour nothing in the world is. Those keep the map palette. Greyscale textures — grass, leaves, water — are tint masks the game colours by biome, and nothing here knows the biome; the texture supplies the pattern and the palette supplies the green.
Reading a texture never happens on a game tick: a block Almin has not seen before takes its palette colour for that picture and is loaded on a daemon thread for the next one.
Where the top of a column has no colour of its own — glass, a barrier, a light block, scaffolding — the sampler looks down past it for a block that does, rather than reporting a hole. A glass roof used to punch one straight through the map, and the ground was there all along; it just was not the block being asked.
Only chunks the server already has loaded are drawn; anything else stays transparent. Generating terrain in order to photograph it would be an enormous cost for a picture nobody asked for, so the picture is of where people are — which is what the activity log is about anyway.
Sampling has to happen on the server thread, because block states belong to it, so it is deliberately bounded; encoding the PNG and writing it happen on a daemon thread afterwards, where they cost nothing. One snapshot is taken at a time, so a slow disk delays the next rather than queueing up.
Pictures thin with age rather than stopping. Keeping every picture for a month is impossible and keeping none past a day loses the thing the map is for, so the further back a picture is, the fewer of its neighbours are kept:
| How far back | One picture every |
|---|---|
| the last half hour | every one taken |
| up to 2 hours | 1 minute |
| up to 6 hours | 5 minutes |
| up to a day | 15 minutes |
| up to 3 days | 30 minutes |
| up to a week | 1 hour |
up to map-snapshot-days |
4 hours |
What survives a slot is the newest picture in it — what the world ended up
looking like — and two areas being watched at once are thinned separately, so
one does not erase the other. A month of pictures taken every thirty seconds is
86,400 of them; the curve keeps about 600. map-snapshot-thin turns it off and
goes back to keeping the newest N.
Everywhere anyone has been stays drawn. Pictures are taken of wherever people are and their windows are aligned to a grid, so a server played on for a week has pictures of a dozen different places rather than a dozen pictures of one. The map draws one per place — the newest taken at or before the cursor — rather than only the one nearest the cursor, so walking away from your base no longer blanks the map behind you. Nobody is touching that ground, so the last picture of it is still the right one.
Only what changed is stored. The ground barely changes, so writing all of it again every half minute was writing the same picture over and over. Capture windows are aligned to a 64-block grid, so a player wandering around produces pictures of exactly the same square — and each one is then filed as the difference from the last whole picture, which on a world standing still is a few hundred changed pixels out of a hundred and fifty thousand. Differences are always against the whole picture rather than against the one before them, so reading any snapshot costs two files and never a chain of forty; a fresh whole one is taken when the window moves, when half the map has changed, or every half hour. A whole picture something depends on is never deleted before the things that depend on it. What leaves all this is an ordinary PNG — nothing downstream knows differences exist.
That is what makes keeping two hours of them affordable, which matters now that
the log itself keeps five days: without it, map-snapshot-keep at 40 was twenty
minutes of ground under a five-day timeline.
| Setting | Default | Meaning |
|---|---|---|
map-snapshot-seconds |
30 |
how often a picture is taken; 0 leaves the map a grid |
map-snapshot-keep |
1500 |
hard ceiling on how many are kept, behind the curve |
map-snapshot-days |
30 |
how far back ground pictures go; 0 follows the activity log |
map-snapshot-thin |
true |
keep fewer the older they get, rather than deleting outright |
map-blocks-per-pixel |
1 |
detail; 1 is a pixel per block, 2 is four times cheaper |
map-radius |
192 |
blocks either side of the players each picture covers |
Ground pictures have their own window — map-snapshot-days, a month by default
— rather than the activity log's. That is defensible rather than an oversight:
a thinned month-old snapshot is a picture of the world, what was built and what
was cleared, and not a record of who was standing in it. The paths and the rows
that say who did it still expire on the log's clock. They all go when the log is
cleared. They are pictures of where people were, so they are not allowed to
accumulate any more than the log is.
Over a day a path covers kilometres and a picture covers a few hundred blocks, so one framing cannot serve both. Fit switches between them: ground keeps the world in view and follows the players as you scrub, everything zooms out to wherever anyone has been.
The in-game map has the paths and the shapes but not the ground — the pictures are files, and the in-game screen is fed by a packet. It receives a thinned copy of the paths, every nth point rather than the most recent ones, so a shorter path over the whole period beats a complete path over the last five minutes.
Movement is sampled, not followed: a position every activity-track-seconds,
and only when the player has actually gone somewhere, so standing still adds
nothing. Points expire on the same clock as the log, which is the limit on how
long a record of someone's movements exists.
They are written to config/almin/tracks.json and picked up again at the next
start. Holding them in memory only sounded like a privacy measure and was
really an inconsistency — every row in the activity log carries the coordinates
it happened at and has always been written to disk — and it meant a player who
was offline had no path at all, which is exactly when you want one. Clearing
the log deletes the file with everything else.
Block edits would otherwise drown everything else, so consecutive edits of the
same block by the same player fold into one row with a count — break ×47 Stone. Turn activity-blocks off if you only care about chat, commands,
containers and deaths.
/almin op activity clear (or Clear log in either UI) deletes the whole
thing immediately, from memory and disk. There is no export.
The log records events: break Oak Log at 214,71,-88, four hundred times.
Nobody reads that, and reading it is not how anyone finds out what happened.
What happened is "someone cleared the trees behind spawn" — and that sentence
is not in the log. It is in the shape of the log: which blocks, over what
ground, in what order, how tall, how long it took.
Under the map, What happened shows that shape. Rows are cut into runs — one player, one place, no long pause — and each run is classified by its geometry and its materials:
| Looks like | Comes out as |
|---|---|
| logs in a narrow column, three or more high | Chopped down about 4 trees |
| one column of breaks going down | Dug a shaft from y 64 down to y 11 |
| a long line two blocks high | Tunnelled 120 blocks east-west at y 11 |
| placements inside a box with height | Built something 14 across and 6 high, mostly Oak Planks |
| placements all at one level | Laid 60 blocks flat of Stone Bricks — a floor or a path |
| swings and hits traded in one spot | Traded blows with Steve |
| twenty breaks over flat ground | Cleared 18×14 of ground at y 71 |
| a death anywhere in the run | the game's own death message |
Two more come from the paths rather than the rows, because walking is not an event and a player who spent twenty minutes going somewhere leaves no trace in the log at all: Travelled 640 blocks — and, when someone walks a long way without getting anywhere, Back and forth around 90,12 — 620 blocks walked without leaving 18 blocks, over 7 minutes.
All of this is worked out on the server from the log itself. No model, no network, no key, nothing configured — click any line and the map goes to that moment and place.
Block names arrive from the game already translated, so the material rules read English. On a server running in another language they simply miss and classification falls back to geometry: a shaft is still a shaft, but "chopped down a tree" becomes "broke 40 blocks". Nothing here knows intent either — a hole is a hole whether it was a mine or a grief.
A stretch of work is something that happened once. A place is somewhere people keep going back to, and that is a different kind of fact — it is not in any one row of the log, it is in how often the rows land on the same ground.
The map draws them as dashed rings with a name on them: Steve's base, Ghost's mine, a shared gathering place. Hovering says what the evidence was; clicking zooms to it. The Places switch in the map's look settings turns the layer off, and it works the same on the flat map and in BlueMap, where places get their own toggleable marker set.
Nothing is configured and nothing is marked by hand. Somewhere becomes a place when the log shows people coming back: at least three separate visits — gaps of 25 minutes or more apart — and at least a couple of dozen events on roughly one patch of ground. One long afternoon of building, however much of it there was, is an episode and belongs in the timeline above; it never becomes a place. Talking and commands are ignored entirely, or every conversation anybody ever had would put a mark under it.
What kind of place it is comes from what was done there, in this order:
| Looks like | Comes out as |
|---|---|
| people keep crossing at one spot | a portal |
| somebody slept, or chests and building together | a base |
| planting and cutting the same crop | a wheat farm, a nether wart farm |
| a lot of breaking, deep, with nothing built | a mine, with the depth |
| crafting, enchanting and chests, alongside building | a workshop |
| four or more people, a lot of activity | a gathering place |
| none of the above, but people come back | a worked spot |
A worked spot is the one that has to earn its mark. Every other kind is recognised by what was done there and is drawn whatever its score; a spot has nothing to say for itself, so it is only kept if it scores at least 46 out of 100 — which in practice means people go there a great deal.
Each place is attributed to whoever did most of it, showing the mask beside the real name the way the rest of the panel does, unless no one player owns 60% of it — in which case it belongs to nobody and says how many people use it. Places are sorted by how much they are worth looking at (how often, how long, how many people, what kind), and at most 40 are drawn.
Two rules about what you get shown. A place is assembled after the rows you are allowed to read are filtered, not before: an account restricted to its own activity gets places built out of its own rows only, and never learns where anybody else sleeps. And a place is not drawn before it existed — scrub the timeline back past its first visit and the ring goes away, because a base marked on ground nobody had touched yet would be the map telling a lie about the past.
The AI menu gets the same thing as a tool, list_places, under the same two
gates: the Activity menu, and being shown coordinates at all.
Off by default. Settings → Reading the log with a model has the whole thing in one place: where (a local model, Anthropic, OpenAI), the address for a local one, the model name, the API key, how often to summarise on its own, and the switch. Turn on stays disabled until there is enough to talk to — a model name, and an address if it is a local one. The key is not part of that test: a local model does not want one, and refusing to start until somebody typed a key they do not need would be the wrong half of the check. Test it saves, asks for a summary, and tells you what came back, so "is it working" has an answer that is not a guess.
Once on, it summarises on its own every ai-auto-minutes (half an hour by
default) as well as when Summarise is pressed. A summary you have to ask
for is one nobody reads; the value is walking up to the panel and finding out
what happened. It skips when nothing has been recorded since the last one.
You get a paragraph saying what the session was about, up to five moments worth looking at — marked on the timeline, clickable — and, for up to a dozen stretches, what that stretch was probably for. That last one is the thing the episode's own sentence cannot know: "dug a shaft from y 64 down to y 11" is what happened, and "getting down to the ore layer under the base" is what it was in aid of, read from what came before and after it. It shows under the episode in the list and on the badge's hover, kept visually apart from the fact, because the fact is certain and the reading is not. A stretch the model has nothing to say about gets nothing rather than an invented reason.
The model is given the episodes, not the log — forty sentences rather than four thousand rows. That is deliberate twice over. Counting coordinates is the thing a loop is best at and a model is worst at, so it is done first; and a prompt of forty sentences fits in a 3B model running on the same machine, which is the difference between this being a feature every server can use and a feature with a bill attached.
A moment's coordinates come from the episode it names, never from the model — asking it to copy numbers back is asking it to invent them. If it answers with prose instead of JSON, the prose becomes the summary rather than an error.
| Setting | Default | Meaning |
|---|---|---|
ai-enabled |
false |
let a model summarise at all |
ai-provider |
local |
local, anthropic, or openai |
ai-model |
qwen2.5:3b |
model name, as that service spells it |
ai-base-url |
http://127.0.0.1:11434/v1 |
where local lives |
ai-send-chat |
true |
include what players said |
ai-auto-minutes |
30 |
summarise unattended every N minutes; 0 is only when asked |
local means anything speaking the OpenAI chat API at ai-base-url — Ollama,
llama.cpp's server, LM Studio — and it is the default because it is the only
option where nothing leaves the machine. Almin does not ship an inference
engine; running a small model is ollama pull qwen2.5:3b and pointing this at
it.
What leaves the machine, if you pick a hosted provider. Player names, what
they did, where they did it, and — unless ai-send-chat is off — what they
said in chat. That is a decision about other people's data, which is why this
is off until someone turns it on, and why the Settings tab says it in those
words next to the switch. ai-send-chat is separate from the rest because it
is different in kind: coordinates are a record of a game, chat is a record of a
conversation.
The API key is set in the panel or by writing config/almin/ai-key. It is
deliberately not in config.json: that file is served by the panel's own
file browser and rewritten whenever a setting changes, and a credential in it
would end up in every copy anyone pasted into a bug report. The browser refuses
to open or list ai-key at all, and on a POSIX host the file is written
0600.
Nothing is sent until somebody presses Summarise, or sets
ai-auto-minutes. Even then an unattended run is skipped when nothing has been
recorded since the last one — re-summarising an unchanged log is spending money
to be told the same thing.
The same pass that writes the summary also flags stretches of the log — and the flags land in the Activity menu itself rather than in a report you have to go and read. There is no extra model call and no extra cost: it is one more section of the same answer.
A flag covers a stretch, not a moment. What makes something worth a second look is almost always the shape of several actions in a row: not one chest opened, but nine chests opened in four minutes in a base nobody lives in. So a flag has a window, a player (or nobody in particular, when it is about several), and one sentence saying what the log shows.
Two levels, deliberately kept apart. Worth checking is "this might be a problem"; Notable is "you would want to know, and nobody is at fault" — a large amount of work lost, a first trip somewhere dangerous, a build finished. They get different colours, because reading the second as the first is how a flag turns into an accusation.
It says what it is, every time. The strip above the log says these are a reading of the log rather than a finding, that they can be wrong, and that nothing there says anybody broke a rule. That line is not decoration: this is the one place Almin's prompt lets the model say something might be wrong, and the prompt still tells it to describe behaviour rather than judge people. An admin draws the conclusions.
What the parser refuses to pass on. Three things, because all three are ways a confident model goes wrong and the output of this one is a real person's name under a heading telling an admin to look at them:
- A flag whose window falls outside the period the model was actually sent is dropped, not clamped into view. One that overhangs the edge is clamped.
- A flag naming somebody who does not appear in that period is dropped — an invented name is the single worst thing this feature could print.
- A level the prompt never offered becomes Notable, the milder of the two. A model that invents a level must not be able to invent a louder one.
At most five flags, and the prompt says plainly that an empty list is the right answer far more often than not.
Where they show up. Four places, so a flag is not something you only see if you happened to be looking at the right half of the menu:
- The strip above the log, listing them. Clicking one moves the timeline to the stretch and filters the log to the person — and the filter box shows what was done on your behalf, so you can widen it or clear it.
- The log rows themselves, marked down the left edge in the flag's colour.
- The timeline strip, as a band across the minutes it covers.
- All three map views — the flat map, the 3D world and the isometric picture of what was built — as a ring around the ground the flagged actions cover. A ring rather than a recolour: the mark colours already mean an action or a person, and a third meaning laid over them makes all three unreadable.
The map is handed a slice off the end of the log — the newest few thousand rows, which is the right thing to be given for the evening in progress and the wrong thing for a question about last Tuesday.
Two things follow from that, and both used to be visible. The strip worked out where the quiet stretches were from the slice it had, so a five-day log drew as the last few hours with no sign there was any more of it; and an older session, reached through the session buttons, showed paths with no events in them.
The shape of the record is now worked out on the server, over the whole log and every path, and sent as a list of quiet stretches. That makes the strip cover everything there is, and — more importantly — makes it hold still: sessions are numbered, the buttons and the strip navigate by that number, and a boundary that moved as more rows arrived would renumber the record under whoever was reading it. It is cached, because it walks everything and the live map asks again every few seconds.
The events themselves are fetched for the stretch being looked at, when it is looked at. Move to an older session and its rows arrive on their own; go back and they are already there. A window wider than the cap is answered with its newest end and says so, so the page knows what it still has not seen rather than taking the answer for the whole of it.
Dragging the playhead off the end of the strip carries into the next session rather than pinning to the edge, one session at a time while it is held there, which makes the strip a way to travel through the record instead of a view of one evening you have to leave by the buttons.
The summary above is one paragraph about a period. The AI tab is a conversation about the whole server — who has been digging near spawn, what happened while I was away, has anyone been in the nether this week. It uses the same model and the same settings, and it reaches the whole activity log, which is kept on disk: questions are not limited to the session running now, only to how far back the log still goes.
Nothing is pasted into the question. That is the difference from the summary, and it is the whole design. The model is sent the question and a list of things it may look up, and it goes and looks them up:
| Tool | What it reads |
|---|---|
search_activity |
log rows, filtered by player, action, dimension, text or time |
count_activity |
those same rows counted by player, action, dimension, day or hour |
activity_overview |
how far back the log goes and who is in it |
player_positions |
one player's path, thinned across the window |
list_players / player_summary |
who the server knows, playtime, sessions |
server_status |
running, online, version, uptime, tick rate, memory |
server_load |
what the server is spending its time on, and where |
list_places |
the places on the server, and who keeps going back to them |
list_mods |
the server's mods, or one player's reported client mods |
settings_summary |
Almin's settings and what they mean |
So "who has been busiest this week" is one count_activity call over the whole
log and a two-line answer, not four thousand rows in a prompt. Every result is
capped; when one is too big the model is told so and asked to narrow it, which
it can, because narrowing is what the arguments are for. Each lookup is one
request to the provider, and ai-tool-rounds is the ceiling on how many a
single question may make — running out is not an error, the model is asked to
answer with what it has and to say what it did not reach.
The panel shows every lookup above the answer it was written from — counted 4,102 rows by player next to Steve, by a distance. An answer about your own server should be checkable rather than taken on faith.
While it works, it says what it has done. A question can be several round trips to a model — minutes, at the far end of it — and a page that shows nothing for minutes looks broken rather than busy. The transcript keeps a line at the bottom naming the last lookup it made, in the same words the finished answer will show above it: counted the log · thinking…. It survives a reload, because the work is running on the server rather than in the tab: come back to the menu and the line is still there.
Every tool runs as the person who asked. A tool is a second road to data the panel already guards, so it is guarded the same way: an account is only handed the tools for menus it can open, every call is re-checked, and the readers go through the same helpers the panel's own pages use. An account that is not shown chat does not get chat here, and cannot use the text filter to find out what a hidden line said — searching for a word in it matches nothing, exactly as if the line were blank. An account restricted to its own activity gets its own activity. The password hash is never among the settings a model is given, and neither is the API key, which is not a setting at all.
| Setting | Default | Meaning |
|---|---|---|
ai-chat |
true |
show the AI menu at all |
ai-tool-rounds |
6 |
lookups one question may make |
Conversations are per account, in memory, and go when the server stops. The log is on disk and can be asked about again; what somebody asked about it is not the sort of record that should quietly outlive the asking.
The summary is about a period and the conversation is about a question. The Timeline tab beside Ask is about neither: it is the server's history, one sentence per day, from the oldest day the log still reaches to today.
Yesterday · 2 players · 1,820 events Steve finished the spawn bridge while Alex cleared the ravine below it.
Sunday, September 6 · 1 player · 900 events Alex dug a shaft to bedrock and came back with most of an iron block.
Each day is written from what the log already worked out — the same episodes the map's What happened list is built from, not the raw rows — so a day of four thousand events reaches the model as forty sentences and comes back as one. That is what makes it work on a 3B model on the same machine.
A day that is over never changes, so it is written once and kept, on disk,
in config/almin/ai-story.json. A restart does not re-bill the history. Only
today's line is ever rewritten, and only when today has moved on.
Nothing writes itself. Opening the tab reads what is already there and asks no model at all. The button writes four more days and says so before you press it — a fortnight of requests fired at a paid service the moment somebody opens a tab is a bad surprise, and four at a time is a decision somebody keeps making.
Days the model has not reached yet are still listed, with a hollow mark on the line and not written yet under them, because a gap in a timeline is information. A day with a handful of rows in it is marked quiet and never written: there is nothing to say about it.
Every day the log holds, not the last few thousand rows. The history used to be handed a slice off the end of the activity log — the newest twenty thousand rows. On a quiet server that is a week; on a busy one it is an afternoon, and the days before it simply were not there to write about. It is given the whole log now, so the Timeline reaches as far back as the log itself does.
A day the log has only part of gets no line at all. The log forgets from the back, so its earliest day is one whose morning has already expired — and the hours that are gone look exactly like hours nobody played. A sentence written from that fragment would call a busy day a quiet one, confidently and permanently, because a written day is never rewritten. So a partial day is shown with a dashed mark, labelled partly forgotten, and left alone:
Saturday, September 5 · 3 players · 1,500 events · partly forgotten The log no longer holds all of this day, so no line is written for it — what survived would read as a quieter day than it was.
It is also not counted in "days still to write", because the button is an offer and this is a day nothing will ever take it up on.
Today is unfinished, not forgotten. On a server whose retention window is shorter than a day the log does not hold all of today either — but that is "we have not got there yet" rather than "the record is gone", which everybody already understands about today. Today keeps its line and keeps being rewritten.
The dates, the event counts and how many people were there are the log's, not the model's. They are shown whether or not there is a sentence, and they are still shown when the model is switched off entirely — with a line saying why the sentences are missing.
Who sees it. The history is one text about the whole server, written once
and shown to everybody, so it cannot be written per reader: whatever one
reader's narrower slice produced would become the line everybody else inherits.
An account restricted to its own activity is told that in words instead of
being shown a history of itself. And where ai-send-chat is on, what the model
wrote is a paraphrase of chat, so an account that is not shown chat is not
shown the history either — the same rule the period summary keeps.
Clearing the activity log clears the history written from it. A history of a server that no longer exists is a fiction.
The Mods tab holds two lists that are easy to confuse and are not the same
thing at all: what this server runs, and what it suggests to the people who
join it. The first is the jars in mods/, loaded by this server itself. The
second is covered further down.
Each row says which of three states a jar is in, because a list that only says "installed" cannot answer why is it not working:
| Chip | What it means |
|---|---|
| Loaded | running in this server right now |
| Waiting for a restart | the jar is in mods/, but this server started before it arrived |
| Off | renamed to .jar.disabled; it will not load |
Turn off is the reversible one, and it is what to reach for when the
question is is this mod the problem. Deleting the jar answers the same
question permanently. Neither touches the running server: nothing in mods/ is
hot-loaded, so adding, removing or switching one off takes effect at the next
start. + Install a mod searches Modrinth or takes an upload; Almin's own jar
is in the list but cannot be switched off from the panel it is drawing.
Every row opens. The ▾ button — or clicking the row — asks the server about that one jar and shows what the mod says about itself: its description, who wrote it, what it is licensed under, what it depends on, and links to its home page, source and bug tracker. Underneath that are its settings files, each of which opens in an editor and saves back.
That is the whole point of the redesign. Configuring a Fabric mod used to mean
knowing that FTB Chunks calls itself ftbchunks, opening the file browser,
finding the right one of the ninety files in config/, and editing it. Every
step of that is knowledge the panel already had and the person did not.
It is asked for one row at a time. Reading a manifest means opening a zip
and listing a mod's settings means walking config/, and doing both for every
jar on a server with a hundred of them — on every refresh, to draw something
nobody has opened — is work nobody asked for. Nothing is read until a row is.
How a file is matched to a mod. There is no standard: Fabric has nothing to say about where a mod keeps its settings, so mods pick their own. In practice they pick one of a few shapes, and all of them are matched:
| Shape | Example |
|---|---|
| a folder named after the mod | config/carpet/carpet.conf |
| a file named after it | config/sodium.json |
| the same with a suffix | config/sodium-mixins.properties |
| any of those against the jar's own name | carpet-extra-1.4.163.jar → config/carpet-extra.conf |
It will miss some. A mod that writes config/mycoolmod-v2/ under a name
nothing can be derived from is not found, and a mod that has never been started
has not written its file yet. That is fine — the file browser still reaches all
of it. What it must not do is claim a file belongs to a mod when it does not,
because the panel offers to edit what it finds, so "sodium" is not allowed to
claim sodiumextra.json: the character after a matched name has to be a
separator.
What it will not reach. Only config/, only text-shaped settings files
(.json, .toml, .properties, .conf, .yaml and a handful of others),
only files under 2 MB, and only paths Almin derived itself — a request names a
file from the list it was given, and that name is looked for in the list again
rather than resolved, so a path that was never offered cannot be read whatever
it claims to be.
config/almin/ is never offered here, whatever name reaches it. It holds
the panel's own password, its accounts and its AI key, and the Mods menu is a
different door from the Settings menu: an account allowed to configure mods is
not thereby allowed to read the settings that decide who may log in. Almin's own
row says so instead of listing them.
Both routes behind this sit under the Mods menu permission, and writing a file needs a secure session, not merely a logged-in one. A saved file takes effect the next time the server starts — the editor says so, because a configuration that appears to do nothing is worse than one that plainly waits.
A server can suggest mods to joining players. Manage the list with
/almin mods in game, or the second list in the web panel's Mods tab.
These run on the player's computer, not this one, and nothing is installed
without that player agreeing to it.
In the panel it is one list, whichever way a mod got onto it. Each row has the mod's own icon, its name and version, whether the jar is served by this server or fetched from a link, and where it came from. Edit opens the same form for every mod; the ⋯ menu flips required, opens the Modrinth page, or stops advertising it. + Add mod offers the three ways in — search Modrinth, upload a jar, or advertise a link by hand — and all three land on that one list: uploading a jar now advertises it, reading the id out of the file, instead of leaving a second step to do. The settings that govern offering mods live behind the cog beside it.
Icons come from the jar itself where there is one — Fabric mods name their own icon — and otherwise from Modrinth, downloaded once when the mod is added and served from your server afterwards. That is deliberate: linking straight to Modrinth's CDN would mean every admin who opens the tab tells a third party what this server runs, and it would leave the tab blank on a machine whose browser cannot reach the internet. A mod with no icon anywhere gets its initial.
/almin mods list what's advertised
/almin mods files list jars this server holds
/almin mods addfile <id> <file> advertise a jar the server hosts
/almin mods add <id> <https-url> advertise one by external link
/almin mods required <id> true mark it required
/almin mods remove <id> stop advertising it
/almin mods reload re-read mods.json
To pull a file onto the server from a link, without a browser:
/almin op fetch mod <https-url> [restart]
/almin op fetch datapack|config|resourcepack <https-url> [restart]
/almin op fetch <dest-path> <https-url> [restart]
The first form works out the filename from the link and puts it in mods/;
the last one puts it exactly where you say.
The easiest way, and the one that gets the mod id right:
https://modrinth.com/mod/modmenu
Paste that into the web panel's Mods tab, or just search by name. Almin picks
the Fabric build for this server's Minecraft version — not whatever is
newest — downloads it, and reads the mod id out of the jar's own
fabric.mod.json.
That last step matters more than it sounds. The mod id is what a player's client checks to see whether they already have the mod. It is the id inside the jar, which is often not the name on the download page and often not the Modrinth slug either. Typed by hand and slightly wrong, detection fails silently: the player is offered a mod they already have, on every single join, with nothing anywhere saying why. Asking the jar is the only way to be sure, so Almin asks the jar.
Uploading a jar yourself does the same thing — the id is read from the file, so you never have to know it.
Having a different version counts as having the mod. Someone on Sodium 0.5.8
against a server suggesting 0.5.11 is not asked to reinstall it, because being
nagged every login would be worse than the mismatch. Matching also tolerates
the ways one mod gets written: Mod Menu, modmenu and mod-menu are the
same thing, and a mod that provides another id answers for it too.
Preferred: host it yourself. Drop the jar in config/almin/modfiles/ and
advertise it with /almin mods addfile — or upload it in the web panel's Mods
tab, which does both at once. Players download it straight from your server over the
game connection they're already on. Nothing needs a public link, no third-party
host is involved, and the file never has to be reachable from the internet.
Jars are capped at 32 MB.
Alternative: an external link. /almin mods add <id> <https-url> points at
someone else's host. It must be https://.
Either way you can set a sha256 in config/almin/mods.json to pin the exact
file; a client refuses a download that doesn't match.
On join, a client running Almin shows the list: each mod's name, version, and
where it comes from — this server, or the external host that would serve it.
Nothing downloads until they press
Approve. Mods already installed are filtered out, so returning players aren't
asked again. Approved jars land in mods/ and load on the client's next
launch — nothing is injected into the running game.
| Setting | Effect |
|---|---|
mods-advertise |
send the list at all (default on) |
mods-deny-kicks |
declining disconnects the player, but only when a required mod was offered (default off) |
require-client-mod |
players without the Almin client mod are disconnected at join (default off) |
require-client-mod is what closes the obvious hole: without it, a player can
simply not install Almin and never be shown the prompt.
mods-deny-kicks is a house rule, not an anti-cheat. The client reports its
own choice, so a modified client can claim it approved and install nothing.
Fabric gives a server no reliable way to inspect what a client has loaded. Use
it to steer honest players; don't rely on it to keep dishonest ones out.
Approving runs someone else's code. That is the nature of the feature. The
guards are: https:// only for external links (enforced when the offer is
stored, again before it is sent, and again by the client), an optional SHA-256
pin, a size cap, a check that the download really is a Fabric mod jar, and a
filename derived from the mod id rather than the URL. Hosting the jar yourself
removes the third-party host from the picture entirely, which is why it's the
recommended route.
A client asking for a server-hosted jar sends only a mod id. The filename
comes from the server's own offer list, and modfiles/ names are restricted to
a plain .jar in that one folder — so the request path can't be turned into
"read me any file on the server".
Almin's half of the 3D view is a small script served into BlueMap's own page. BlueMap is an optional install with versions of its own, and the bridge does not get to pick which one it lands in — so it is written to keep working when the app is not the one it was written against.
The one that bit: it used to open by registering a camera listener on
bluemap.events. On an app that keeps its events anywhere else that threw, and
took the stylesheet, the "ready" message and the first render down with it. The
panel waits for that ready message before it sends anything, so the result was
a 3D map with none of Almin's places, faces or coloured paths on it — and
nothing on screen to say why, because from the panel's side the frame had
simply never answered. Fixed in v2.69.0: everything that has to happen happens
before anything that can fail, the event source is looked for in several
places, and a version that offers none of them gets its camera polled instead.
If the 3D map is still bare, the browser console inside the BlueMap frame is where it will say so; Legacy 2D in the map's mode switch draws the same data with no BlueMap involved at all.
The in-game panel is not something a client can open by itself. Every admin screen appears only in response to a packet the server chose to send, and the server only sends it to a caller who passed a permission check:
| Screen | Opened by | Gate |
|---|---|---|
/almin dashboard |
/almin, /almin dashboard |
vanilla op or TrustedOps UUID |
| Console viewer | /almin op console |
TrustedOps UUID |
| File browser | /almin op dir |
TrustedOps UUID |
| Nano editor | /almin op nano |
TrustedOps UUID |
| Mod offer prompt | join | everyone — it is player-facing by design |
There is no keybind and no client-side command that opens any of them.
The dashboard's trusted flag only decides which buttons are drawn. That is
cosmetic: a modified client can draw whatever it likes, and it gains nothing,
because every button re-issues an ordinary command and every client→server
packet re-checks permission on arrival:
| Packet from client | Re-checked against |
|---|---|
| console subscribe | TrustedOps.isTrusted |
| directory listing request | TrustedOps.isTrusted |
| nano save (file write) | TrustedOps.isTrusted |
| file upload | TrustedOps.isTrusted |
| mod file request | must be a mod this server currently advertises |
So the answer to "can a normal player reach the admin tools" is no, at both layers: they are never handed the screen, and the server would refuse the actions even if they built their own.