Two controllers own the server row itself, as opposed to what happens to it once it exists: ServersController (create/remove/ignore/auto-ban toggles/health recheck) and ServerCredentialsController (SSH user/port/password, one at a time or in bulk). Neither had a doc page before this one — everything else in Features assumes a server already exists and has working SSH.
Almost every SpinupWP server arrives via clockwork:import-spinupwp, not through this page — see Integrations → SpinupWP. What’s here is for the boxes that import doesn’t reach (Hetzner, hand-rolled, anything outside the SpinupWP account) and for fixing/rotating credentials on any server regardless of how it got created.
Adding a server by hand
/servers/new (ServersController::create / store) is a plain form: name, hostname/IP, SSH user (defaults to clockwork.ssh.default_user), SSH port (defaults to clockwork.ssh.default_port), and an optional SSH password. The new row starts at status=unknown — it only turns green/yellow/red once a poll cycle or “Recheck health” runs.
store() checks whether SpinupWP is actually configured (SpinupWpClient::isConfigured()) before doing anything SpinupWP-specific. If it is, store() launches the same background clockwork:import-spinupwp + clockwork:poll-servers pair the Refresh button uses (see below) — harmless for a genuinely hand-rolled server (no SpinupWP match, no-op), but it means a server you thought wasn’t in SpinupWP can suddenly pick up sites shortly after creation if it turns out it was already there under a different name. If SpinupWP isn’t configured on this fleet at all, only clockwork:poll-servers is launched, so the new server still gets a real status without waiting for the next scheduled tick.
Use this form for one-off additions. For several servers at once, the page itself links to the bulk paste-and-import flow (/servers/credentials/feed, below) instead.
Refresh from SpinupWP / GridPane, on demand
The server detail page header shows a Refresh from SpinupWP or Refresh from GridPane button — whichever control panel actually manages that server’s fleet inventory ($server->spinupwp_id !== null for SpinupWP; $server->provider === Server::PROVIDER_GRIDPANE for GridPane, since GridPane has no separate server-level id column). Neither button renders for a server managed by neither (hand-added, or a cloud-VPS provider with no panel of its own) — there’s nothing to refresh.
Each button (POST /servers/refresh-spinupwp → ServersController::refreshFromSpinupWp, POST /servers/refresh-gridpane → refreshFromGridPane) launches the matching clockwork:import-* command and clockwork:poll-servers as one detached background chain (BackgroundArtisan — Cache::add lock + nohup, same helper as the Operations “Re-poll fleet” button). The HTTP request returns immediately; a second click while the lock is held flashes “already running.” Import and poll are chained with &&, so a failed import skips the poll. Progress lands in storage/logs/.
Removing a server
Destroy (DELETE /servers/{server}) is permanent — it cascades to sites, server metrics, blocked-IP records, and the tag pivot via FK constraints. The operator must type the server’s name as confirmation (either the full hostname or the short UI display_name), checked server-side (not just a JS confirm()). Use this when the box is actually decommissioned or disconnected from control panels like SpinupWP.
To make server removal seamless when cleaning up decommissioned or disconnected servers, removal actions are surfaced directly across the server detail view:
- Ignored Banner: If a server is marked as ignored (e.g. decommissioned in SpinupWP), the monitoring exclusion banner provides a red Remove from Clockwork Control button right alongside Stop ignoring. Below
sm, the copy stacks above full-width actions so the pills cannot crush the reason text. - Header Action Row: In the tags and action row next to the Edit button, a Delete button is directly accessible on the server header.
- Empty Sites State: When 0 sites are mapped to a server not managed by a live panel, the Sites tab empty state offers an immediate Remove this server from Clockwork Control button.
- Provider-Missing Banner: When
clockwork:poll-serversfinds a server’sprovider_idgenuinely absent from the cloud provider’s own inventory (CloudProvider::isDeletedAtProvider()), it stampsprovider_missing_sinceand displays the red alert banner with Remove from Clockwork Control. - Settings Tab Danger Zone: The full-page Settings tab contains the complete cascade overview and destruction form.
Auto-ban toggles
toggleAutoBanLlar / toggleAutoBanWordfence (POST /servers/{server}/auto-ban-llar and .../auto-ban-wordfence) flip auto_ban_llar / auto_ban_wordfence on the server row. When on, Gatekeeper login lockouts or Wordfence blocks blocks from that server skip the human review step in the Review queue and go straight to fail2ban over SSH (fail2ban-client set clockwork banip <ip>). When off, they land in the queue like any other source.
Recheck health
Recheck health (POST /servers/{server}/recheck-health) re-polls the cloud provider’s CPU metrics API on demand — the same evaluators the scheduled clockwork:poll-servers job uses (DigitalOcean, Hetzner, or Azure, dispatched on server.provider), just scoped to one server so you don’t wait for the next cycle after, say, resizing a droplet. Requires provider_id to be set (a hand-rolled server with no cloud-provider link returns a 422 — there’s nothing to poll). Updates status, last_polled_at, and last_alert_at (only on a fresh red transition) exactly like the fleet-wide poller does.
SSH credentials
Three ways to set SSH credentials, all landing in ServerCredentialsController:
One server at a time
/servers/{server}/edit (edit / update) — SSH user, port, and password, plus a Clear password checkbox (for switching a server to key-only auth). Changing user or port, or setting/clearing the password, triggers an inline SshClient::test() after save so the confirmation message tells you immediately whether the new credentials actually work — no separate “Test SSH” click needed on this path (though the button exists everywhere else, see Integrations → SSH + fail2ban).
Bulk edit
/servers/credentials (bulk / bulkUpdate) lists every non-ignored server with a password field each. Submitting only touches rows where a password was actually typed (blank fields are skipped, not cleared) — sets the password, backfills ssh_user from the default if the row has none, and saves. It does not open an SSH session per row (that used to time out the request on a 24-server fleet). Use the per-row Test SSH button to verify. This is the fast path for “I just rotated the clockwork-deploy password fleet-wide.”
Paste-and-import feed
/servers/credentials/feed (feed / feedParse / feedApply) accepts a block-separated text dump in a specific format (name, IP, ssh <user>@<ip> line, password, repeated per server, ----separated) — the shape SpinupWP’s own credential export uses. CredentialFeedParser::parse() splits it into rows; feedParse() then matches each row against existing servers by name (exact) or hostname (only if unambiguous — multiple hostname matches are flagged ambiguous, a name match against a different IP is flagged name_ip_conflict), producing a preview table with a status per row (matched / unmatched / ambiguous / name_ip_conflict).
The operator reviews the preview and checks which rows to apply. feedApply() then, per checked row: updates credentials on a matched server (SSH user only changes if update_user was explicitly checked — accidental user drift from a mismatched paste is exactly what the ambiguous/name_ip_conflict statuses exist to catch before this step), or creates a new Server row for unmatched entries (re-checking for a name/IP collision at apply time, in case something changed between parse and apply). It does not run a fleet-wide SSH verify — use Test SSH on each row.
What this isn’t
- Not tag management. Assigning tags to a server (
TagsController::syncServer) happens from the server detail page, and is documented under Features → Server updates + reboots → Tags because of its load-bearing effect on the update pipeline. - Not fail2ban provisioning. That’s a separate one-time action (
ServerProvisionController::fail2ban) — see Integrations → SSH + fail2ban. - Not the fleet health grid. That’s the main Dashboard — this page is about the CRUD/credentials layer underneath it.
