What’s new in HF Panel
Every change to HF Panel, HF Local and HF Connect, newest first.
Upgrade notes and known issues are in the release notes. What comes next: roadmap.
Latest changes
Old forms removed: plural API names only, JSON sessions and settings, one DNS check at a time
- The token API's per-account resources have one name each, the plural one:
/v1/accounts/{username}/mailboxes,/forwarders,/ftp-accounts,/ssl-certificates,/ssh-keys,/dns-zones(with…/dnssec),/mysql-databases,/mysql-users,/postgres-databases,/cron-jobs(with…/run,…/runs),/uptime-monitors,/backups. The older paths (/email/mailboxes,/email/forwarders,/ftp,/ssl,/ssh/keys,/dns,/mysql/databases,/mysql/users,/postgres/{database},/cron,/uptime) answer "No such endpoint". Each resource answersGETof one item; its lists are lists (paged, 50 by default): the FTP, SSL and cron lists no longer wrap the list in an object with the FTP host, the AutoSSL setting or the cron email.GET …/mysqlandGET …/postgresanswer where to connect, the limit and the count.tokenapi_rest_compat.goand its alias table are gone (x-hf-alias-of, theAliasfields); the routes are markedRestintokenapi_spec.go, so their bodies are checked like every REST resource's (a taken mailbox or FTP login answers 409). Fixed: an allowed value of a REST field (a DNS record'stype: "A") was refused when written in capitals. Docs, OpenAPI, plugin scopes and licence module paths follow. - Sessions are JSON (
internal/sessfile, wasinternal/phpsess): the PHP serialize format and its PHP differential test are gone. - Settings are JSON only:
hf-api config convert,--webmail, theconfig.php/webmail.phpreaders (settings.ParsePHP,Resolve,Convert,AddEdition) and the installers'config.phpchecks are gone, with Mail's oldwebmail.json,/var/lib/hostfog-webmail,webmail.sqliteand/usr/local/hostfog/webmailhandling. - update.sh: no more
hf-webmail--*.confandwebmail/renames,hf-agent→hf-agentdswitch, old command names (hf-server,hf-admin,hf-panel, also in install.sh and uninstall.sh) or savingapi_port2087.server-fixes.shno longer patches/files/and/api/into older nginx files andhf_api_nginx_upgradeis gone. - Mail answers its page on
/only (/index.phpis gone; the interface's raw links are?a=…). HF Local's API is/api/v1/…only. - Team roles: a custom role saved with a merged screen no longer opens its successors (
teamScreensMoved); phpMyAdmin is a screen of its own in the databases area (no longer an alias of DB Studio). The Feature Manager key of Malware ismalware(wassite-security). - DNS setup lists no
webmail.record (Mail is onmail.), in step with the browser installer. - The browser installer runs one live DNS check at a time per setup token: a second check (or Continue) while
one runs answers 429 "A check is running now." without looking anything up; the DNS step never overlaps its own
checks (a timed check skips, the button and Continue wait). Test:
TestDNSCheckOneAtATime. - Unused texts removed from all 20 languages: "Redirects & IP blocks", "Security scan", "Phone app", "Included features", "Points to {1} (via {2})", "found {1}.", the old password rule, the phone app prompt, the DNS host note with named placeholders and the archive's config.php message.
- Checks (
tests/checks):apihost.py,webhooks.py(tohook.<domain>/stripe|paypal),superadmin.py,dns2.py,cluster.py(/v1/node/agenton the node's API name),billing-admin.pyandgoonly.pyuse the current addresses;shell.py,logout.py,dav.pyand_signin.pywrite and read JSON sessions; the parts that only tested a removed form are gone.
Machine addresses on api.<domain> only; HF Cloud without default prices; Business without HF Cloud and HF Deploy
- The API's own address is the only home of every machine address. The panel's port no longer answers the token
API (
/api/v1with a token,/api/v1/desktop/…,openapi.json,status/page),/billing/webhook,/billing/paypal-webhook,/node/agent,/licence/api/*,/licence/*,/cloud-host*,/ddns/updateor/nic/update; they arehttps://api.<domain>/v1/…(/v1/webhooks/stripe|paypal|deploy,/v1/node/agent,/v1/licence/…,/v1/cloud-host.sh|agent|join,/v1/ddns/update,/nic/update:machinePaths).hook.<domain>answers only/stripeand/paypal. Withoutapi.<domain>there is no API (Ctx.APIURLis ""; the screens leave the API lines out, PayPal's set-up and HF Cloud's join command say to runhf-agent api-portal on). Gone:OldAPIURL,OldWebhookURL,EndpointURL,licenceOldPaths/LicenceURLs/licenceTryNext(oneLicenceURL),company.LicenceOldBase(nowLicencePanelBase, the licence server's pages),nodeAgentURLs/nodeTryNext, the endpoint list'sold_base(it namespanelinstead), Profile's "old address" line and the Billing screen's old webhook address. The cluster join code carries the joined server's API address (api); standby pairs talk API to API. HF Deploy's/deploy/webhookand/deploy/wakestay on the panel's port. The licence fetches of the wp-vulns feed now use/v1/licence/wp-vulns(the API name was wrong before and only the old address worked). Callers moved: the installer,update.sh,uninstall.sh,licence-setup.sh(hf_licence_url),hf-agent,make-latest.sh,setup-template.sh(no licence links without the API name), the HF Cloud host script, docs and tests. - Desktop apps and billing modules use the API address only. HF Local, HF Tunnel and HF Connect take the panel's
domain (or
api.<domain>) and talk tohttps://api.<domain>/v1;Server.Callhas no fallback to/api/v1(apiTryOld,apiDown,APIBase,CallAPI,company.ServerURLgone). The WHMCS and Blesta modules (HfPanelClient::baseUrl) always useapi.<domain>/v1and refuse a panel's/api/v1;panelUrl()readspanel. - Only a screen's own path opens it. No redirects of merged screens (
site-security→malware,git-previews→git-deploys), no/<other group>/<route>or/<route>of a grouped screen, no old slug paths (/billing/superadmin-downloads) and nocanonicalize()in the interface;/index.phpis no other name of the root (server,router.ts, the Vite dev proxy).GET /api/v1/_boottakes nor, and the screens' query has nor. - No
mx.<domain>:mail_old_name,mail_old_users, the agent'smail-renameandmail-old(and their helpers),OldMailKept,OldMailUsers, the Server health row "Old mail server name" and the update step "Mail server names" are gone;MailAliases, the DNS list and the certificate never namemx.. studio-portal-on/-offand thestudio-portal-offflag are gone:db-portal-on/-offonly.- No PHP-era conversion:
server-fixes.shno longer converts PHP-era nginx files, PHP-FPM pools, PHP cron lines, the old phpMyAdmin or Mail onwebmail.(hf_go_nginx,hf_go_php_pools,hf_go_cron,hf_convert_*,hf_mail_on_mailname,hf_mail_move_web,hf_pma_legacy_cleanup,hf_nginx_swap_location,hf_studio_location_add);tests/check-installer/php-era/is gone with its checks. - HF Cloud has no default plans (
licDefaultLinePlans: an empty list; HF Deploy keeps 9/25/49). The publisher adds them in HQ › Plans & pricing. A line without plans is not offered on the order pages or the website ("Prices coming soon"); a choice of an HF Cloud plan that doesn't exist is refused on its field. Licences that hold a plan keep it (an unknown plan has no limit and costs nothing; nothing is deleted). - Business (and every HF Panel plan) no longer includes HF Cloud or HF Deploy: they are bought only as their own
lines (
LicPanelFeatureKeys; a custom bundle drops them). The "included" plan (LicIncluded) and "Included with HF Panel" are gone; a licence without a features list has every HF Panel feature, not the lines (a trial without a plan still tries every product). Not launched, so the rule for licences from before the lines is not kept. - Unused texts: the old search placeholders and the homes' texts design 4 no longer shows, and the texts of the removed parts, are taken out of the panel's dictionary.
Only the new addresses (the old forms removed before launch)
- One address per page: its path. Every link, redirect, email, form and fetch of Server Admin, the Reseller Panel,
the Control Panel, HQ, HF Cloud, HF Deploy, Mail, Files and DB Studio uses
/<group>/<route>?…(URL()/RoutePath()in Go,href()in the panel). The/?r=<route>form is gone: the server no longer readsr(it is the panel's root, the dashboard), the interface no longer rewrites it, and the language and look pickers come back to the page's path. Server-rendered pages answer at their paths only:/sso,/push,/offline,/list-unsub,/usage-refresh,/email/webmail-open,/storage/files,/databases/studio,/kb, and the public billing pages/order,/pay,/quote,/licence,/licence/manage,/licence/partner,/licence/paypal-return,/account(also onpay.<domain>, which no longer takes/?r=). Stripe's and PayPal's return addresses, the welcome and notification emails, the help articles' links and the billing modules' sign-in links follow. - HF Tunnel downloads: only
https://api.<domain>/v1/tunnel-get. The panel's/?r=tunnel-getpage and its route are gone; HF Local and HF Tunnel download the tunnel client from the API address only (Server.CallAPI). - HF Tunnel addresses: always
<name>.<tunnel domain>.COMPANY_TUNNEL_DOMAINis required and may not beCOMPANY_DOMAIN(build.sh,company.sh, the Vite configs); the<name>.port.<domain>form is gone (company.TunnelSuffixOf,Ctx.TunnelSuffixFor,tun_suffix, the installer's andhf-dns-setup's*.port.record, the zone's tunnel wildcard). HQ › Tunnels andtunnel-enablerefuse the panel's domain or a name under it (Ctx.TunnelDomainOwn,tun_own); the screen's two texts that describedport.say so now, in all 20 languages. HF Local's/api/stategivestunnelDomain(wastunnelSuffix). - HF Cloud and HF Deploy: their screens on a panel (
/cloud/…,/deploy/…) are "not found" instead of a redirect tocld./dep.. - Old panel names: the free HF Tunnel server is
srv.<domain>(HF Local no longer moves savedpanel./server.entries); the licence server's default issrv.<domain>, and onlycp./srv.licence servers get anapi.address (installer, uninstaller,make-latest.sh,svc_licence_client.go); a new server's addresses in HQ mail aresrv.andcp.;update.shgives DB Studiodbs.<domain>and no longer movesstudio.todb.(db-portal-renameremoved); the installer no longer takespanel.<domain>as the panel's name.
Tests and fixes for design 4 and the DNS step
- Fixed: the Control Panel's website strip showed another website's uptime monitor when its name started the same
(
sarahbakes.example.orgforsarahbakes.example); it now matches the monitor's host exactly (www. or not). - Fixed: HQ › Payments past its last page (
?p=999) answered that page with no rows, and the list read "Showing 49901–49900 of 3"; a page past the end is the last page now. - Fixed: Server Admin's Load meter on another cluster server measured its load against 1 processor ("· 1 cores", always red); without that server's processors it shows the load alone.
- Fixed: the framed list took seconds per keystroke on a list of thousands of rows (each row read through the live
rowslist); it walks a fixed list of the rows now (10,000 rows: about 0.15 s in the test browser). - Fixed: two data races in the browser installer, seen with many DNS checks at once: the setup server set its state store on every request, and the live check read a record while filling it in.
The panels in design 4
- Owner-approved design 4 for Server Admin, the Reseller Panel, the Control Panel and HQ (HF Cloud and HF Deploy
share the frame): flat, no shadows anywhere — thin borders (
--line) and soft tints; the shadow tokens (--shadow,--shadow-pop) and everybox-shadowrule are gone (focus and selection rings are outlines now). - Top bar: the HF mark and the panel's name, a wide search ("What do you want to do?" in the Control Panel, "Find
an account, domain or setting" elsewhere; Ctrl K as before), notifications and your menu, which now also holds the
language list. Sidebar: warm grey, plain sentence-case group labels, the active screen a white row with a thin
brand edge; Server Admin's server box on top (address, system, processors, memory — boot
host,HostSummary; the server switch inside it). HQ's menu is grouped Sales / Product / Support / Services / Managed / Team, with the overview first. - Page heads: an icon square in the screen's area tint (websites, email, files, databases, domains/DNS, security:
--c1-…–--c6-…,ui/area.tsx), the title and one line. The New card keeps its place, dashed and brand-soft, with the tinted icon. Dialogs and wizards: a thin border, the icon in the header, the wizard's step bar under the title, Cancel / the main button on a grey strip. - One framed list for every list (
Table/TableBoxwithlist,ui/kit.tsx): a toolbar with the filter box, quick filter chips and "N of M", a quiet header, roomy rows with an optional grey second line and faded row buttons, and a footer with "Showing a–b of n" and pages; lists the server filters and pages (Accounts, HQ › Payments) pass their own search, counts and pager. Sorting, ticking and bulk actions are unchanged. - Home screens rebuilt with real data: Control Panel (website strip with its certificate and uptime state, six tools with live counts, quick actions, the plan's meters), Server Admin ("N things need you" from server health, websites down, the panel backup and the capacity forecast; management tiles; quick actions; server meters and services), Reseller Panel (the same with their customers, packages and limit), HQ Overview (to-do strip from tickets, failed and overdue payments and failed installs; business tiles; product meters per line and trials; 12 months of revenue). The old cards stay below each home (charts, all tools, connection details, the licence server).
- HQ: Licence keys with product pills and chips (HF Panel, HF Cloud, HF Deploy, Trials); Plans & pricing as plan
cards with their buttons on one line; Support as a list beside the conversation (
sidein a ticket's answer); Payments and Tunnels with meters on top; Status page with a strip and the services' uptime. - Sign-in keeps its earlier look (brand side panel with the tagline and points, card on the right; owner, 6 Nov 2026).
- Removed with the redesign: the top bar's breadcrumb and its moon/sun button (the look stays in your menu's
Appearance), the overview key-figure rows and the "+N in 30 days" deltas (now tiles, meters and sparklines), the
Server health card of Server Admin's overview (now its meters), the capacity card (now a line in the strip), the
HQ overview's Licences and Tunnels tool tiles (now business tiles),
productsLine(now product pills). - 42 new texts in 20 languages. Tests: the framed list (filter, chips, count, pages, server lists) and the four homes.
Mail, Files and DB Studio in design 4
- Owner-approved design 4 (
shared/styles/hostfog-look.css): flat, top-bar search (Mail: mail; Files: "Find a file"; DB Studio: "Find a table", which replaces the side bar's table filter), sentence-case side groups, icon page heads, framed lists with chips and "N of M" (Files' folder, Studio's databases and tables, the grid, Mail's filters, contacts and app passwords), dialogs with an icon header and a grey footer. Three new texts in 20 languages.
Licences: HF Panel, HF Cloud and HF Deploy on one key
- Owner-approved design: the publisher sells three product lines — HF Panel, HF Cloud and HF Deploy — and one licence key carries one, two or all three. One install and one codebase: the licence switches the products on, and a product bought later reaches the server at its next licence sync, with no reinstall.
- Licence model (
licfeatures.go, one home):LicProducts, a plan per product (LicChoice: HF Panel's plan, features and tier, ornone; HF Cloud's and HF Deploy's plan keys in the newlicences.productscolumn), prices added up byLicChoicePrice. HF Cloud plans limit KVM hosts, HF Deploy plans apps; a server refuses one more with a field error ("The licence's HF Cloud plan allows no more KVM hosts." / "… HF Deploy plan allows no more apps."). Existing licences keep working: an HF Panel plan with thecloud/deploymodule (Business, and every licence without a features list) includes the product, unlimited and at no extra price. The catalogue is version 2: HF Panel'splansunchanged (older servers read them),productsadded; a catalogue saved before gets the default line plans when read. The licence answer'sfeaturesinclude the lines' features, andproductseach line's plan and limit (after the old keys: older servers still verify it). - A licence without HF Panel gets the small Server Admin of
LicCore(status, updates, backups, firewall, SSL, logs, staff, billing, settings, API tokens) and its products' screens; the hosting screens are hidden, the Control Panel and Reseller Panel are off; nothing is deleted. - HQ › Plans & pricing has a tab per product: HF Panel (as before), HF Cloud (its plans and the HF Panel licence a VPS gets) and HF Deploy. HQ › Licence keys shows each key's products and changes them (Change plan / features, Give a licence).
- Shop and website: the order and manage pages choose a plan of each product (or none, or no HF Panel); the account pages show each key's products; the website's pricing page shows the three products with their plans (live prices from the catalogue).
- Partner API and webhooks:
productswhen issuing,POST /v1/licence/partner/productsto change them,productsin the licence list, the fleet and every webhook, new eventlicence.changed. The WHMCS and Blesta modules sell licences with a partner key (hfp_…): each product's plan in the product / package settings. - Installer: the browser installer's Licence step shows the key's products; install.sh skips the FTP server and
wp-cli on a server without HF Panel, and HF Deploy installs Docker; update.sh and
hf-agent licence syncfollow (FTP stopped or set up, Docker installed). - Default plans and prices (owner to confirm): HF Cloud Starter 5 KVM hosts 19, Pro 20 hosts 49, Unlimited 99; HF Deploy Starter 10 apps 9, Pro 50 apps 25, Unlimited 49 (a month, billing currency). HF Panel's plans are unchanged.
HF Cloud and HF Deploy: apps of their own (cld.<domain>, dep.<domain>)
- Owner-approved design: HF Cloud (
cld.<domain>) and HF Deploy (dep.<domain>) are web apps of their own on port 443, like Mail, Files and DB Studio, with the panels' sign-in: one account works everywhere, the same sessions store and two-step sign-in (cookieshf_cloud,hf_deploy). Administrators get the admin screens, customers only their own VPSs or apps; resellers are sent to their own panel. A licence without the product closes the app: administrators go to the Licence screen, everyone else gets "Access denied"; signing in still opens. - The screens moved, one home each: HF Cloud — Hosts, VPS list (with the VPS tools), VPS plans, IP pools, Images,
My VPS, Order a VPS (
frontend/cloud/src, withui/cloudAdmin.tsx,ui/vpsTools.tsx,styles/cloud.css); HF Deploy — the administrator's screen and the customers' services (frontend/deploy/src, withui/deployNew.tsx,ui/deployService.tsx,styles/deploy.css). Their Go screens and services are unchanged (screen_cloud*.go,svc_cloud*.go,screen_deploy*.go,svc_deploy*.go);svc_apps.goserves them on the apps' addresses only (appRoutes): an app's screen asked for on a panel's API gets 403 with the app's address, a panel screen on an app's API 403; old panel addresses (/cloud/…,/deploy/…,/hosting/deploy-admin,?r=…) redirect to the app with their query. HF Deploy's administrator screen is now/deploy/deploy-admin. - The panels keep one link each: Server Admin's Cloud group and Hosting entry, and the Control Panel's Cloud and
Deploy groups, are now "HF Cloud" / "HF Deploy" links (new tab, external-link icon), shown only when the app has its
address and the licence includes it (
Ctx.AppLink); the customers' HF Cloud link as before only once VPSs are sold or they have one. VPS-order emails, Stripe and PayPal returns lead tocld.(PortalURL("cloud", …)). - No copies: the apps run the panel's own frame —
frontend/panel/src/start.tsx(the panel's start-up, now shared:main.tsxcallsstartApp),registry.ts(setPageFiles: each app hands in its pages),accountPages.ts(the sign-in pages the apps take in),@panel/…imports; Vite settings once inpanelAppConfig(frontend/shared/vite.ts); hf-api reads each app'smanifest.jsonfrom its embedded build (API.AppUI,//go:embed all:web/cloud|deploy,.gitkeepplaceholders) and writes its page with the start-up data like the panel's. Sign-in wording for the apps and their sign-in side panel (9 new texts in all 20 languages). - Plumbing:
X-HF-App cloud|deployinroute()(main.go) andAPI.AppOf; nginx 443 blocks from the agent'sportals-web(portal_app_block),portal_cloud/portal_deployset byportals-on, newhf-agent cloud-portal on|offanddeploy-portal on|off; update.sh addscld./dep.on existing servers with separate panels (a saved name is never changed); the certificate takes the names (portal_names); CSP form-action, the panels' host list, reserved names and tunnel names include them; DNS lists: setupwebdnsRecords(CNAME →<domain>.),hf-dns-setup(--portalstakes two more parts),BuildZone, the server-ready email, install.sh, setup-template.sh, the smoke test, the docs.scripts/build/frontend.sh cloud|deploy, build.sh's frontend step, package.sh. - Removed (not named by the owner, listed per rule 16):
@novnc/novncfrom the panel's dependencies (only HF Cloud's VPS console uses it now) and the panel's top-level-await build settings that existed for it. - Screenshots for the docs wait.
HF Tunnel works the same as HF Local's tunnel feature
- Owner: "HF Tunnel software should work the same as HF Local's tunnel feature — everything the same." HF Tunnel is now HF Local's tunnel feature for any local address; the only differences come from it having no sites of its own (it shares an address, with an optional Host header; it has a Remove button for an address it no longer needs).
- Links like HF Local's: each address gets its own link name (offered from the Host header or at random, and
changeable in the Share dialog, with HF Local's notes: You already have this link, A new link is made on the
server (1 of 3 used), the limit), remembered in
share.json, so the link is the same every time; several addresses can be shared at once; Stop sharing keeps the link on the server (it no longer deletes it, and the clean-up of "left behind" links at start is gone). The random 10-character name per start is gone. - One shared link from start to stop (
backend/os/internal/tunnel):share.go(tunnel.Share: local check, relay, link, frpc, reconnect 1 s → 60 s),relay.go(request log, Host header kept or rewritten, redirects moved to the link,/phpmyadmin/pma/adminer/server-status/server-infoand local bundles' dashboards refused, only loopback and private targets — moved from HF Tunnel),prefs.go(share.json),servers.go(servers.json, adding a server, the free links' server; moved from HF Local'sremote_servers.go), names (tunnel.NameFor,tunnel.CheckName,tunnel.FieldError),tunnel.UntrustedNotice,tunnel.StrayClients(leaves clients of another running copy alone).platform.WriteFileAtomicreplaces HF Local's ownwriteFileAtomic. - HF Local gains (from HF Tunnel): the relay in front of Apache (request log, the database-tool paths refused
there too), automatic reconnect when the tunnel client stops (it used to drop the share: "Share again"),
Reconnecting… on the site (
shareStatein the site list), Open, QR code and Requests on the Tunnel page (/api/share {Action: 'log'}), and a QR code afterhostfog share(backend/os now usesbackend/qrterm). - HF Tunnel gains (from HF Local): the service card (Ready · free, no account, Links on this computer,
Live now, Refresh, HF Local's messages when the service can't be reached or is off), the Share dialog's link
name field,
--name LINKand--server NAME,hf-tunnel servers [add|remove](the user's own HF Panel servers, likehostfog servers), stray tunnel clients ended at start with HF Local's message,/api/sharewith HF Local's actions (tunnels, start, stop, log; plus forget and open). - One interface:
frontend/shared/ui/Tunnel.tsx+styles/tunnel.css(service card, rows with Copy link / Open / QR code / Requests / Share / Stop sharing, request log, Share dialog head and link name field) are used by HF Local's Tunnel page and Share dialog and by HF Tunnel's window; the QR code is one component (frontend/shared/ui/QRCode.tsx, qrcode-generator; HF Tunnel's server-drawn/api/qr.svgis gone). HF Tunnel's 20 languages translate the shared texts too (36 new phrases; 11 old ones dropped). Removed from HF Local's main.css: the old Tunnel page and Share dialog styles (.srv*,.dot,.tn-h,.ways/.way,.suffix); the Share dialog's always-hidden "Through" server list (it only ever had the free links) is gone. - Docs and website:
tunnel/app/rewritten (Share an address, link names, buttons, options, your own servers),desktop-app/sharing/(Open, QR code, Requests, reconnect),desktop-app/command-line/,tunnel/index; HF Local's Help; the Software page; README.
HF Tunnel: a free app to share a local website
- New desktop app and command,
desktop/tunnel(owner's request): HF Tunnel shares a website running on the computer at a publichttps://<random name>.hostfog.iolink — free, no account, a new 10-character name at every start, removed from the server on Stop (or when the window closes / Ctrl+C), so it stops working at once. A link a crashed run left behind is removed at the next start (offline and older than 2 minutes). - Window: local address with quick picks (80, 8080, 3000, 5173, 8000), an optional Host header (virtual hosts
like
mysite.test), Start/Stop, the link with Copy, Open and QR code, status, request count and a live request log (time, method, path, status, duration); a notice before the first start; the HostFog look; English plus the panels' 20 languages (frontend/src/lang, the shared phrase translator), right to left for Arabic, Persian, Urdu. - Command:
hf-tunnel 80,hf-tunnel http://localhost:8080 --host mysite.test,--help,--version; prints the link, a QR code and every request; Ctrl+C stops it. One program: no arguments opens the window. - Safety: nothing listens publicly (frpc connects out; a relay on 127.0.0.1 only); HTTPS on the public side from
the tunnel server; only loopback and private addresses can be shared;
/phpmyadmin,/pma,/adminer,/server-status,/server-infoand local bundles' dashboard pages answer 403 through the link; the tunnel client reconnects with a growing pause (1 s to 60 s). - One tunnel client for both apps: HF Local's client (panel API client with certificate pinning, tunnel API,
frpc download/config/run, error messages, stray clients) moved to
backend/os/internal/tunnel; HF Local'sshare_tunnel.goandremote_servers.gouse it.internal/tunnel/tunneltestruns a real frps and a stand-in panel (HF Local's end-to-end test now uses it too;tunnel.LoopbackForTestslets it use 127.0.0.1). The QR encoder is the modulebackend/qrterm(hf-api's installer and HF Tunnel). No server change: the free-link endpoints were already anonymous (device key), with their limits and lifetimes unchanged. - Packaging:
desktop/build.sh tunnel→build/hf-tunnel/(Windows setup x64 + ARM64 that putshf-tunnelon the user's PATH, portable zips, macOS zip,.deband.tar.gzfor amd64 and arm64,HF-Tunnel-<v>-SHA256SUMS.txt), pure Go so every system builds anywhere;build.shstep "HF Tunnel"; website Software page and downloads (site.jsonhf_tunnel); help pagetunnel/app/.desktop-app.nsitakes-DREPLACE_OLD(HF Connect) and-DCOMMAND;packaging/windows/apps/hftunnel,packaging/macos/hftunnel-Info.plist.
Official-only screens and cards move from Server Admin to HQ
- Owner-approved: on the official edition with HQ's own address, everything official-only lives in HQ; the data, settings and actions are unchanged, and the paid edition looks exactly as before.
- HQ › Tunnels (
tunnels-admin, path/billing/hq/tunnels; the old/hosting/tunnels-adminstill opens it): in HQ's new Services group, insuperOnlyRoutes(a 403 with HQ's address from srv.) andofficialMovedToSuper. - HQ › Community blocklist (
superadmin-blocklist,/billing/hq/blocklist, screen_hq_feeds.go): the list's figures, top entries, Allow and the allow-list left Abuse watch for a screen of their own (actionsbl-allow,bl-allow-remove). Server Admin › Firewall's card is the same on every edition: the official server's 30-minute run records its published list as its sync (blocklist_synced/blocklist_listed). - HQ › WordPress feed (
superadmin-wpfeed,/billing/hq/wordpress-feed): importing the vulnerability list (feed-import) left WordPress updates, which keeps the list's state and, on a paid server, Fetch now. - HF Cloud's licence plan (
cloud_licence_plan) is set in HQ › Plans & pricing (HF Cloud card,licence-plans.cloud-plan); the Cloud settings dialog keeps only the paid edition's partner API key. - Overview tiles: the Tunnels and Licences tiles are on HQ's overview; Server Admin's overview shows the Licence
tile on the paid edition only and leaves out the tiles of every screen HQ has (
inHQ, fromofficialMovedToSuper). - HF Local's tunnel errors (
backend/os/internal/tunnel), README and MEMORY point administrators to HQ › Tunnels. Ctx.HQOn(official edition withportal_super) is the one check for "HQ has its own address".- Translations: the new texts in all 20 languages; Mail texts without a key got one (Mail: the expired-link notice, the new-sign-in email and its subject, the Settings password lines, the switch-mailbox page, "Cancelled", the forward limit; panel emails: "Open Mail", the spam summary's "And {1} more…" and switch-off lines, "Mail is ready", the Mail address and password-reset emails, the quota email's search line, the welcome email's calendars and contacts section). Three panel texts that name Mail (DavSetup and the mail import) no longer say "webmail" in any language. Help pages updated (Tunnels limits, settings reference, Cloud settings, Firewall, WordPress updates, the vulnerability list, the overview).
Clean-up: build output out of the source, dead Go code, tunnel leftovers
- The source holds no build output (owner): the built interfaces (
server-files/panel/public/ui,server-files/mail/public,backend/api/web/{files,dbs},backend/os/web,desktop/connect/frontend/dist), HF Connect's binary (desktop/connect/build/bin),frontend/{site,docs}/distandfrontend/site/deploy-outare gone;build.shmakes them. The embedded folders keep a.gitkeep(HF Local's//go:embed all:web), so the Go programs compile from the raw tree. - HF Tunnel leftovers on the paid edition: the OpenAPI description leaves out the Tunnels and desktop-tunnel
operations unless the server has tunnels (
OpenAPIDoc(servers, tunnels)withCtx.HasTunnels; the help site's copy is the paid edition's); the licence plans' "HF Tunnel server" service name is gone; install.sh and update.sh copyhf-tunnel-auth.pyon the official edition only; the help pages "Tunnels on your own server", "Customer tunnels", "Tunnels (Server Admin)" and "API: tunnels" are removed (links fixed; HF Local's free links page stays); the tests follow (tests/checks/tunnels-admin.pyswitches the sandbox to the official edition, apihost.py, tests/e2e, the e2e-screens baseline). - Browser installer: 11 texts the redesign no longer shows removed (and "The HF Tunnel server and customers'
tunnels.");
installArgsalways passes--domain(the DNS step requires one; install.sh's--no-domainstays). - Dead Go code (staticcheck U1000, deadcode, checked by hand on linux, windows and darwin): unused fields and a
constant removed (
htmlParser.encSeen,MailImportIn.quiet, the capacity report'stotal,haFailAfter, a test server'swg);UpperWords,osinfo.Info.RHEL,sieve.Stringand hf-api'scompany.TunnelSuffix/TunnelHostremoved; helpers only tests used moved into the tests (AppsCatalogCheckand its patterns,AppsCatalogUnpinned,HookVerify,HooksWait,PluginSignFiles, the caches'commands,cursorBook.size,qrterm.Code.Dark,studiodb.FindColl,check.Data(rules_data_test.go),appPathPrefixes,openAPIExampleServers, HF Local'ssaveServers/company.TunnelHost, HF Connect'sSavedPanelToken).
Clean-up: unused scripts, test data, images and texts
- Removed (owner-approved): the unused shell helpers
hf_step_pct(logo.sh, and its line intests/checks/logo-ui.sh),rowandrule(install.sh); two recorded PHP answers no test reads (the old service worker and web app manifest,internal/panel/testdata/golden); the help site's unusedtunnels-admin.webpscreenshot; 53 translation texts nothing shows any more (36 panel, 2 email, 15 Mail: old wordings such as "webmail", replaced plugin and licence texts), with everysrc/<code>.txtandtests/check-lang.allowrenumbered. The panel text "Cancelled — ends " lost its trailing space, so the billing screen'sc.T("Cancelled — ends")is translated again.
HF Tunnel: the official edition only
- HF Tunnel is no longer sold (owner): the
tunnelsmodule is gone from the licence catalogue (licfeatures.go: feature list, prices, the Pro plan), so from the website's pricing and plans (site.json), the licence shop and account, HQ › Plans & pricing, the installer's plan features (licence-setup.sh) and the help pages. Prices are unchanged. - The paid edition has no HF Tunnel at all, whatever its licence says: no Tunnels screen (now an official-only route) or menu entry, no Overview tile,
/v1/tunnels…and/v1/desktop/tunnels…answer 404 "No such endpoint", tunnel downloads 404, no tunnel domain (DNS lists, health and smoke test leave it out), and the agent refusestunnel-enable,tunnel-sync,tunnel-keyandtunnel-ca. One check:Ctx.HasTunnels(=c.Official()), agenttun_need_edition. The official edition keeps everything as before (tunnel domain hostfog.io, HF Local's free links); HF Local itself is unchanged. - Paid servers that had tunnels on: update.sh (Licence step) and
hf-agent licence syncstop and disable hf-tunnel and hf-tunnel-auth, take the tunnel sites offline and close port 7000 (hf_tunnel_off); the tunnel list, keys, certificates and settings are kept. Saved licences, licence records and catalogues that still nametunnelskeep validating (the key is ignored). - Tests:
TestTunnelsOfficialEditionOnly,TestTunnelsNotInCatalogue,TestLicenceOldTunnelsKey; tunnel-agent.sh (refused on the paid edition); check-installer.sh (hf_tunnel_off, update.sh and licence sync call it, agent commands gated); the Overview tile test; price fixtures updated.
Browser installer: the panels' look
- Every step (Code, Check, DNS, Setup, Licence, Admin, Installing, Done) sits in the panels' frame: the white 64px top bar with the HF mark, "Install HF Panel" and the edition; the warm grey sidebar with the server (IP, then the system, cores and memory, or the domain once known) and the steps as menu rows (done: green tick and a small line — "Server is ready", the domain, the admin, the minutes the install took; current: raised in the brand colour; next: numbered), a note at the bottom while installing and on Done; a white left-aligned page. On a phone (≤ 760px) the sidebar is a strip of the steps under the top bar. The step pills in the header are gone.
- DNS is a map: each name (green "CNAME" when it points here, pink "Not found" when not) drawn to the A record it leads to (solid green / dashed red, measured from the boxes and redrawn on resize), the
@andmailA records and a dashed "Optional for Email" box on the right, "N of M ready" with a bar and "Checked N s ago", then "Add every record at once" with Copy all andDownload <zone>.zone. Back, Check again and Continue (only when every required record is green). On a phone the map is the list without lines. - Installing: "Installing on <domain>", the share in large type with the running line's name, a striped bar (still with reduced motion), the lines with how long each took (setupweb:
tookper line from install.sh's "step done in" lines) beside "What's happening" (the log's tail, always shown). - Done: the "live" card (official: "HostFog is live"; customers: "HF Panel is ready") with Open Server Admin, the addresses as cards with Open/Copy (Server Admin, Control Panel, HQ on the official edition, Webmail, the server IP, the licence, the publisher's sites; the install command and DNS lines wide), the company account's new password in its own card with Copy and "Shown once" (setup-template.sh writes it as a
passline, setupwebdone.password), the to-do list as numbered dashed cards. - Check shows the processors (with their load), memory and disk as three cards with bars (setupweb state
server:HostInfofrom os-release and the panel's capacity readings). - Tests: the frame on every step, the sidebar's states and lines, the DNS map's lines per record and their colours, the phone list, the Done cards; setupweb
took,hq,password,server,osName. 16 new texts in 20 languages.
DNS: CNAMEs wherever DNS allows them
- Only the names that must be A records hold the server's IP: the bare domain (
@),mail.(an MX target can't be a CNAME),ns1./ns2.(glue) and, on a tunnel domain of its own,*and@. Every other name (www, srv, rs, cp, hq, fm, dbs, api, ftp, pay, hook, cdn, docs, webmail, smtp, imap) is a CNAME →<domain>.; the publisher's own website domain gets@A andwwwCNAME → itself. If the server's IP moves, only the A records change. MX stays MX →mail.<domain>. - Applied in each place's one list: the browser installer's DNS step (
dnsRecords;@is now a required A record for customers too, as the CNAMEs' target),hf-dns-setup/hf-agent dns-setup(list,--checkand the zone file), the server's own BIND zone (BuildZone: CNAMEs for the panel's names,@ Aadded when missing, an A record kept where a name has other records), the server-ready email, install.sh's pre-install DNS list, setup-template.sh's Done page and summary, the help pages (DNS setup, quick start, Rocky/AlmaLinux, choose a VPS, GoDaddy, Hostinger, Namecheap, OVHcloud, updating, CLI, glossary) and README. - Live check follows the CNAME: a CNAME row is green when its name leads to this server's IP and shows "Points to <IP> (via <target>)" (
net.Resolver.LookupCNAME, in parallel with the address look-up, 3 s). - Download DNS file next to Copy all:
<domain>.zone, a standard BIND zone file (;line with the server and day,$ORIGIN,$TTL 3600, the records with full names, MX priority 10, no SOA or NS) that GoDaddy, Cloudflare, Namecheap, Route 53 and most others import; one per domain (a second file for the publisher's website domain). The server builds it (zoneFile,GET /setup/api/zone?domain=…&site=…&zone=…with the browser token, domains checked withcheck.Domain) and Copy all copies the same text. The panel has no DNS screen for the server's own domain; DNS tools' zone export and the customers' DNS setup download are unchanged. - Tests:
TestZoneFile(exact text;named-checkzonewith a SOA/NS added when installed),TestZoneDownload, the records and the CNAME-following check with a fake resolver,TestOwnZoneNamesTheMailNames, the ready email; panel install tests (CNAME rows, via, Copy all text, the download links); check-installer.sh (hf-dns-setup's CNAMEs and zone file throughnamed-checkzone, setup-template'sdns_records). 2 new texts in 20 languages.
Browser installer: a DNS step after Check
- Check → DNS → Setup → (Licence) → Admin → Install. The DNS step asks the server's domain (moved from Setup; the publisher's server also the website's domain, offered from
COMPANY_DOMAIN/ the saved setup), lists every record to add at the DNS provider (type, name, value: srv., cp., rs., fm., dbs., api., ftp.; hq., pay., hook. on the official edition; @, www., cdn., docs. and the website's domain on the publisher's server; mail., smtp., imap. and MX as optional, since Email is chosen on Setup) with a copy button each and Copy all (zone-file lines), and Check DNS looks them all up at once (3 s each; again every 20 s while the page is open), with ✔ / ✘ and what each name points to. Continue works only when every required record points here, and the setup server checks again itself before it moves on. DNS is required: no domain-less install from the browser. One list:dnsRecordsininternal/setupweb. Tests: step order, the records and the live check with a fake resolver, the server's own check, the panel's DNS step; 8 new texts in 20 languages.
The installers never stop without a word
- Fixed:
bash official-install.shshowed the logo and ended with no message when the panel port was free: the browser installer's port check (hf_port_userinbrowser-setup.shnow) failed underset -o pipefailwhen grep found nothing. The same mistake fixed insetup-template.sh(Let's Encrypt rate-limit message without a date, the paid edition's empty build log),server-fixes.sh(nginx version),autofix.sh(free disk);grep -qafterjournalctl(install.sh) andopenssl pkeyutl -help(get.sh) could be killed by SIGPIPE and read as "no" — on get.sh that refused a correctly signed package on OpenSSL 3 without Python's cryptography module. install.shandsetup-template.shprint one line when a command stops them unexpectedly:Stopped at line N of FILE: COMMAND — see /var/log/hostfog-install.log(ERR trap,set -E; logo.shhf_err_line); a stop that already says why prints nothing more.install.sh --browserrun directly from the filesofficial-install.shunpacked says to runofficial-install.shagain instead of installing the paid edition without the publisher's later steps.- The setup page: the company account's password is no longer written to the install log ("Show details"); a re-run's Done page says the password is unchanged and how to set a new one; a setup server answering on the last try is no longer reported as not answering.
- Fixed (browser installer): a publisher's setup whose later step failed is no longer stuck for good —
hf-agent setup-code's link works once the panel has its administrator (only Try again follows), and a failed install nobody came back to for a day is removed like a finished one; a stop before install.sh restarts its log shows this run's reason ("Stopped at line …", or the output's last line) and details instead of the previous run's; Try again is no longer marked failed by a poll before the new run starts; the activity log masks the licence key.TestCloudRDNSno longer fails now and then. tests/check-installer.sh: the port check on a free port underset -euo pipefail, a scan forx="$(… | grep …)"without|| truein everyset -escript, the stop line (and none after a deliberate exit), the password mask.
Strict input checks on both sides, from one source
- Browser and server share one rule set. The Go rules (
backend/api/internal/check) write their limits, character sets, reserved user names, common-password list and typing runs tofrontend/shared/lib/rules.data.json, and their test cases tofrontend/shared/test/rules.cases.json(HF_WRITE_RULES=1 go test ./internal/check/); a Go test fails when either file is out of date, and the panel's tests run every Go case againstrules.ts, so the two sides give the same answer to each input. The browser now also checks reserved names, common passwords, typing runs, look-alike letters in domains and every other kind: IP address, network, list of addresses, host (name or IP), folder path, website folder, cron schedule, database and mailbox name, one line and several lines of text. - Every input is checked in the browser, not only the ones that opted in. A value the server checks names its rule (
data-rule), any other text input is one line without control characters, a textarea several. The rule is checked when you leave the input and before the form is sent; a form with a broken rule is not sent and the reason shows under the input. Every form of the panel (with the browser installer), Mail, Files, DB Studio and HF Local was gone through (about 170 inputs got their rule); a test in each app walks every input and fails when one the server checks has none. - The strength meter never says "Strong" for a password the server refuses: a common one ("Welcome2026!!"), too short, built from the username or email, a typing run or too few different characters shows "Weak" with the reason, in the installer's admin step and the panel's first-run setup (both now compare it with the username and email typed).
- Server: every request body is read strictly — a JSON key twice, text after the value, broken escapes or nesting too deep are refused (
check.JSON: the panel and its API, the token API, licence and partner endpoints, payment webhooks (also: id and kind must be texts, at most 1 MB), Files, DB Studio, Mail, the browser installer, HF Local's window API); a NUL or other control character (besides tab and line breaks) in any field is refused as a field error of that field (panel, token API, Mail, the installer, HF Local); unknown fields are ignored; bodies have size limits (token API 8 MB, HF Local 8 MB). Typed values are kept trimmed and in Unicode's composed form (check.Clean, used byCtx.Post). - One home: hand-written patterns replaced by
internal/check— website and FTP folders and protected-folder paths (check.Folder), backup SFTP and mail-relay hosts (check.Host; IPv6 relays now accepted), the installer's domain and email checks (check.Domain/check.Email). New passwords for backup and panel-archive passphrases and VPS root passwords follow the password rules. Existing passwords and passphrases keep working. - Tests: Go cases for every rule (also run by the browser),
TestRulesData(drift),TestJSON,TestBodyStrict,TestTokenAPIStrict,TestWebhookEvent,TestPostClean, HF LocalTestDecode; browser tests for the shared cases, blur and submit blocking, the meter ("Welcome2026!!" = Weak) and the every-input walker in all five apps. 9 Mail texts added in 20 languages (the panel already had them).
Strong input checks in every panel, with the reason under the input
- One home for the rules:
backend/api/internal/check(Go) — account user names (3–16, a–z/0–9, a letter first, the agent's whole reserved list), panel logins (3–32, a–z 0–9 _ . -), new passwords, emails, domains, ports, IP addresses and networks, folder paths, database and mailbox name parts, cron schedules and one-line / several-line text. Each returns a specific reason ("At least 12 characters.", "Only a–z and 0–9.", "That name is reserved. Choose another.") that the screens put under the input. The panel'sValidUsername/ValidPassword/ValidDomain/ValidEmail/ValidIP/ValidCronSchedulenow use it; the duplicate reseller, staff and admin user-name patterns are gone. - New passwords (Server Admin, Reseller, Control Panel, team members, password resets, mailboxes, FTP, databases, MongoDB, Web Disk, protected folders, apps, the website account): 12–128 characters, no quotes, backslashes or control characters, at least 5 different characters, not a common password or a typing run ("Password123!", "Welcome2026!!", "123456789012"), not the username or email with a few characters around it. Existing passwords keep working: only new and changed ones are checked. Generated passwords already meet the rules.
- Domains: typed with letters of another alphabet they are kept in their xn-- form (bücher.de → xn--bcher-kva.de); a part mixing Latin with Cyrillic or Greek look-alikes ("pаypal.com") is refused; a trailing dot is dropped; each part at most 63 characters, 253 in all, an ending of letters. Emails use the agent's shape (no quoted parts, spaces or line breaks). The agent's
is_domain/v_emailaccept xn-- endings. - More forms answer with field errors instead of a page error: account import, billing order, free licence, mailing lists, Node.js/Python/Ruby environment, MySQL/MongoDB remote addresses, MongoDB names and passwords, DNS cluster, protected folders, team join, Web Disk share links (now also through Files), Files' new names, renames, copies, zips and permissions, DB Studio's import file, and Mail's forwarding, settings, contacts, event attendees and recipients. Files refuses new names with invisible direction marks (an existing name can still be renamed). Cron schedules check each field's range (
61 * * * *is refused). - Browsers check the simple rules while you type (
frontend/shared/lib/rules.ts,data-rule="password|username|login|domain|email|port"on an input; the reason shows under it like any field error); the server stays the authority. The password strength meter uses the same rules. - Tests: Go table tests for every rule with hostile input (look-alike letters, CRLF, NUL, shell and SQL characters, direction marks, very long values) and a fuzz test (
FuzzDomainEmail); the reserved list against the agent's; screen tests that each bad value comes back under its input (TestInputRulesOnScreens,TestNameFieldErrorsin Files,TestInputRulesInMail); browser tests (inputRules.test.tsx: the rules, a rule shown before sending, the server's reasons under the inputs). 26 new texts in 20 languages.
The official installer finishes in the browser
bash official-install.shwith no options now uses the same browser installer ascurl -fsSL https://cdn.<domain>/latest | bash: it unpacks, checks the server, starts the setup page and prints the logo, one linkhttps://<IP>:<port>/setup?code=…and a QR code. Check → Setup → Admin → Installing → Done happen in the browser, in the panel's look. No Licence step: the page shows "Official edition".- Setup asks what the terminal install asked: the domain (offered: the build's
COMPANY_DOMAIN, or a re-run's), the website's domain (empty = the same), the email, email/DNS/PostgreSQL/Docker, and the publisher's parts (website, download servercdn., docs, paid edition, company account), all ticked. - Installing covers the whole setup: install.sh's steps, then the company account, download server, website, docs and paid edition, each a line with progress (one share for both). A later step that fails is tried again on its own (the panel already has its admin). Done lists the panel's addresses, the sites, the company account (its password once), the install command for other servers, the DNS records and the to-do list.
- One home per step:
setup-template.sh --after-install --result FILEruns its own steps after the panel from the unpacked files (the setup page runs it afterinstall.sh --from-setupin the samehostfog-installunit, withHOSTFOG_EDITION=official); the terminal path runs the same code.hf-api installer inittakes--official-env,--after,--domainand--website(browser-setup.shpasses them). - Unchanged: any option (or no TTY with options) keeps the terminal install; an installed server is updated (
update.sh), no browser; a server whose install stopped halfway carries on in the terminal. Run again while the setup page waits, the installer only prints a new link. - DNS checks: every name at once with a 3 s limit (
hf_dns_checkinlogo.sh, from install.sh's earlier fix), now also in install.sh's panel certificate step, the one-file installer's site certificates and the setup page's live check (parallel, 3 s). - Fixed:
HOSTFOG_EDITION=officialthroughget.sh's browser path was lost when the setup page started install.sh (the server was written as the paid edition). - Tests: setupweb (
TestPublisherSetup,TestJobScript,TestInitPublisher), panel UI (publisher Setup, Installing and Done; Official edition on Admin),check-installer.sh(a small one-file installer run with and without options, step names against the page's lines,hf_dns_check). 7 new texts in 20 languages.
HF Tunnel addresses directly on the tunnel domain
- A tunnel domain of its own gives addresses right on it:
<name>.<tunnel domain>(app.hostfog.io,myshop.hostfog.io), no.port.label. Its DNS is two records:*and@. When the tunnel domain is the main domain (emptyCOMPANY_TUNNEL_DOMAIN, or a server owner's single domain) the addresses stay<name>.port.<domain>, apart fromsrv.,cp.,mail.and the other names. Old<name>.port.<tunnel domain>addresses on a domain of its own are not kept (owner's decision). - The tunnel domain is kept apart from the main domain: its bare name and
www.show a plain HF Tunnel page (no redirect, no link, address or name of the website), an unknown name shows "No tunnel here" (404), the offline page names nothing of the main domain; static pages only, no cookies. Its certificate is its own:*.<tunnel domain>and the domain itself (Let's Encrypt's DNS check throughhf-acme-dnswhen its zone is on the server, as for any wildcard; otherwise one certificate per tunnel and one for the bare name andwww.). No email addresses on it. - One place per language decides the form:
company.TunnelSuffixOf/TunnelSuffix/TunnelHost(hf-api and HF Local),Ctx.TunnelSuffixFor/TunnelOwnDomain/TunnelDNSNamesfor a server's own tunnel domain, the agent'stun_suffix/tun_own(lib/98-tunnel.sh, also used byhf-dns-setup, the one-file installer andbuild.sh). The frontends take the suffix from the API: HF Local's/api/statetunnelSuffix, the Tunnels screen'ssuffixanddns. - Reserved tunnel names:
hqandportadded to the existing list (www,mail,smtp,imap,ftp,api,cdn,docs,pay,hook,srv,rs,cp,fm,dbs,ns1/ns2,admin,status,hfserver,hflocal…). - Server Admin › Tunnels: you type the domain (filled in with the suggested one) and the screen shows the links it gives and the DNS records it needs (
*and@, or*.port); the server-ready email lists the same records. - HF Local: Tunnel page, Share dialog, Help and
hostfog shareshow<name>.<tunnel domain>;sources.jsonre-signed. hf-agent dns-setuplists a tunnel domain of its own as its own table with only*and@; the installer's summary and to-do list do the same. Website, help site (free links, own server, Tunnels, API, DNS set-up), README andclaude/notes use the new addresses.- Tests: both forms and the reserved names (
TestTunnelHostin both company packages,TestTunnelSuffixForms,TestTunnelReservedNames, the Tunnels screen and HF Local'stunnel.test.tsx), the new agent checktests/checks/tunnel-agent.sh(forms, pages, Nginx site served by a real nginx),check-installer.sh(dns-setup and installer DNS names). - The translations'
_keys.jsonfiles (missing from the tree) are back; 6 new texts in 20 languages. - More tunnel tests (edge cases, handlers, hostile input, fuzz
FuzzTunnelNameOfHost, 200-goroutine load, real nginx with a stand-in tunnel server) and the fixes they found: a trailing dot (hostfog.com.) no longer turns the main domain into a domain of its own (Go and agent); the host parser ignores case and a trailing dot and gives only valid names; Switch on checks the domain as the agent does (labels up to 63 characters); unknown names on the tunnel domain get "No tunnel here" over http without the wildcard certificate too.
Optional tools removed; translations built, not stored
- Removed (owner-approved): the load test (
tests/load), the Playwright dialog and link testers (tests/ui), the throw-away-server tests (tests/real-install.sh,tests/deploy-test.shand their folders) and the API client SDKs (integrations/sdk, withintegrations.sh --sdk-testand the help page Developers › SDKs; the API explorer's OpenAPI description stays). The WHMCS and Blesta modules are unchanged. - The translations'
<code>.jsonfiles (panel, panel emails, Mail) are no longer kept in the source:tools/languages/build-all.shmakes them from_keys.jsonandsrc/<code>.txt(byte for byte the same files).build.shruns it in its first step; after a fresh unzip run it once beforego test,npm testortests/check-lang.py, which stop with a "run tools/languages/build-all.sh" message without them.
One README
source/README.mdis the only README: what it is, quick start (config,build.sh,official-install.sh, local run), folder map, then backend, frontends, desktop apps, agent, installer and scripts, integrations and SDKs, tests, translations, keys, releases, commands and the address reference. The 30 other READMEs,backend/os/BUILD.mdandpackaging/UPDATES.mdwere merged into it and removed; outdated paths (installer/,backend/agent/,backend/os/packaging/, the PHP reference) were corrected on the way.- The WHMCS and Blesta zips get a short README made by
scripts/build/integrations.sh(pointing to the set-up guide on the help site). The SDK packages no longer list a README (pyproject, Cargo, gemspec, package.json, the NuGet project); usage per language is on the help site's SDKs page. claude/: CONTEXT-FOR-NEW-CHAT, PROJECT-A-TO-Z, PROJECT-STATUS and ROADMAP-1.1 merged into CLAUDE.md and TASKS.md (open owner items, decisions and the roadmap after 1.0) and removed. The old notes besidesource/(launch drafts, Lynis and phpMyAdmin how-tos, the start-here and build-and-install files) are gone; the project zip's README points tosource/README.md.
HQ fixes: prices, tables, lists, focus
- One home for licence prices: Licence keys › Selling shows the plans' prices from Plans & pricing (read-only, with a link there) instead of its own Solo/Pro/Unlimited fields (£4.99/£9.99/£19.99 defaults,
lic_price_*). The old tiers' price (licences from before plans, bulk partners, tier changes, monthly revenue) is now the catalogue price of the plan on that tier (Ctx.LicTierPrice); the savedlic_price_*settings are no longer read. - Licensed servers: version, OS and accounts are one line under the server, so the table fits its card at 1280 and 1440 px (the IP button was cut off).
- User analytics: only a chart (or its empty note) is centred in a chart card; lists such as Most used screens and Devices start at the top.
- Help: the search box has one focus ring (the box's), not two.
- Managed customers: the panel address shows without
https://, ends in … when long, and the full address is in its tooltip.
Developer docs folder removed
docs/(the developer docs and the engineering guide) is gone, and with ittools/docs/make-guide.py(the developer-guide PDF). Code comments, READMEs, build messages andclaude/notes no longer point into it: they explain in place or point to the help site; the local-run commands with sample data are inbackend/api/README.md.
One domain for everything; HF Tunnel addresses on their own domain
config:COMPANY_DOMAIN="hostfog.com"is the website and every service (panelssrv.rs.cp.hq.on the panel port,mail.fm.dbs.api.cdn.docs.pay.hook., thecontact@/no-reply@/security@addresses);COMPANY_WEBSITEis empty (the same domain: the bare domain serves the website, no redirect). NewCOMPANY_TUNNEL_DOMAIN="hostfog.io": only the HF Tunnel addresses (*.port.<tunnel domain>); empty =COMPANY_DOMAIN, the old single-domain tunnel names.- Plumbed like
COMPANY_WEBSITE:company.sh(load,hostfog.io,-X …CompanyTunnelDomain),build.shasks Tunnels after the website (--tunnel-domain=;--yeskeeps the saved value),company.TunnelDomain()in hf-api and HF Local, the website, help site and HF Local window (COMPANY_TUNNEL_DOMAIN), the server package'sconfig, HF Local's README and signedsources.json(re-signed, now with placeholders),tests/check-company.sh(the tunnel domain is a company domain). - HF Local: free links, the Tunnel page, Share dialog, Help and
hostfog sharenameport.<tunnel domain>; it still reaches the publisher's server onapi.<domain>. The website's and help site's tunnel examples use the tunnel domain; the help site also fills the company into page descriptions. - Server Admin › Tunnels on the publisher's own server offers
port.<tunnel domain>;hf-agent dns-setuplists a tunnel domain outside the panel's domain as its own table (not in the zone file). The tunnel server itself is still switched on in that screen, otherwise unchanged. - One-file installer: the summary's DNS part lists the A records per domain (the domain: all names; the tunnel domain: only
*.port), and the to-do list says to switch on HF Tunnel withport.<tunnel domain>until it is on. - LICENSE, project notes (
claude/, CLAUDE.md's address table),keys/README.md, the launch drafts and test fixtures use the one domain.
Clean-up: frontend code, second pass
- Panel: one
Buttonfrom the kit instead of 54 hand-made busy buttons; one copy button (CopyTextButton, used byCopyButton, Email and Domains);useEditFormfor the New/Edit dialogs of Cloud hosts, images, plans and pools;takeParam(lib/router.ts) for the setup code and the wizards'?new=; the browser installer's steps share one submit helper; the sign-in pages share their badge and message (@hf/shared/ui/SignIn). - Comments without history or notes to the reader; the unused
eslint-disablelines removed (the project has no ESLint); three duplicate CSS rules merged. No feature, screen or look changed.
Release folder: one file
build.shputs one file inreleases/:official-install.sh(formerlyreleases/install.sh), the one-file installer with the server, download server, website (HF Local, HF Connect and the WHMCS/Blesta modules as its downloads) and documentation inside.- Everything else goes to the work folder
build/next tosource/:hf-local/,hf-connect/,hf-panel/(the server package),integrations/andbuild.log. It is kept between builds, so--server-onlyreuses the last desktop build.desktop/build.shwrites tobuild/hf-connect/by default. - Folders from earlier builds in
releases/are left as they are. The private-key check coversreleases/andbuild/. - Docs, notes and
tests/deploy-test.shuse the new names.
Git branch previews removed
- Removed: the
<branch>.preview.<domain>websites of Git branches, the previews part of the Git screen (previews on/off, branches, keep days, the previews table, Rebuild / Remove), the package limit Git previews (0 = unlimited), thepreview.deployedwebhook event, thepreviewscount in the API's Git repositories, the agent's preview mode andHF_PREVIEW, and the help page. - Live branch deploys stay. The screen Deploys & previews is renamed Live deploy (
git-deploys; the oldgit-previewsaddress and team permissions open it): the live branch deploys to its website on every push, with the deploy script and build log. The job ishf-api git-deploys(formerlyhf-api previews), the agent commandgit-deploy-build(formerlygit-preview-build). - Updating:
update.shreplaces the old cron line, andhf-api migrateremoves preview websites made by an older version (their Nginx/Apache sites, files and DNS names in the live domain's zone). The tablesgit_previewsandpkg_previewsare kept, unused. A*.previewDNS record made for previews can be deleted. - Not affected: HF Deploy's pull-request previews (
<name>--pr-<n>on the apps domain) and Maintenance mode's private preview link.
Browser installer
curl -fsSL https://cdn.hostfog.com/latest | bashwith no options checks the server, draws the logo and within about 20 seconds prints one link,https://<server IP>:<panel port>/setup?code=XXXX-XXXX, and a QR code of it (hf-api installer qr, encoder ininternal/qrterm). The SSH window can be closed: the setup page runs as the systemd unithostfog-setup(hf-api installer serve,internal/setupweb) on the panel port, with a self-signed certificate for the IP that the panel keeps until it has a real one.- The browser steps, in the panel's look (HF mark, top bar, wizard step strip,
HfLoader;src/install.tsx,src/install/):- Check: system, memory, disk, public IP, ports 80/443/25/panel (precheck.sh); another control panel or websites already served stop it.
- Setup: domain (optional, with a live check that
srv./cp.point here), email, and what to install: websites and MariaDB always; Email and DNS on, PostgreSQL and Docker off by default. - Licence: free trial or key, checked with the licence server (
licence-setup.sh check|trial). - Admin: the login, a password strength meter and the licence it will run with.
- Installing: live percentage, steps ticking off, Show details with the log's tail; a reload or later visit carries on; a failure shows why, with Try again.
- Done: addresses, whether real SSL is in place, licence, Open my panel. The "server is ready" email has the same.
- The setup page runs install.sh itself (
install.sh --yes --from-setupwith the chosen options, as the unithostfog-install) and reads its step lines. New install.sh options:--browser,--from-setup,--admin-file(the login as a bcrypt hash;hf-api admin setup-admin),--no-domain(one panel on the IP),--postgresqland--docker(step "Extra software"). Unattended installs in the terminal are unchanged:… | bash -s -- --yes --domain … --email … --trial|--licence KEY, orbash latest --yes …aftercurl -o latest. - One install address:
cdn.hostfog.com/latest;InstallCommand()returns thecdn.command. The separate short install address is removed (its Nginx and certificate names,HF_EXTRA_DNS, DNS hints, website, help site, licence email, docs and checks). - At step "Panel on port" the setup page moves behind nginx (
/etc/nginx/hostfog/setup-proxy.conf, keeping the setup's certificate for the IP). After Done it removes itself (unit, state, nginx file, download folder). After 24 hours without a visit it stops; while it waits,hf-agent setup-codemakes a new link (and starts it again), and other hf-agent commands say the install is in the browser. The SSH login message shows during the setup and is removed with the admin login. - Security: one package for the setup code rules (
internal/setupcode, also used bysvc_setup.go): 8 characters, stored as SHA-256 only, valid 24 hours, usable once, exchanged for a browser token (HttpOnly, Secure, SameSite=Strict cookie). 10 wrong codes per address in 15 minutes or 20 in total void it. Every call is JSON with theX-HF-Setupheader, same origin, and is refused once the panel has an administrator. Log:/var/log/hostfog-setup.log. Only the setup page, its API and the panel's built files are served. - Also: trial days left in the panel's top bar (
LicenceBoottrialDays); the password strength meter on the panel's own setup screen (ui/PasswordMeter.tsx); the page's style sheets include those of shared chunks (panel.UIEntryCSS: with two entries Vite moves shared styles there);precheck.sh --plain --setupand a "Websites" check; 62 new texts in 20 languages. - Tests:
internal/setupweb(code rules, steps, install.sh options, progress parsing, refusal after the admin exists, automation options, lifecycle),internal/qrterm(same symbols as qrcode-generator),TestSetupAdminFrom,TestTrialDaysInTopBar,TestManifestSharedCSS,test/install/install.test.tsx, check-installer.sh "Browser installer", setup-motd-agent.sh. - Fix: the built-screen caches (
uiPages,UiBuilt) are tied to one manifest file, so a panel with anotherPanelDirno longer reuses a stale answer (TestLicencePausedReadonlybroke the following flow tests).
One install command, licence added in the panel
curl -fsSL https://cdn.hostfog.com/latest | bash.get.shasks no licence question: it downloads the paid-edition installer without a key (/licence/downloadserves the stable channel to anyone, rate limited, recorded as "no key").--licence KEY/HF_KEY=…still activate a key during the install. New options for unattended installs:--yes,--domain,--trial.- The install ends with one setup link,
https://<server IP>:<panel port>/setup?code=XXXX-XXXX: random, shown once, stored hashed, valid 24 hours, usable once.hf-agent setup-codemakes a new one and voids the old. Until the setup is done, the SSH login message (/etc/motd.d/hostfog-setup, agentsetup-motd) says how to get a link; it never contains the code. - The setup screen (
screen_setup.go,svc_setup.go,pages/setup.tsx): the code from the link (a box only without it) → Add your licence (Start free trial with an email, or a key; See plans) → Create your admin login → signed in. Wrong codes are counted per address and in total (20 void the code); every step is refused once an administrator exists; activity log entriessetup.*.hf-agent create-adminand the setup shareCtx.AddAdmin. - Without a valid licence (none yet, trial ended, expired), websites, email and databases keep running. Administrators can open every screen with a "Changes are paused" banner; only the Licence screen and their profile accept changes (
shellReadonlyGuard, the same guard as read-only staff). Customers and resellers keep the lock screen.LicenceOfflineDaysis unchanged. - HQ › Licence keys: KPI Installed, no licence yet and Waiting rows from keyless installer downloads (last 30 days, no licence for that IP). Server Admin's Getting started checklist (licence, two-factor, first account).
- Licence emails give the key, the same install command for everyone and "open your panel and paste the key". The website (Download, Pricing "Just want to try it?") and help site (Install, Licensing, Customise, Server commands) describe this flow.
- Tests:
svc_setup_test.go(anonymous download, waiting list, setup code valid/used/expired/brute force, steps, trial, key, read-only while paused,setup-codecommand),tests/checks/setup-motd-agent.sh,test/pages/setup.test.tsx, check-installer.sh checks.
Small fixes: build steps, order page, screenshots
build.shcounts its first step (shared files): the log shows "14/14" instead of "14/13" (8 desktop-only, 9 with--server-only).- The order page's "You're all set" has no emoji (same key in all 20 languages).
- HF Local's top bar shows "Local" instead of "Server"; its 15 website and help-site screenshots are retaken with the HF Local title. The help site's Refer a friend and Affiliate screenshots are retaken (both showed the "Not found" page).
Website: documentation only on the help site
- The website no longer builds its own
/docs/pages (14 copies of the help site's installation and reference pages). Every documentation link goes todocs.hostfog.com, and the Contact page shows that address. - Old links keep working: each
/docs/<page>/redirects (301) to the same page on the help site, any other/docs…address to its home (website.nginx.conf,.htaccess).
Clean-up: panel and shared frontend code
- One copy each of: the root password dialog (
PasswordDialog, formerly three), the VPS date and size text (dateTime,sizePartsinui/cloudAdmin.tsx),gbFmt(inlib/format.ts), the cloud image and order plan types, and "reload while something runs" (useReloadWhileinui/kit.tsx, formerly six copies in HF Cloud, PITR restores and bulk changes). - HF Deploy uses the panel's kit: its settings, price, variable, domain and service-settings forms send with
useSend(replacing the screen's ownuseSaveand three local senders), the Services list is aCard, connection and webhook details use.kv; onebareUrlreplaces five copies of the same replace. - Shared by every app: button spinners (
@hf/shared/lib/pressBusy, with askipoption for HF Local's side menu) and the icon set (@hf/shared/ui/Icon,icons.ts; the panel's copies are removed).sync-shared.shmakes the panel'sassets/fonts/fonts.cssfromfrontend/shared/fonts/fonts.css. - My VPS: the Traffic figure links to the VPS's Usage tab, where the allowance is managed.
- Removed unused code:
formatNumber, the console's unusedpassword/onState/classNameoptions, the.site-tagand.live-spinstyles; the managed hosting screens useButtoninstead of hand-written button markup. No feature, screen or setting changed.
Clean-up: website, help site and the smaller apps
- Shared frontend code in one copy: Mail uses the shared base stylesheet (
app-studio-files.css, replacing its 87 KB copy), the shared translator and button spinners (@hf/shared/lib/i18n,lib/pressBusy; HF Local too). Files and DB Studio share their sign-in pages (@hf/shared/ui/SignIn), dialog helpers (lib/dialog.ts:trapTab,holdBusy) and frame styles (styles/files-studio-shell.css). HF Local uses the shareda11y.tsand HF mark; the help site uses the shared lockup and the website's Vite config. - Addresses: the website and help site link the licence shop and account on
pay.hostfog.com, and the website's/licence…and/accountredirect there (website.nginx.conf,.htaccess);/ordergoes topay.hostfog.com/order. The Apache panel shortcuts carry the panel port, and its/statuspasses to the panel instead ofapi.. The screenshot address bars show the panel port and path addresses; the install page's DNS link goes to the help site. - Website: no repeated menu and footer links; the unused plan-builder price code is removed from
site.js(its check against the licence server stays intest/price.test.mjs); plainer home-page text. - Help site: one Files and one DB Studio screenshot (two copies removed); no mentions of the plan builder or of other file transfer apps; the order-page tutorial names
pay.. - Project notes (
claude/,docs/): the address map in CLAUDE.md; websitehostfog.com, account onpay., 30-day trial and no plan builder throughout; the engineering guide and READMEs describe the shared files instead of copies.
Licence and account links go to pay.<domain>
- Licence pages, licence emails, "renew at …" / "change the server IP at …" messages, and Stripe and PayPal returns use
pay.<domain>when the billing pages have it (Ctx.PublicURL), else the website, else the panel. The account's Install guide opensdocs.<domain>/installation/(company.DocsURL). - Installer: with
pay.<domain>, the website redirects/licence…and/account…there (301, path kept) instead of passing them to the panel;/statusand the live chat stay on the website. TestTestPublicURLPrefersPay.
Clean-up: Go code without copies
- Files and DB Studio share one
internal/webapppackage: security headers, JSON answers, request reading, the session lookup, assets and branding logo, languages (Langs) and the forgotten-password steps (Forgot, replacing the twoforgot.gocopies). - One recorded-PHP-answer reader for the tests (
internal/phpgolden, formerly threegolden_test.gocopies). The panel's email check issieve.ValidEmail(formerly two 110-line copies). - Panel: one
randHex,sha256HexandhmacHex(formerly 13 copies under other names), and one each of the "1"/"0", RFC 3339 time, mail-limit text, URL host, server list, staff owner, site-screen and<br>helpers;cmp.Or/slices.Containsinstead of hand-written copies (panel,cloud,deploy). - Removed unused code: the plan builder's leftover
licFeatureGets,PluginNetHosts,LicRouteFeature,requestPort,API.panelPort,company.ServerURL/Copyright, Mail's unused strict email check (filtervar.goand its recorded answer), HF Local'swriteHttpdConfwrapper, HF Connect'sKnownHostsOf. No feature, screen or setting changed.
Clean-up: Go code, second pass
- No behaviour changes. Comments: the decorative separator lines are gone (508 lines in 156 files), and comments no longer narrate history or speak to the reader. One JSON string escaper (
omapJSONString) for the panel's PHP-compatible encoders (billing, licensing, lists, hosting: formerly four copies);errors.Newwherefmt.Errorfhad nothing to format (37 files). - Named constants for repeated answers and limits (
internal/setupweb,setupcode.TTL,cloud.MaxDiskGB, the host's clock skew, nonce and console times). Long functions split into named steps: HF Cloud's order (cloudOrderBuyer,cloudOrderPayPal,cloudOrderStripe), a deploy's image and environment (deployRunImage), HF Connect's SSH chain (hops,dialHop), a backup download (copyBackup). Smaller copies removed: the VPS create seed,virshwith input, the setup's IPv6 address joins (net.JoinHostPort), deploy domain lookups, a VPS's power checks (cloudReady).
Clean-up: shell code without copies
- Agent checks:
check,refuses,stuband the closing count are defined once intests/checks/lib.sh(formerly copied into 41 checks); every check ends with the same "all good" / "N failure(s)". - Agent:
is_domainandhas_systemdin00-core.sh, used byv_domain, portals, mail relay and protection lists (replacing four copies of the regex and a helper in78-redis.sh).79-deploy.shhas one each of the repository/branch check, git token environment, size check, CPU value, cgroup slice, running-container lookup and Nginx reload (formerly two or three copies each). - One-file installer: one
serve_site/site_stepfor the download server, website, main-domain redirect and docs; oneask_domainquestion;check_domainusesis_domain; unusedsay/okremoved. - Translations: one
tools/languages/build.py FOLDER [codes]for all three dictionaries (replacing threebuild.pycopies; output byte-identical).tools/brand/make_brand.pywrites the desktop icons topackaging/(formerlybackend/os/packaging).
The desktop app is called HF Local (formerly HF Server)
- Every visible name: window and PostgreSQL Manager titles, tray and messages, the CLI's texts, the Windows setup (wizard texts and new artwork, shortcuts, Apps & features name, exe details),
HF Local.app, the Linux menu entry, starter pages, READMEs, LICENSE and third-party notices, the website (Download, Software, menus), the help site (Desktop app section, tutorials, tunnels), the panels' Tunnels screen in 20 languages, the developer docs, guide and PDF. - Downloads:
HF-Local-<v>-Windows-Setup.exe(and-Setup-Offline.exe,-Windows-portable.zip,-macOS.zip),hf-local_<v>_<arch>.deb,hf-local-<v>-linux-<arch>.tar.gz,SHA256SUMS.txt; build folderreleases/hf-local/(build.shrenames an existingreleases/hf-server/once);site.jsondesktop entries. - Existing installs upgrade in place. Windows: the setup keeps the Apps & features key and
Software\HostFog\Panel(so it installs into the old folder, e.g.C:\hfserver) and replacesHF Server.exe, its uninstaller, shortcuts and sign-in entry. macOS keeps the bundle IDcom.hostfog.panel, Linux the packagehostfog-panel.~/hfserverstays the data folder when it exists (new installs:~/hflocal,C:\hflocal). The local CA made as "HF Server" keeps working. The update notice comes from the signedsources.json(re-signed) and points at the website's Desktop page. - Tests:
TestHomeFolderKeepsOldFolders, old names inTestCANamesandTestPortableProgramFiles.
The public website on its own domain (COMPANY_WEBSITE)
confighasCOMPANY_WEBSITE(empty =COMPANY_DOMAIN, so single-domain builds are unchanged); this tree sets the website tohostfog.comwhile the services stay onhostfog.com.build.shasks Website after the domain (--website=DOMAIN;--yeskeeps the saved value).company_fillfills the website placeholder, Go getscompany.Website()/WebsiteURL()(-X …CompanyWebsite), and the website, help-site and desktop app builds read it fromconfig.- Website: canonical addresses, sitemap, robots.txt, security.txt, JSON-LD, Open Graph,
.htaccess(http andwww.→https://<website>) andwebsite.nginx.confuse the website; panel shortcuts,api.,cdn.,docs., mail addresses and HF Tunnel stay on the main domain. The help site's header/footer, contact/pricing/account/status links, the desktop apps' website links and setup metadata, the WHMCS/Blesta modules' homepage andget.sh's pricing link use the website. - One-file installer: the Website question defaults to the build's website on the publisher's own domain (a saved website equal to the main domain moves to it). Once
https://<website>has its certificate, the bare main domain andwww.redirect to it (301, path kept), either as the company account's root-only extra or as ahostfog-mainsite with its own certificate (hf-agent company-accounttakes the redirect over). The live chat (/chat/) is passed through on the installed website too. - Checks:
tests/check-company.shalso catches the website domain typed literally; the website's link check fails on links to the bare main domain when the website has its own;tests/check-installer.shchecks the website settings, fill, installer default, and the redirect and chat rules withnginx -t; GoTestWebsite,TestExpandWebsite.
The panel port follows config everywhere
- The panel port chosen in
build.sh(COMPANY_PANEL_PORTinconfig) reaches every file:precheck.sh(bash latest --check) checks the build's port instead of a fixed 8443 and takes--portfrominstall.sh --check; the developer guide PDF (tools/docs/make-guide.py) prints the configured port; the SDKs, READMEs,docs/and the engineering guide write<panel port>. A server's ownpanel_portstill takes precedence. tests/check-company.shfails on 8443 typed literally in code, docs or panel texts (fallbacks,legacy-port,config-defaultlines and the desktop app's local HTTPS port are allowed) and warns about 2087 or the configured port in code.
HF Cloud: review fixes
- Security: the host's
/consoleanswers the panel's addresses only;vps-backup/vps-restoreonly take the VPS's own backup keys; a downloaded image or a backup about to be restored must be a standalone qcow2 (Driver.CheckDisk: no backing or external data file); a VPS can't send IPv6 router advertisements or redirects; a join code can't be used by two joins at once; an address taken meanwhile fails the create instead of being shared. - Billing: a resize moves the Stripe subscription's plan item found by its product (a changed plan price no longer makes the resize free); a resize the subscription can't carry is refused before it starts; turning backups off finds the item the same way.
- Tasks: a refused snapshot or backup no longer leaves a row behind (made in the task's transaction); the firewall and the 5-minute throttle wait for a running task. Order page and My VPS show the plan's own backup days.
cloud-host.shmakes/etc/hf-cloudfirst and is shellcheck clean. VPS list: the HF Panel licence key may stay empty (one is issued); the delete and suspend questions are translated; Escape in the console's Send text closes only that box.- HF Deploy: containers can't reach the server's own addresses or 169.254.169.254 (
deploy_guard: chainHF-DEPLOYfromINPUTandDOCKER-USER, restored when Docker starts, removed by uninstall). - Tests: Go
TestCheckDisk,TestCloudResizeStripe,TestCloudBusyVPSand more cases; 11 agent checks; New card count on Hosts, Plans, Pools, Images and Discount codes; bundle price fixtures in catalogue order. 2 new texts in 20 languages.
HF Deploy and managed hosting: review fixes
- HF Deploy: two pushes no longer build and switch at the same time (a release waits for the one in progress, so an older commit can't go live last); a push of a commit the service already has is ignored (webhook replays); the webhook reads the body only for a known service (25 MB at most); preview branch names are checked.
- HF Deploy: repository addresses on the server or its local network are refused on save and in Detect; access tokens and variable values are checked (no line breaks reaching the agent's input or the container's environment file).
- Agent (
79-deploy.sh):deploy-routecan't replace another account's site; removing serviceNAMEno longer deletes the environment files of a service calledNAME-2;deploy-probeuses the agent's private temporary file and removes its checkout as the account. - Managed hosting: the team's session ends as soon as the customer switches team sign-in off; a sign-in ticket without a proper one-time value is refused; a server that is no longer managed switches team sign-in off.
- Managed screens: Managed plans and Managed customers edit in their New dialog (no second card); the Work queue's Done is a plain dialog.
- Tests: Go
TestDeployRepoAddress,TestDeployOneAtATime,TestDeployEnvOneLine,TestManagedStaffSessionEndsand more cases; 3 agent checks; panel New card count. 2 new texts in 20 languages.
HF Cloud: VPS tools (phase 3) — backups, snapshots, resize, reinstall, rescue, firewall, usage billing, reverse DNS
- My VPS (and Backups and tools on Server Admin › Cloud › VPS list) gets tabs under a VPS's details (
ui/vpsTools.tsx): Backups (turn the daily backups extra on/off, Back up now, Download, Restore), Snapshots (within the plan's number; Revert, Delete), Resize (bigger plans only, with what changing now costs; the disk never shrinks, the VPS restarts), Reinstall (image, new password or SSH key, confirmed by typing the host name), Rescue (a rescue system with the VPS's disk attached, temporary root password, Leave rescue mode), Firewall (inbound rules: allow/block, protocol, port or range, source; everything else allow or block; applied at once), Usage (traffic this month against the allowance), Reverse DNS. Dangerous actions ask first. - Host side (
internal/cloud:ops.go,firewall.go,store.go; newDrivermethods with the fake extended):vps-resize,vps-reinstall,vps-rescue,vps-rescue-exit,vps-firewall,vps-throttle,vps-snapshot[-revert|-delete],vps-backup(libvirt push backup while running),vps-restore,backup-get(streamed download),backup-delete. Every card now uses the VPS's own libvirt filterhf-cloud-vps-<n>(the anti-spoofing filter plus IPv6 anti-spoofing and the firewall). Hosts installrclone. - Backups go to the panel's existing backup storage (the S3 storage or another destination; their settings travel only in the signed command's body), daily from the 5-minute job, kept the plan's days. Usage: over the allowance a plan either caps the speed or charges per GB at month end; resizes, backups switched on later and overage are billed through the existing billing (Stripe subscription items/invoice items, else panel invoices).
- Cloud settings (VPS list, administrators — the one home for): extras' prices (
cloud_price_backups,cloud_price_ipv4), HF Panel licence plan (official edition) or the sealed partner API key (paid edition), where VPS backups go, the rescue image, reverse DNS through the OVH API (keys sealed; behind anRDNSProviderinterface). Plans gain Snapshots, Backups kept (days), Over the traffic allowance, Price per GB over / Speed cap. - Tables
cloud_snapshots,cloud_backups; columns oncloud_plans,cloud_vps,cloud_jobs,cloud_traffic(also added byhf-api migrate).docs/CLOUD.md§12 with owner decisions; help Backups, snapshots and more and Cloud settings and VPS tools. - Tests: Go
TestHostOps,TestGoldenFilter,TestGoldenRescueDomain,TestGoldenVirshOps,TestGoldenRclone,TestFirewallCheck,TestCloudMoreLife,TestCloudMoreTick,TestCloudRDNS; paneltest/pages/vps-tools.test.tsx(12). 83 new texts in 20 languages.
HF Cloud: customer side — My VPS, ordering and billing
- Control Panel › Cloud › My VPS (
pages/my-vps.tsx,screen_cloud_my.go): the customer's VPSs (keyboard and right-click through@hf/shared/ui/rows) and one VPS in detail — state, IPv4/IPv6, plan, CPU, disk and traffic, Console (noVNC, a fresh token per connection), Power (Start, Restart, Shut down, Power off with a question), New root password (shown once), Open HF Panel for HF Panel images, billing with Cancel VPS / Keep this VPS, recent tasks; it reads itself again while a task runs. - Order a VPS (
pages/vps-order.tsx, public routevps-order): size, what to install (Ubuntu 24.04 + HF Panel, Ubuntu 24.04, Debian 13, AlmaLinux 9), location, host name, SSH key, extras (daily backups, one more IPv4; off until priced), the total, then the existing checkout (Stripe Checkout or PayPal). Guests give a username and email and get a VPS-only account (a customer with no hosting: only My VPS, Billing, Support and account screens). - Billing through the existing billing (
svc_cloud_order.go): plans become Stripe products/prices and PayPal plans, one subscription per VPS; the paid order makes the VPS (webhook or PayPal return), its invoices land in the customer's Billing, failed payments follow the dunning settings (suspend afterdun_suspend_days, close afterdun_term_days), a cancelled VPS is deleted at the end of its paid month. - HF Panel licences for "Ubuntu + HF Panel" VPSs are issued by themselves: from the licence server's own records on the official edition (plan
cloud, settingcloud_licence_plan), through the partner licence API on a paid edition (settingcloud_partner_key); given back when the VPS is deleted. - Host: new command
vps-password(root's password through the QEMU guest agent, hashed on the host); every VPS gets the guest agent (cloud-init) and its channel (domain XML). - nginx: the panel passes WebSocket upgrades for
/api/v1/cloud/console(hf_api_console_location; fresh installs and update.sh), checked bytests/check-installer.sh. - Tables
cloud_orders,cloud_customers,cloud_usage. Help: Control Panel › Cloud (My VPS, Order a VPS).docs/CLOUD.md§11 and owner decisions. - Tests: Go
TestCloudOrderPaid,TestCloudOrderGuest,TestCloudRootPassword,TestCloudPartnerLicence, cloud package (vps-password, guest agent); paneltest/pages/cloud-customer.test.tsx(5). 43 new texts in 20 languages.
HF Deploy: screens and the full product (phase 2)
- Control Panel › Deploy › Services (
pages/deploy.tsx,ui/deployNew.tsx,ui/deployService.tsx): tiles for a web service, static site, worker or cron job and database, each opening a step wizard (repository with Detect, which fetches the branch as the account and fills in the language and commands; build and start; a pasted.envand a linked database; size with its price, and region); the services table; a service's page with Deploys (the build log read live, roll back), Logs, Runs (cron), Environment, Domains, Connection (databases) and Settings (webhook, Delete after typing the name). Lists work with the keyboard and mouse (@hf/shared/ui/rows), loading showsHfLoader. - Server Admin › Hosting › HF Deploy (
pages/deploy-admin.tsx): apps domain, services per account, region, build memory, releases kept for roll back, idle minutes before a Free service sleeps, the sizes' monthly prices, every service with Delete. - Own domains per service (DNS checked, then served with their own certificate; pending ones rechecked every 10 minutes), Free services sleep when idle and wake on the next request (
GET /deploy/wakefrom the site's Nginx error page), cron jobs run on their schedule from the minute job with a run history and Run now, managed databases (PostgreSQL 17, MariaDB 11, Redis 7 in a volume, on the account's own network, linked asDATABASE_URL/REDIS_URL), builds in a memory-limited builder (docker buildx), image pruning (last N releases kept for roll back), Restart. - Billing: each size has a monthly price (all 0 until the owner sets them); a paid size becomes a Stripe product and price like a package, and each account's services of that size are a prorated item on its card subscription.
- Containers now run on the account's own network
hfd-<account>(was the sharedhf-deploybridge) so services reach their databases by name. - Agent: deploy-db, deploy-domain, deploy-undomain, deploy-idle, deploy-sleep, deploy-wake, deploy-cron, deploy-cron-status, deploy-prune, deploy-probe; deploy-build takes the builder's memory, deploy-route a sleep flag. Tables
deploy_domains,deploy_runs,deploy_links,deploy_sizes,deploy_billingand three columns ofdeploy_services(also added byhf-api migrate). - Tests: Go
TestDatabase,TestOwnDomains,TestDeployDatabaseLinked,TestDeployOwnDomain,TestDeploySleepWake,TestDeployCronRuns,TestDeployPrune,TestDeployBilling,TestDeployAdminAndDetect; paneltest/pages/deploy.test.tsx(11);tests/checks/deploy-agent.shextended. Help: Control Panel › Deploy and Server Admin › Hosting › HF Deploy. 100 new texts in 20 languages. Design:docs/DEPLOY.md§12–16.
HF Cloud: Server Admin screens (phase 2, administrator side)
- New Cloud menu group in Server Admin (licence feature
cloud): Hosts (cards with memory and disk given to VPSs, VPS count, free IPs, CPU now, online state; Add a host shows the one-time join command with Copy and its expiry, Join command again for a host still waiting; Edit with the reserves and vCPUs per thread, Refresh, Stop/Take new VPSs here, Remove with a question; a notice when a host that takes new VPSs has fewer than 3 free IPs), VPS list (totals, host and state filters, Console in the browser, Manage menu: Start, Restart, Shut down, Power off, Suspend/Unsuspend, Delete; a three-step create wizard: customer, plan, image, host or automatic, location, host name, root password or SSH key, HF Panel licence key; root's password shown once; the list reads itself again while a VPS is being created, stopped or deleted), VPS plans, IP pools (extra IPs with a virtual MAC, bridged network, routed IPs; addresses added as "IP MAC" lines, free ones removed) and Images — each with a New card and dialog (pages/cloud-*.tsx, shared partsui/cloudAdmin.tsx). - Keyboard and mouse on every Cloud list through
@hf/shared/ui/rows: ↑ ↓ Home End, Enter (edit, or the console), F2, Delete (asks first), letters jump, right-click / Shift+F10 / menu key for the row's actions. - API:
cloud-vpsalso answerscustomersandcurrencyto administrators andhf_panelper VPS;cloud-plansanswerscurrency(docs/CLOUD.md§9). - Help: Server Admin › Cloud, Set up a host and Sell VPSs (
frontend/docs/content/server-admin/cloud/). - Tests: panel
test/pages/cloud-admin.test.tsx(9), GoTestCloudVPSAdminData,TestCloudMenu. 98 new texts in 20 languages.
Website: no more redirect loop on the panel's own hosting
- The website's
.htaccesssent every request to https again when it was hosted on an HF Panel server, because Nginx ends the TLS and Apache only sees http (ERR_TOO_MANY_REDIRECTS). It now also trustsX-Forwarded-Proto: httpsfrom the proxy and theHTTPS=onthe server's Apache config sets (frontend/site/deploy/.htaccess).
Managed hosting (HQ and the customer's Server Admin)
- A paid service on top of a licence: HQ's team looks after a customer's HF Panel server. HQ › Managed starts it for a licence key and plan (
managed_servers); the licence check answer then carries"managed": <plan>and the server reports every 5 minutes (hf-api sample) to the new POST /licence/managed (also/v1/licence/managed), HMAC-signed with its licence key like the blocklist reports; HQ answers the state signed with the licence key pair. No new licence feature: managed status rides on the licence. - Customer side, Server Admin › Support › Managed care (
screen_managed.go,svc_managed.go; only while managed): plan and price, uptime (30 days), last backup and restore test, requests used of the allowance, "What we did", what the plan includes, the requests with the team's notes, "Ask us to do something" (dialog), and Team sign-in (off by default; the customer switches it on or off, each sign-in logged inmanaged_signins, the audit log and the notifications centre). The customer keeps full access. - HQ, new Managed group (
screen_managed_hq.go,svc_managed_hq.go;/billing/hq/managed-customers,work-queue,update-rounds,monthly-reports,managed-plans): Customers (health, plan, requests used, Add / Edit, Open panel: a one-time ticket signed with the licence key, 2 minutes, only with the customer's consent, logged inmanaged_access), Work queue (alerts raised and cleared by themselves — disk nearly full, backup missed, server down from the reports and an uptime monitor on each panel address, update round stopped — and customer requests, each with an assignee and due time from the plan's reply hours; Take, Done with a note for the customer, Log work; HQ support staff may act here), Update rounds (system or HF Panel updates on chosen servers, one at a time; each server checks its websites before and after and a broken one stops the round), Monthly reports (emailed to the licence address on the 1st, send again), Managed plans (prices start at 0 = not set, for the owner to fill in). New events in the notifications centre: managed.alert, managed.request, managed.signin. - Tables in schema.sql (also made on first use). Tests: Go
TestManaged*(plans, signed reports, alerts raised and cleared, requests and allowance, rounds stopping on a broken site, monthly report, Open panel consent, one-time and changed tickets), paneltest/pages/managed.test.tsx. 117 new texts in 20 languages.
HF Cloud: foundations (phase 1, no screens yet)
- New package
backend/api/internal/cloud: the KVM host side, run ashf-api cloud-host join|serve(servicehf-cloud-host, userhf-cloudin the libvirt and kvm groups). It answers the panel's signed commands on/node/agent(the same protocol as a joined HF Panel server: host-info, vps-list, vps-create, vps-start/shutdown/poweroff/reboot, vps-delete, vps-console) and opens VPS consoles on/console(one-time token, raw VNC stream). libvirt throughvirsh(domain XML on stdin), disks throughqemu-img, the cloud-init seed throughgenisoimage, behind theDriverinterface (Virsh, andFakefor tests). Images are downloaded once per checksum and checked against the distribution's list; root's password is hashed on the host and never kept. cloud-host.sh(served at/cloud-host.sh, also/v1/cloud-host.sh): installs qemu-kvm and libvirt, makes the bridge br0 (the old network comes back by itself after 3 minutes if the new one fails), the anti-spoofing filter, then downloads the host program from the panel (SHA-256 checked) and joins with the one-time code from the Hosts screen.- Panel (
svc_cloud.go,svc_cloud_jobs.go,svc_cloud_join.go,svc_cloud_console.go,screen_cloud.go,cli_cloud.go): tablescloud_hosts,cloud_plans,cloud_pools,cloud_ips,cloud_images,cloud_vps,cloud_jobs,cloud_traffic; placement on the host with the most memory left; IP pools for Additional IPs with their virtual MACs, bridged networks (IPv6 per VPS out of the /64) and routed addresses; tasks one at a time per VPS (hf-api cloud <job>), the 5-minute job reads every host (states, traffic); screen API for Hosts, VPS list, VPS plans, IP pools, Images and My VPS with ownership checks (customer own, reseller their customers', administrator all); the console route/api/v1/cloud/consolewired toConsoleProxy(ConsoleDialeradded there so the stream goes through the VPS's host). - Licence feature
cloud(Business plan; catalogue price 5, owner to confirm). Design, protocol, API and the list of phase 2/3 work:docs/CLOUD.md. - Tests:
internal/cloud(domain XML and cloud-init golden files, spec checks, virsh commands, domstats, image checksums, signed-command checks, create/delete on the fake, console tunnel, join codes), panelTestCloud*(join over HTTPS with the pinned key, pools, plan, image, create, placement, power, the 5-minute job, console through the host, suspend, delete with addresses back, ownership, refusals). 24 new texts in 20 languages.
HF Deploy: foundations (phase 1, no screens yet)
- New engine
backend/api/internal/deploy: service kinds (web, static, worker, cron, database), sizes (Free, Starter, Standard, Pro), language detection, a Dockerfile per runtime (Node.js, Python, PHP, Go, static, or the repository's own), GitHub webhook signatures and events, and the deploy itself: build, start next to the live version, health check, switch traffic, stop the old one. A failed build or health check keeps the old version live; a roll back starts an earlier image again. Git, Docker and the proxy are interfaces;deploy/deploytesthas fakes. - Panel (
svc_deploy.go,screen_deploy.go,screen_deploy_admin.go): tablesdeploy_services,deploy_releases,deploy_env; signed webhookPOST /deploy/webhook?s=<id>(also/v1/webhooks/deployon api.<domain>); the jobhf-api deploy <release>(agentjob-deploy); pull request previews; environment variables, webhook secrets and repository tokens sealed with the server key; customer and administrator screen API with ownership checks. Settings:deploy_domain,deploy_max_services(Server Admin › HF Deploy). - Agent
agent/lib/79-deploy.sh: deploy-fetch (as the account), deploy-build, deploy-start (no capabilities, limits, own network, 127.0.0.1 only, only the service's own images), deploy-check, deploy-stop, deploy-route (Nginx site, wildcard or per-name certificate), deploy-logs, deploy-remove; suspend, unsuspend and account delete include deploy containers. - Licence feature
deploy(in the Business plan, which has every feature). Design and later phases:docs/DEPLOY.md. - Tests:
internal/deploy(switch, failed build keeps old, roll back, workers, detection, Dockerfiles, webhooks), panelTestDeploy*(create, push → live, failed build, roll back, previews, refused webhooks, sealed variables, ownership, field checks),tests/checks/deploy-agent.sh. 19 new texts in 20 languages.
HF Cloud: browser VPS console (building block)
frontend/shared/ui/VpsConsole.tsx: the VM's screen over VNC in the browser (noVNC @novnc/novnc 1.7.0, pinned, loaded only when a console opens): fit to window or actual size, full screen, Ctrl+Alt+Del, Send text (typed into the VM key by key), Reconnect, and connecting / disconnected / VPS off / error states with the HF loader. Not on a screen yet; HF Cloud wires it in.backend/api/internal/panel/svc_console.go:ConsoleProxypipes the console WebSocket to the VM's VNC server (golang.org/x/net/websocket, as the web terminal) after checking a signed console token (one use, 60 seconds, one VPS and one user); where the VNC server listens comes from aConsoleLookup(HF Cloud). Close codes 4001 / 4002 / 4003 tell the page refused token / VPS off / unreachable.- Tests: Go
TestConsoleTokenChecks,TestConsolePipe,TestConsoleRefusals; paneltest/vpsConsole.test.tsx. noVNC added to THIRD-PARTY-NOTICES.txt; 18 new texts in 20 languages.
Website: HF Cloud, HF Deploy and Managed hosting pages
- New pages /cloud/ (KVM VPS sizes 1/2/4/8, London and Gravelines, daily backups and extra IPv4 add-ons, HF Panel pre-installed if wanted), /deploy/ (push to Git; web services, static sites, workers, cron jobs, databases; Free / Starter / Standard / Pro) and /managed/ (Managed and Managed Plus). All "Coming soon" with a waiting-list button; sizes, add-ons and prices live in
site.json"products" (a price leftnullshows "Coming soon"). - Home page: a product row linking HF Panel, HF Cloud, HF Deploy and Managed. Top menu gains Cloud, Deploy and Managed ("Product" is now "Panel"); the menu folds into the menu button below 1180 px so it never overflows.
HF Server's installers folder renamed
build.shnow puts the HF Server installers in releases/hf-server/ (was releases/hf-server-pc/). An old hf-server-pc/ folder is renamed on the next build, so a--server-onlybuild still finds the last desktop build. README, the build guide, DOCUMENTATION.md and deploy-test follow.
HF Server: the whole window by keyboard and mouse
- Everywhere: Ctrl/⌘+1…9 (or Alt+1…9) switch pages in side-bar order; Ctrl/⌘+K or Ctrl/⌘+F the page's search or find; F5 / Ctrl+R read the page's data again (not the window); "?" or F1 list the current page's shortcuts; Esc closes menus and dialogs and clears the selection. Pages register their refresh, find and shortcuts with
useViewKeys(src/main/ctx.tsx). - Lists (Sites, Databases, Tunnel, Mail, Docker containers / images / compose / apps, Ports, Components, PHP extensions, the Control panel's servers): ↑ ↓ Home End PgUp PgDn, type a name to jump, Enter or double-click the main action (open the site, Browse, open the link or Share, read the mail, start/stop the container, install/update, extension on/off, start/stop the server), Delete with the button's own question, right-click / Shift+F10 / menu key for the row's actions. Sites, Mail and Docker containers and images select several (click, Ctrl/⌘-click, Shift-click, Shift+arrows, Space, Ctrl/⌘+A, Esc): delete sites, remove or start/stop/restart containers, remove images and delete mails in one go, after one question.
- Control panel: Space / Enter starts or stops a server; its menu has Start, Stop, Restart, the admin page, every config file and log.
- Logs: ← → switch logs, Ctrl+F the filter, a tab's right-click menu (Show, Open file, Empty log). Mail: j / k move and open, Delete removes the selected or open mail (
IDsin one request:mailDeleteinmail.go, every id checked first), Ctrl+F finds in the open email with every match marked. - PostgreSQL Manager: the Browse grid works like a spreadsheet (
src/pg/grid.ts): arrows, Tab, Home / End, PgUp / PgDn, Shift extends a block, Ctrl+A, Ctrl+C copies it as tab-separated text, Enter / F2 / double-click edits a cell in place (Enter or Ctrl+S saves only a changed value, Esc cancels), Delete deletes the block's rows after one question, right-click Copy / Edit cell / Edit row / Copy as new row / Delete row / Refresh, F5; Ctrl+Enter runs the SQL from anywhere on the SQL page; "?" lists the keys. - One home for the list helper:
desktop/ui/rows.tsxmoved tofrontend/shared/ui/rows.tsx(now with selection, PgUp/PgDn, type-to-jump and anonswitch for pages kept mounted); HF Connect's lists import it from there.lib/keys.tsalso stays quiet while a native<dialog>is open. - Help: new page HF Server › Keyboard and mouse;
frontend/os/README.mdhas the shortcut list. Tests:test/keyboard.test.tsx(the window on mock data),test/pgGrid.test.tsx, GoTestMailDeleteSeveral.
HF Connect: the terminal, the lists and the last Files gaps by keyboard and mouse
- Terminal like a desktop terminal (rules in
desktop/connect/frontend/src/term.ts): Ctrl+Shift+C / Ctrl+Insert copy, Ctrl+Shift+V / Shift+Insert paste, middle-click pastes the text last selected on Linux (⌘C / ⌘V on a Mac); pasted text can't leave a bracketed paste; text with a line break asks first with a preview (can be turned off); Copy on select; Ctrl+Shift+F find bar (Enter / Shift+Enter, Escape); Ctrl+= / - / 0 font size, saved interminal.json(prefs.go); Ctrl+Shift+T new tab to the same host in the shell's folder without asking again (TerminalAgain), Ctrl+Shift+W close, Ctrl+Tab / Ctrl+Shift+Tab / Ctrl+PageDown / PageUp switch, Ctrl+1…9 jump; tabs reorder by drag, close on middle-click, "+" opens one; Ctrl/⌘-click opens a link; right-click menu (Copy, Paste, Select all, Clear, Find…, Open files here, Close tab, the two settings, Keyboard shortcuts); F1 or Ctrl+Shift+/ lists the shortcuts ("?" is typed). Every other key goes to the shell untouched. xterm.js addons@xterm/addon-search0.15.0 and@xterm/addon-web-links0.11.0 (pinned). The browser window asks before closing with terminals open. - Lists (Hosts, Keys, Known hosts, Port forwarding;
desktop/ui/rows.tsx): ↑ ↓ Home End, Enter or double-click, F2 (edit a host, rename a key), Delete (the same question as the button; stops a forwarding), right-click / Shift+F10 menus; Ctrl/⌘+N new host, Ctrl/⌘+F the search, "?" / F1 the shortcuts. - Files: cut and paste on the other side now moves: the queue deletes the originals once every file has arrived, then the folders left empty, and keeps them all (saying why) if one fails or is cancelled; a folder holding links stays with them inside (
UploadCut,DownloadCut,cut.go). A move to another drive or filesystem copies, then deletes (EXDEV / ERROR_NOT_SAME_DEVICE). A copy on the server runs in the queue with its progress, pause and cancel. Files dropped into the browser window (macOS,--browser) upload or copy too: sent in 1 MB pieces to a temporary folder, removed when done (drop.go; folders: the Wails window only). Fixed: downloading a folder over SFTP queued it as one file (Sizenow refuses folders). - Tests:
term.test.ts,panes.test.ts(cut moves, base64 pieces); GoTestCutUpload,TestCutDownload,TestLocalMoveOtherDrive,TestDropUpload,TestTerminalPrefs,TestRemoteCopy(queue progress). Help: Terminals, Files and Saved hosts pages;desktop/README.md.
Launch pricing (licence catalogue defaults)
- New default plans, all at fixed monthly prices: Free 0 (Server Admin and Control Panel, up to 5 accounts), Starter 7 (+ Email, DNS, Files, DB Studio, WordPress, Backup storage, up to 30), Pro 15 (+ Reseller Panel, Billing, Domain sales, Apps, Docker apps, Tunnels, up to 100; most popular), Business 29 (every feature, unlimited). Pro Web is no longer a default plan; licences on it keep their features (
DefaultLicCatalog,site.json). - Account tiers: a new Starter tier (30 accounts, ×1.5); Pro allows 100 accounts instead of 50. Solo, Pro and Unlimited keep their multipliers, so custom bundles cost what they did. A catalogue saved in Plans & pricing is never changed.
- Bulk licences: 5+ −15 %, 25+ −30 %, 100+ −40 % (
LicBulk); the licence shop's line is made from the same list. - Help: the default plans (Plans & pricing), a founders' code example (Discount codes), bulk discounts (Partner programme);
docs/PRODUCTION.mdhas the steps for a licence server that already saved its prices. - Tests: price fixtures regenerated with a Starter-tier case; the licence tests expect the new tiers, plans and bulk steps.
Loading: the HF mark everywhere (panel and the shared loader)
- One loader for every app: the HF mark in its rounded square with a brand-coloured ring turning around it, an optional label and progress line ("42% · Unpacking…"); green and stopped when done, red when failed, still while waiting; slower for people who ask for less motion; light and dark.
frontend/shared/ui/HfLoader.tsx+frontend/shared/styles/loader.css(the mark is the sharedHfMark). - Server Admin, Reseller Panel, Control Panel: the first load shows it centred instead of a blank window; a screen change that takes over 400 ms shows it in the middle of the window, the page dimmed under it; the live job dialog's head, its corner card and the waiting jobs' cards use the small mark (waiting ones stand still). Buttons keep their own small spinners. Tests:
panel/test/hfLoader.test.tsx. - Mail, Files, DB Studio, HF Server's window and HF Connect show it centred in the view that is loading, in place of a blank page or a plain "Loading…": every Mail page until its data is here (the mailbox, a message, Compose, Contacts, Calendar, Settings…; nothing after a failed load, the notice says why); Files' first folder read and the editor, and over the folder left while another one opens; DB Studio's databases, a database, a table's rows and structure, a collection's documents and indexes; HF Server's Sites, Databases, Mail, Docker, Ports, Logs, Tunnel and the PostgreSQL window; HF Connect's file panes and the SFTP panel. Mail's "Loading…" is translated into all 20 languages. Tests:
loader.test.tsxin mail, files, dbs and os,desktop/connect/frontend/src/loader.test.ts. - The thin loading bar at the top of the window is gone in the panels, Mail, Files and DB Studio; a request in progress shows the busy cursor, a whole screen loading shows the HF mark. The view loaders' spacing rule lives once in
loader.css(.view-loader).
Card spacing, third pass (Server Admin, Reseller Panel, Control Panel)
- A short card beside a tall one keeps its own height instead of an empty bottom half (Settings' DNS & network, White label's Logo, Account isolation, Server health, Status page, Support tickets' Saved replies, Password & security, Site check, Images, Getting started and many more):
.grid2,.health-gridand.gs-stepsalign their cards to the top; the odd last card of a.grid2takes the whole row (Statistics' Response codes). - Cards in a grid or a stack were 32px apart (the grid's gap plus the stacked-card margin: Tax, Quotes & credit, Domain sales, Help's categories) and the Overview's Live card was shorter than the Traffic card beside it; they now keep the 16px gap. The Overview's stretched chart cards centre their chart instead of leaving the space at the bottom.
- Card heads: 16px to the content (phone 12px) in form cards too (they had 30px: Stripe, PayPal, Weekly summary, Mail health, Backups, Hotlink, Site firewall…); a plain title, a note or a list in a form card no longer adds its own margin to the form's gap; Business' charts use the normal head; a card that is only a head (Other sign-ins, DNSSEC) has no empty band under it; a title over a table no longer touches it.
- Notes, website tabs and notices are no longer stuck to the card above them; a notice at the end of a card leaves no extra space.
- 16px everywhere between New cards, the Overview's key figures, the live chat's columns, Getting started's steps, DNS tools' templates and DB Studio's databases (some were 12 or 14px); DNS tools' template tiles start their text at the top.
- Phones: WordPress' Update buttons stay inside their card (the page was 425px wide); scrolling tables (FTP accounts, Packages, Uptime…) keep words whole and scroll instead of breaking names letter by letter. One rule per case in
app.css,mobile.css,ui.css,chat.css(card-gaps.test.ts).
Card spacing, second pass
- A form card stretched to the height of the card beside it (Billing › Selling, Stripe) no longer spreads its fields down the card; they stay together at the top (
.formin app.css). - Plans & pricing: the Account tiers and Bundle discounts tables' inputs fit their columns, so the Multiplier box no longer runs under the discounts table (
.t-inputs). - Live chat: with no chat open the conversation card takes the whole width instead of leaving an empty third column.
HQ: spacing, staff roles and wrong figures
- Cards keep the panel's 16px gap everywhere: a lone card followed by a row of cards or key figures touched it (Licence server's Revenue, User analytics' "Seen in the last 24 hours"); the chat's second card sat 16px lower than the first; an odd last card in a two-column row (User analytics' Systems) now fills the row. One shared rule in
app.cssandchat.css(card-gaps.test.ts). - HQ staff roles are enforced: a View only or Support member could still change licences, prices and settings (the guard's quick path handed the request on and the full path never checked the role), and could open the pages read-only staff can't (the server's config files, DB Studio, phpMyAdmin, Log in as).
hqRoleGuardinsvc_hq_more.go, used by the screens and the server-made pages (TestHQRolesCantOpenActingPages). - HQ's home (the brand link, "/", every return to the overview) is the Licence server, not Server Admin's overview (
TestHQHomeIsLicenceServer). - A licence waiting for its first payment shows as Pending with no date, not "Expired · 8 Jan 1970"; its server is still told the licence has ended (
TestHQPendingLicenceShown). - User analytics: the funnel's Paid step counts only people who came through the steps before it (renewals showed as "400% of the step before") (
TestHQFunnelPaidFollowsTheFunnel). - Licence keys: the bulk resellers' table scrolls inside its card on a phone instead of widening the page (
licences.test.tsx).
HF Connect: keyboard and mouse in Files
- Both panes of HF Connect's Files work like the web Files app, with the same shared pieces (
@hf/sharedkeys, selection, right-click menu, shortcuts list): click, Ctrl/⌘-click and Shift-click select; ↑ ↓ Home End PgUp PgDn (Shift extends, Ctrl/⌘ moves without selecting), Space, Ctrl/⌘+A, Escape, type a name to jump; Enter or double-click opens a folder (a file goes to the other side, as before); Backspace / Alt+↑ up a level; F2 rename; Delete (asks first); F5 refresh; Ctrl/⌘+Shift+N new folder; Ctrl/⌘+C / X / V; right-click or Shift+F10 for every action; "?" lists the shortcuts. The keys work on the pane last used. - Paste or drag onto the other side uploads or downloads through the queue (after a cut the originals stay: the queue finishes later); on the same side rows dragged onto a folder move there (Ctrl or Alt copies), and a copy pasted into its own folder is named
copy-of-…. Files dragged in from the computer upload to the server pane or copy into this computer's folder (Wails window; the macOS browser window has no file paths, so it ignores them). - Go:
LocalCopy,LocalMoveandRemoteCopy(server side, over a second connection), never over an existing name or into itself (files.go,files_test.go); the Wails window turns on file drops. Rules infrontend/src/panes.ts(panes.test.ts).
HQ addresses without "superadmin"
- HQ's screens now have plain addresses under
/billing/hq/: Licence server/billing/hq/licence-server, Customers/billing/hq/customers, User analytics/billing/hq/user-analytics, Announcements/billing/hq/announcements, Partners/billing/hq/partners, Licensed servers/billing/hq/servers, Installs/billing/hq/installs, Downloads/billing/hq/downloads, Releases/billing/hq/releases, Abuse watch/billing/hq/abuse-watch, Licence server status/billing/hq/status, Payments/billing/hq/payments, HQ staff/billing/hq/staff, Activity log/billing/hq/activity, Trash/billing/hq/trash. The oldsuperadminaddresses still open them and are shown as the new ones.routeSlugsinroutes_paths.go(sent to the interface asBoot.slugs,lib/router.ts).
HQ › Licence keys: blank page fixed
- Opening Licence keys from the HQ menu showed a blank page: a licence with no plan change waiting came to the page as
[], whose.atis the array method, so the page tried to show a "Changes on" date it couldn't read and broke. The server now sends null for it, and the page checks the values (screen_licences_plans.go,licences.tsx). Tests:TestLicencesRowPending,licences.test.tsx. - A screen that breaks while drawing now shows "Something went wrong — please try again." with a Try again button in its place, instead of blanking the whole panel (
ui/PageBoundary.tsx).
Composer: say why Run is off
- Composer: with no composer.json in the folder, Run stays off and a note under it now says why (type the folder that has it and Save folder, or upload the project with Files first). Translated into all 20 languages.
- Billing: the PayPal step no longer shows the old webhook address (
/billing/paypal-webhook); PayPal is set up with the API address only. The old address is still accepted. - Every panel button that can't be used yet now looks it (dimmed, "not allowed" cursor); before, a disabled button looked the same as a working one.
Jobs one after another
- Installs and other jobs followed live now queue: start several (PHP 8.1, 8.2, Nginx…) and each waits its turn as a small "Waiting" card above the running one, then starts by itself (two installs at once would fight over the package manager). × takes a waiting job out of the queue. Before, a second job replaced the first one's card and left its button spinning.
PanelApp.tsx(queue),LiveQueueinui/Dialogs.tsx; tests inapp.test.tsx.
Log in as a reseller
- Server Admin › Accounts › Resellers: Log in on a reseller's row opens their Reseller Panel as them, like Log in on Accounts. From there Log in on one of their customers keeps the administrator as the one to return to; Return to admin lands on Resellers. One rule for who may log in as whom (
loginAsAllowed), used by the guard, the session check and the route. Tests:screen_login_as_test.go,resellers.test.tsx.
Contact and videos
- The publisher's contact address is now
contact@the company domain (website, licence text, HF Connect, Linux packages); the official edition's company mail addscontact@as a forward toadmin@. - The source keeps no build output:
build.shbuilds the interfaces fresh (Node.js 20.19+ is needed for the desktop half too); the Java and TypeScript SDKs are built withjavac/npm run buildas their READMEs say. - Website: a Videos link in the footer and a Video guides card on the Community page, from
links.youtubeinsite.json(left out = no links).
Fixes
- API tokens: a path with an empty segment (
/accounts/bob//email/…,/accounts//dns/suspend) is now checked the way it is routed, so a plugin token's permissions and the licence's API modules can no longer be passed by doubling a slash (tokenAPIRel).
High availability part 2: failover
- Promote now on a standby pair (Server Admin › Accounts › Standby & failover, its one home): type the standby's name; two-step sign-in needed; audited. The standby stops copying, ends MariaDB replication and becomes writable, rebuilds every account from the panel data and per-account settings packs (Move accounts'
move-pack/move-unpack, settings only), starts web, mail and MariaDB, and becomes the DNS master: every record that named the old primary's IPv4/IPv6 (A, AAAA, SPFip4:/ip6:, any record holding the address) names the standby's, zones published to the DNS cluster. The main panel's own standby takes the panel over (the latest panel copy becomes its database). Administrators get a notification and email; eventha.failoverfor webhooks and script hooks. - Short TTL: while a pair is active, zones are published with at most 5 minutes per record; the records' own TTL comes back when the last pair is removed.
- Fencing: servers of a pair ask each other
ha-roleover a link signed with the pair's own secret; a primary that comes back and lost keeps web, mail and MariaDB stopped (masked) and shows "This server was replaced by …" with Make it the new standby. - Witness and automatic mode: per pair manual (default) or automatic, "fail after" (default 180 s) and a witness (a DNS cluster secondary first). Checks every 20 s from the standby's side (the node link's HTTPS, 80, 443, 25, 993, MariaDB); automatic promotion only when the witness can't reach the primary either; manual promotion against the witness shows a warning. Alerts: standby falling behind, primary unreachable, standby took over.
- Failback: the old primary is copied to again as the new standby (the pair reversed), then a planned switch-over in a maintenance window (the primary stops taking changes, the standby waits for its last MariaDB position, then takes over).
- Agent:
standby-promote,standby-demote,standby-panel-get,ha-role,ha-probe(agent/lib/93-standby.sh);move-pack … meta,move-unpack … standby. Tests:svc_standby_failover_test.go,tests/checks/standby-agent.sh,standby.test.tsx. Real-server test:docs/PRODUCTION.md(three VPS).
The panels on phones, and as an app
- Phones and small tablets (Server Admin, Reseller Panel, Control Panel; desktop unchanged): the sidebar is a drawer (menu button in the top bar; the current page highlighted; Escape, a tap beside it, a swipe or opening a page closes it; Tab stays inside), a compact top bar with the brand, tighter page padding, New cards full width, one-column forms, touch targets of at least 44 px and 16 px text in fields (no zooming in). Accounts, Domains, Email, Databases, Backups and Support show each row as a card (
TableBox/Table layout="cards"inui/kit.tsx); other tables scroll inside their card with the first column kept in view; row menus open as a sheet from the bottom; every dialog and step wizard is a full-screen sheet whose Save / Next / Finish row stays at the bottom. Styles infrontend/panel/src/styles/mobile.css, the drawer inshell/drawer.ts. - Install as an app: each panel's manifest is named after the brand and the panel (white label included), starts on the panel's home, uses
#b0573c(or the white-label colour) for its bar, and has the HF mark at 192 and 512 px plus maskable icons (/app-icon-maskable-*.png). Install the app appears in your name's menu while the browser offers it. iOS:apple-mobile-web-app-status-bar-style. The service worker stores only the built interface and the panel's style files (its cache is named after the version and build and replaced on update) and never pages, the API or anything signed in; offline, it shows the offline page. - Notify me on this device (Account › Notifications, its one home; the Phone app card left Password & security): Web Push with the server's own VAPID key, now kept sealed with the server key (
vapid_private), no third-party service. Support replies, websites down and, for administrators, server alerts (service down, disk, servers not answering) wake the device.
DB Studio works like a desktop database tool
- Grid (rows, SQL results, MongoDB documents): click,
Shift-click, drag andShift+arrows select cells,Ctrl/⌘-click toggles rows; arrows,Home/End,PgUp/PgDnmove;Enter,F2or double-click edits a cell in place (saved withrow/save),Esccancels,Tab/Shift+Tabsave and edit the next cell;Ctrl+Ccopies TSV (whole rows with a header line);Deletedeletes the selected rows after a confirmation (Set NULL is in the menu); right-click menu: Copy, Copy as SQL INSERT, Edit, Set NULL, Delete row, Filter by this value; column resizing; grid roles for screen readers; Left/Right follow the reading direction. - SQL box:
Ctrl+Enterruns the selection or the statement under the cursor (error lines still count from the top),Ctrl+Shift+Entereverything,Ctrl+/comments lines,Tab/Shift+Tabindent (undo keeps working;EscthenTableaves the box). - Moving around: the mouse's back and forward buttons step through the tables and queries opened before;
?and a keyboard button show the shortcuts (frontend/sharedShortcutHelp). Fixed: the shared right-click menu now takes the keyboard focus when it opens (it was still hidden for measuring).
HF Connect for macOS, built with the rest
- HF Connect for Mac is built on the same machine and in the same step as Windows and Linux, like HF Server:
HF-Connect-<v>-macOS.zip, one universal app (Apple Silicon and Intel) with an Applications shortcut. It is pure Go: the window opens in your browser (a window without an address bar when Chrome, Edge, Brave, Chromium or Vivaldi is installed, else a tab of your default browser), opening the app again brings it back, and closing the window quits it. File dialogs and the clipboard are macOS's own; passwords and keys stay in the Keychain. - Safe on your computer: the window listens only on
127.0.0.1, opens with a one-time link that becomes a key valid for that run only, and refuses other addresses and other websites. - Linux without WebKitGTK:
hf-connect --browseropens the same browser window. - Build:
desktop/build.shmakes the macOS zip anywhere (packaging/macos/lipo.py,packaging/macos/hfconnect-Info.plist; signed byHF_MAC_SIGN_CMDorpackaging/macos/sign.shwhen set up);build.shstep "HF Connect (tests, Windows · macOS · Linux)" now requires it, and the Software page offers it.
Files works like a desktop file manager
- Mouse: click selects,
Ctrl/⌘-click adds,Shift-click selects a range, double-click opens (a tap on touch screens); a right-click menu (also the row's ⋯ button and a long press on touch screens) with every action, and the folder's own menu (New folder, New file, Upload, Paste, Select all…) on empty space; the middle button opens a folder in a new tab. - Keyboard: arrows (with
Shift/Ctrl),Home/End/PgUp/PgDn, type-ahead,Enter,Backspace,F2,Delete,Ctrl+A/C/X/V,Ctrl+Shift+N,Ctrl+U,Ctrl+F,F5,Esc, and a Keyboard shortcuts list (?); the editor'sEscgoes back to the folder. Arrows follow the reading direction on right-to-left pages. - Drag and drop: folders from the computer upload with their whole tree (onto the list or onto a folder); entries dragged onto a folder, Up one level or the path move there (
Ctrl/Altcopies), never a folder into itself; a file dragged to the desktop is saved there (Chrome, Edge). - API:
files/uploadtakes file names with folders and makes them;files/copyalso copies several entries into another folder (items,dest);files/moveandfiles/copyrefuse a folder into itself. - Shared (
frontend/shared): combination shortcuts inlib/keys.ts(Mod+c,Shift+Delete…),lib/selection.ts, the right-click menuui/ContextMenu.tsx(with long press) and the shortcuts dialogui/ShortcutHelp.tsx, used by Files and DB Studio.
Build
- Step lines of
build.shand the installers never wrap: a long step name is cut with "…" to the terminal width, so a narrow terminal no longer shows a step twice. - The offline Windows setup is skipped with a note when no component file could be downloaded, instead of stopping the build in makensis.
- Panels: a page opened from its address and the same page opened from the menu now show the same thing (an empty map in the page's boot data was
[], notnull: Backups told every customer an export was running); the chat button and the sidebar's current entry meet the WCAG contrast rule (--brand-softis #fdf4f0); dates and times from the browser in English are written the way the server writes them (dateLang). - HF Connect builds with a newer Go (1.27):
build.shbuilds the Wails CLI with a current golang.org/x/tools, and rebuilds an installed CLI that can't read the newer Go's packages ("package context without types"). - The build no longer leaves an
hfserverfolder in the build user's home: the offline-cache run of HF Server uses its own folder in the build's work area (HOSTFOG_ROOT). - The plugin sandbox check passes on Ubuntu with systemd: a dynamic user without a name (no nss-systemd) is recognised by its uid range.
- Agent checks run as a normal user too: the WordPress network check writes its live logs in its own folder (
APP_LOGS, default /var/log/hostfog-apps), and the limits check no longer needs a MySQL client on the build machine. build.shalso installs the Wails CLI and, on Linux, the WebKitGTK headers HF Connect needs, like the other missing tools (--no-installto only list them).- HF Connect ships for the same systems as HF Server, and Windows on ARM: one Windows setup for x64 and ARM64 (it installs the program that fits the computer), portable zips for both, Linux
.deband.tar.gzfor amd64 and arm64 (cross-built on Debian/Ubuntu against an arm64 sysroot,scripts/build/arm64-sysroot.sh;build.shinstallsgcc-aarch64-linux-gnu; on other systems arm64 is skipped with a note), andHF-Connect-<v>-macOS.zip(Apple Silicon and Intel) built on a Mac withbash desktop/build.sh connect darwin; a macOS zip put inreleases/hf-connect/is kept and published. The Software page and the install help list every file per system. The Wails CLI now runs without a display during the build, so making the bindings no longer opens a browser tab under WSLg.
Leave any time: export accounts as standard full-account backups
- Export for another host: any account can be written as a standard full-account backup that most hosting panels can import (
cpmove-USER.tar.gz, the layout Import accounts reads): websites (main inpublic_html, others in a folder named after them), MySQL dumps with their users, password hashes and grants, Maildir mail with passwords and sizes, forwarders, DNS zones, cron jobs (paths moved), SSL certificates and the account's details. Made in the background into the account's backups folder, one export at a time per server. From each account's Manage menu and for the ticked accounts (Server Admin › Accounts), and by customers in Backups › Download all your data. Import accounts now keeps a database user named like its database under that database's new name. - Help and website: new page Server Admin › Accounts › Export your accounts; a "No lock-in" note on the Why page.
Website: Security, System status, Changelog and Roadmap pages
- /security/: how HF Panel is built to be safe (the root helper with checked arguments, file work as the account, two-step sign-in and passkeys, Content-Security-Policy, signed releases and components, account isolation), what the October 2026 review and the November 2026 free-tool scan found and fixed, that an independent audit is planned, and how to report a vulnerability (disclosure policy and
security.txt). - /status/: the live status the publisher's own HF Panel server publishes (
/status.json, with a link to its full status page), and a check from the visitor's browser that the website, licence server, downloads and help site answer. The menu's and footer's System status lead here. - /changelog/: this file, read when the site is built (
src/changelog.ts), newest first, raw HTML shown as text, tests and server-agent bullets left out. - /roadmap/: Now / Next / Later for customers, with no dates.
- Footer: Changelog and Roadmap under Product, Security under Company; all four pages in the sitemap.
Free plan, 30-day trial, money-back guarantee
- Free plan: the Server Admin and Control Panel for one server with up to 5 hosting accounts, no add-on modules, at no charge and without an end date; first on the pricing page (Get it free). The order page sends a signed link (24 hours) instead of a checkout; opening it makes a licence with status
free(one per server IP and per email). From the account it is made at once. A paying licence can't move to it through its subscription. - Business is a fixed £69 a month with high availability included; Starter and Pro Web unchanged (
DefaultLicCatalog,site.json). - Trial: 30 days (was 14): the licence server's default (
trial_days), the website, the installer and the help. - Terms: a 30-day money-back guarantee on the first payment of a paid licence (refunded by hand through Stripe or PayPal; marked for lawyer review); the pricing FAQ says so.
Performance insights: what makes a website slow
- Control Panel › Websites › Speed test › Performance insights (a tab of Speed test, no other home): per website, the last 7 days of requests that waited for PHP — median, 95% and slowest time, how many took 1 second or more — the 20 slowest pages, the slow PHP requests (still running after 3 s) by function, file and WordPress plugin or theme, and suggestions with a link to the screen that helps: switch on the page cache, a PHP version older than 8.2, OPcache when slow requests were loading files, a plugin or theme in a third or more of the slow PHP requests, the account's slowest MySQL query (Database tools).
- Server Admin › Accounts › Top accounts › Performance insights (resellers: their customers): the websites with the most slow requests and slow PHP, and the plugins and themes in the most slow PHP requests across the server; Open (or Performance insights next to an account on Top accounts) shows one account as its owner sees it. Administrators are also told when the MariaDB slow query log is off.
- Where the figures come from: every website's access log uses the Nginx format
hf_timed("combined" plusrt=… urt="…";/etc/nginx/conf.d/hostfog-timed.conf, written before any site config names it; other log readers are unaffected); each account's PHP-FPM pool writes~/logs/php-slow.log(request_slowlog_timeout = 3s, root-written, account-readable).hf-api visitor-stats(hourly) reads the access log once for both the visitor statistics and the response times, then each account's slow log as the account (phpslow-read, at most 64 MB per run), following log rotation. Slow database queries stay in Database tools; only an account's own databases and database users count. Figures kept 7 days (perf_url_hour,perf_php_hour,perf_php_log); at most 50 pages per website and hour. - Logs:
/etc/logrotate.d/hostfog-sitesalso rotates a website log when it passes 256 MB (maxsize, added on update). Existing websites get the new log format when the update writes their configurations again. - Help: new pages Control Panel › Websites › Performance insights and Server Admin › Accounts › Performance insights; Speed test and Reseller Panel › Top accounts link to them.
OpenLiteSpeed as the web server behind Nginx
- Server Admin › System › Web server profiles › Web server: a server runs every "Nginx + Apache" website on Apache (default) or OpenLiteSpeed. Nginx stays in front for everything public (TLS, HTTP/3, AutoSSL, site firewall, IP rules, maintenance and error pages, the Nginx page cache, static files, access logs); OpenLiteSpeed listens on
127.0.0.1:8088and runs PHP through LiteSpeed's lsphp as the account (an lsphp per website, defined in its own vhost; the account's PHP settings and worker limit from its PHP-FPM pool). Switch installs OpenLiteSpeed and lsphp from LiteSpeed's repository (signing keys checked against pinned fingerprints; aptsigned-by, dnfgpgcheck=1; Intel/AMD and ARM), writes every website's vhost from the panel's data, tests it (openlitespeed -t,nginx -t), starts OpenLiteSpeed, points Nginx at it and only then stops Apache; any failure puts the previous web server back. Switching back keeps OpenLiteSpeed installed. The admin console listens on127.0.0.1:8089only, with a random password kept sealed (Show the admin password). - .htaccess on OpenLiteSpeed: rewrite rules are read by OpenLiteSpeed; Basic auth and closed folders are applied by Nginx in front. Changes are loaded by a graceful restart: a few seconds after a change through Files, the .htaccess editor, Directory privacy or WordPress actions (static-rescan for the account), within 5 minutes for SFTP/SSH changes (hf-api sample's static-rescan).
- Control Panel › Websites › Speed test › Speed settings: LiteSpeed Cache (WordPress) on OpenLiteSpeed: installs and activates the LiteSpeed Cache plugin as the account, switches its cache on, adds its
.htaccessrules and the cache module to the vhost; the Nginx page cache is switched off first and can't be switched on meanwhile; Empty page cache empties it too. Columndomains.lscache. - Help: new page Server Admin › System › Web server; Speed test (LiteSpeed Cache), WordPress, System requirements, Ports.
Point-in-time restore of MySQL databases
- Server Admin › System › Backups › Point-in-time restore (MySQL) (one card, no other home): Let customers restore MySQL databases to a point in time and Keep binary logs for 1–30 days (7 by default), applied to this server and every joined server; the card shows this server's binary logs (space, files, since when). Switching on or off restarts MariaDB once; a new retention is set at once (
SET GLOBAL binlog_expire_logs_seconds). - One binary log setting for every feature (
agent/lib/30-db.sh): each user of the logs says how many days it needs (mysql_binlog_want pitr|replication DAYS, agentdb-binlog-set, kept in/etc/hostfog/binlog/); the logs are on (ROW format,mysql-binin the data folder) while anyone needs them and kept as long as the most demanding one asks (62-hostfog-binlog.cnf, written only bymysql_binlog_sync).db-binlog-statusreports them. - Every MySQL dump records its log position: backups, snapshots and Database tools dumps use one function,
mysql_dump_db(--master-data=2while the logs are on, and the time the position was taken). - MySQL databases: Restore to a point in time on each database's row (customers; administrators and resellers on the staff list, MySQL rows): a minute in the viewer's own time zone within the period shown, into a new database (default name
<name>_r<MMDDHHMM>, counts towards the package limit) or over the database itself (its name typed; a dump of it is made first and kept under Database tools › Dumps). A background job (hf-api pitr <id>, agentjob-pitr, tabledb_pitr_jobs, one per account at a time) runs three agent steps —db-pitr-prepare(the newest dump, full backup or snapshot of the database taken before that minute whose dump knows its log position; the logs since, read withmysqlbinlog --database=… --start-position … --stop-datetime …, renamed with--rewrite-dbfor a new database),db-pitr-load(loaded with the account's rights),db-pitr-replay(as a throw-away user whose rights end at that database, withBINLOG REPLAY; GTID and server id lines dropped so replicas take the changes as new ones). The screen shows the step with a progress bar; the activity log hasdb.pitr_start,db.pitr_done,db.pitr_failed; the account and the staff member who started it are notified. A failed restore removes the database it made. - Limits (in the docs): row changes made while another database was selected are restored; table changes made then are not; statements naming the original database fail in a new one and are skipped and counted.
- Help: MySQL databases (Restore to a point in time), Backups (Point-in-time restore of MySQL databases).
Spam filter: Rspamd next to SpamAssassin
- Server Admin › Email › Mail health › Spam filter (one card, no other home): the engine in use (SpamAssassin or Rspamd, green while it runs) and Spam filter engine with Switch (confirmed). Choosing Rspamd on a server under 2 GB says it needs about 250 MB more memory; choosing it before it is installed says installing takes a few minutes.
hf-agent spam-filter status|rspamd|spamassassindoes the same on the command line. - New servers get Rspamd from 2 GB of memory and SpamAssassin below (it uses less; decided in
install.shwith the same 1,800 MB mark as its "2 GB+ recommended" note); a server installed again keeps its engine; when Rspamd can't be set up SpamAssassin stays and the installer warns. Existing servers stay on SpamAssassin until switched;update.shrefreshes HostFog's Rspamd settings on servers that use it. - Every mailbox keeps its settings: both engines write the same headers (X-Spam-Level, X-Spam-Flag at 5), so each mailbox's Sieve script (strength, Spam folder or subject mark, allow and block lists, daily summary) works unchanged. Rspamd also turns away mail at SMTP time for mailboxes set to Refuse it above their score, never for their Always allow senders, and only when every mailbox of the server a message is for agrees (settings priorities). Each time a mailbox's spam settings are applied its refuse score and allow-list go to the agent (
mail-spam-rules, kept in/etc/hostfog/spam-ruleswhichever engine runs); a switch to Rspamd sends every mailbox's at once. - Learning: Not spam / Spam in the Spam folder review list and Not spam in the daily spam summary teach the engine in use (
rspamc learn_ham|learn_spamon Rspamd's controller socket, orsa-learn); the summary's Not spam now teaches too. - Rspamd as HostFog sets it up (
agent/lib/46-mail-rspamd.sh): from rspamd.com's stable repository with its signing key's fingerprint checked (Debian/Ubuntuapt-stable, the rhel family 8–10rpm-stablewithgpgcheckandrepo_gpgcheck), the distribution's package on a Debian/Ubuntu release the repository lacks; the proxy worker as Postfix's milter on localhost:11332 scanning by itself (no normal worker), the controller and web interface only on a root-only Unix socket, greylisting off, SPF/DKIM/DMARC/ARC checked, DKIM signing left to OpenDKIM (dkim_signingand ARC signing off), Bayes in a Redis of its own (hf-rspamd-redis: Rspamd's user, a Unix socket, no network, 64 MB under 2 GB / 128 MB above, saved to/var/lib/hf-rspamd-redis), one scanning process under 2 GB.rspamadm configtestmust pass and the milter port must answer before Postfix is switched; SpamAssassin's packages and Bayes data stay for a switch back. - Health, services, smoke test: the engine in use is checked; Services lists Rspamd and its Redis;
rspamdis a protected package;hf-agent uninstallremoves Rspamd, its repository, its Redis data and HostFog's Rspamd settings. - Help: Mail health (Choose the spam filter), Email accounts (spam filters), Services, Server layout, Email index.
Standby servers: a constant copy of a primary (high availability phases A and B)
- Server Admin › Accounts › Standby & failover (new licence module
ha, which needscluster; shown once a server is joined): the Add a standby card opens a wizard — choose the primary (this server or a joined one) and a joined server without accounts as the standby, Check space, Start the first copy (a background job,hf-api standby <pair>). The list shows each pair's state (First copy running, First copy failed, In step, Falling behind, Not copying), how far behind Files, Email, MariaDB and Panel data are, the last check, its history (Details), Copy again and Remove. A standby takes no new accounts; Servers shows a read-only Standby of … label and refuses to remove a paired server; Fleet has a Standby lag column. - Files: the standby's
hf-standby-sync@<pair>unit pulls each changed hosting account's home with rsync over SSH as soon as the primary's inotify reports it, plus a full pass every hour (homes of deleted accounts removed). The pair's own key reaches only a forced command on the primary (standby-serve: watch, the account list and rsync's sender for a home, the mailbox list or the dump; only from the standby's IP). - Mail: Dovecot's dsync (
doveadm backup -R) of each changed mailbox from the primary's doveadm port over TLS, open to the standby's IP only, with a pair secret sealed withSecretSeal; the primary's mailbox list is the standby's user database (no logins). Dovecot 2.3 and 2.4. - MariaDB: GTID replication over TLS with the pair's own replication user (
REQUIRE SSL, standby IP only), seeded from a consistent dump with the users (--single-transaction --gtid --system=users, MariaDB 10.11 or newer); the CA is pinned when HostFog made the certificate; the replica is read-only with its own server id; binary logs (7 days, on both) through the sharedmysql_binlog_want replication; its lag is read from the replica. - Panel data: a
VACUUM INTOcopy every 5 minutes over the signed node link when the primary is this server. - Checks: every 5 minutes (
hf-api sample) and Check now: lag per kind inha_status, state changes as events inha_events(tablesha_pairs,ha_status,ha_events; schema and migration). - Not yet: promoting the standby, DNS repointing, fencing, failback (later phases).
- Price: the Business plan includes
haat a fixed £69 a month (website catalogue andDefaultLicCatalog); Starter and Pro Web unchanged. - Help: new Standby & failover page; Servers, Fleet, Accounts index;
docs/LICENSING-MODULES.md.
Operating systems: Rocky Linux 8, AlmaLinux 8 and CloudLinux 8, 9, 10
- Rocky Linux 8 and AlmaLinux 8 (x86_64) install like 9 and 10: EPEL with PowerTools, PHP 8.3 and the other versions from the Remi repository (signature checked; remi2018 is EL 8's key; the installer stops when Remi can't be added, as el8's AppStream stops at PHP 8.2), and the newer module streams nginx 1.24, MariaDB 10.11, Redis 6, PostgreSQL 16 and Ruby 3.3. Python 3.11 becomes
python3through alternatives (os_rhel_python; 3.6 is too old for the helpers; put back on uninstall). End of life May 2029, stated in the docs and by--check. CentOS Stream 8 stays unsupported. - cgroups v1 on el8: the installer leaves the boot settings alone and doesn't switch limits on; it warns, and the new
hf-agent cgroups-v2addssystemd.unified_cgroup_hierarchy=1to every kernel (grubby) for the next boot. The agent tells the panel why limits can't be enforced (limits_blocked:OK no cgroup v2,OK no cgroup v2 reboot,OK managed by lve); Packages, Top accounts and the usage charts show the matching notice (LimitsUnsupported,Ctx.UsageLimitsNote, payloadlimitsNote; two new texts in 20 languages). - CloudLinux 8, 9, 10 (
ID=cloudlinuxor/etc/cloudlinux-release;OS_CLOUDLINUXin os.env) is treated as the AlmaLinux of its version. LVE keeps the accounts' limits: HostFog's limits stay off (limits-setkeeps the numbers only,limits-mode onis refused, the installer doesn't switch them on); with CageFS on, the isolation check marks the private filesystem view and the jailed shell as not possible; LVE's default limits are shown read-only (os_lve_summary: isolation check, smoke test); CloudLinux's alt-php versions are listed (hf-agent php-versions, smoke test), not managed. EPEL falls back to Fedora's signed release package; any*-crb/*-powertoolsrepository name is found. - el8 tools: Ed25519 signatures (download, self-update, licence) are checked with Python's cryptography module where OpenSSL is 1.1.1 (
os_ed25519_verify, get.shhf_sig_ok; get.sh installs python3 and python3-cryptography on el8);tar -I zstdinstead of--zstd(tar 1.30); config history ids without bash 5'sEPOCHREALTIME; SELinux booleans missing from a policy are named instead of failing. - get.sh, install.sh,
--check, smoke test: the new systems in detection and refusal messages; the pre-install box shows "cgroups v1";--checkreports resource limits (cgroups v2, v1 or LVE) and el8's end of life; the smoke test has a System section. - Docs: System requirements (table, "What works on which system" with el8 and CloudLinux columns, version 8 and CloudLinux sections), the Rocky Linux / AlmaLinux tutorial (version 8, cgroups v2, CloudLinux), Resource limits, Smoke test, CLI (
cgroups-v2).
Plugins: signed packages, sandboxed hooks, scoped tokens (marketplace phases A and B)
- Manifest 2 packages (
.hfp):plugin.jsonwith"manifest": 2,publisher,min_panel,permissions[],ui[]andhooks{}; the package carriesMANIFEST.sha256,SIGNATURE.vendor(Ed25519, the vendor's key) andSIGNATURE.market(the marketplace's countersignature of the vendor key and file list). The marketplace public key is built into hf-api (PluginMarketPubkey) and the agent (HF_MARKET_PUB); its private half iskeys/hostfog-market.keyonly (build.sh's key guard keeps.keyfiles out of releases). - Install policy, checked by the root agent itself: countersigned packages install; vendor-signed ones only in Developer mode (
hf-agent plugin developer on|off|status, a root-owned file hf-api can't create; logged); unsigned manifest 1 plugins only as a local trusted plugin (plugin-install local,hf-agent plugin install … --local), labelled "runs as root, local only". A revoked vendor key or package (/etc/hostfog/plugins-revoked) is refused and its hooks stop. - Sandboxed hooks: a manifest 2 plugin's hook never runs as root: new agent command
plugin-hook ID EVENT [API_HOST]runs it ashfp-<id>throughsystemd-run(DynamicUser, ProtectSystem=strict, ProtectHome,/etc/hostfogand HostFog's files hidden but its own folder, PrivateTmp, NoNewPrivileges, no capabilities, IPAddressDeny=any + IPAddressAllow for the public addresses of itsnet:hosts, 30 s, 256 MB), the payload filtered to its permissions.hook-runruns only local plugins' hooks. - Plugin tokens: enabling a plugin with API permissions makes an
api_tokensrow withkind = 'plugin'andscopes; the token API refuses every path its scopes don't cover (PluginTokenAllows, areas accounts, domains, dns, mail, databases, billing); deleted on disable or remove, hidden from API tokens. Columnsapi_tokens.kindandscopes(schema and migration). - Server Admin › System › Plugins & hooks: each plugin shows its trust and permissions; Install plugin opens the Review the plugin permissions dialog (signer, permissions, those an update adds marked New, the "my own plugin" tick for a local one) before anything is installed; a Developer mode notice.
- Extension points (phase B):
client.account-card(a card on the customer's Overview) andadmin.account-tab(an entry in each account's Manage menu), and the role menus, show a marketplace plugin's page in a frame without the panel's origin (sandbox allow-scripts, CSPsandbox, no connections) with a postMessage bridge (hf:context,hf:ready,hf:resize;ui/PluginFrame.tsx,svc_plugin_ui.go). - Help: Plugins (rewritten), new plugin.json and Plugin permissions pages, Plugins & hooks screen.
Community IP blocklist
- Server Admin › Security › Firewall: a Community blocklist card with two separate switches. Use the community blocklist fetches the publisher's signed list every 30 minutes (Ed25519, like the licence answer) and loads it into an nftables table of its own (
inet hostfog_blocklist, both OS families, before ufw's and firewalld's rules); entries expire after 2 hours unless they are still listed. On for new servers, off for servers installed before. Share attack signals (off by default) sends, every 30 minutes, the addresses that clearly attacked the server: fail2ban bans for SSH and email, panel sign-in floods, web firewall blocks by exploit rules (930–933, 944) and 404 scans of well-known attack paths. A report holds only the address, a reason and the time, each address at most once a day, signed with the licence key (HMAC; the key itself is not sent). Never blocked: the server's own addresses, joined servers, administrators' sign-in addresses of the last 30 days, the current administrator, and the Unblock list on the same card. Figures: listed, blocked here, reported in 24 hours. - Licence server (official edition):
POST /v1/licence/blocklist-reportandPOST /v1/licence/blocklist(rate-limited, working licences only); an address is listed when at least 3 servers reported it within 24 hours, scored with a 12-hour half-life, at most 50,000 entries; HQ's allow-list, licensed servers' own addresses and verified search engine crawlers are never listed. HQ › Abuse watch shows the list's figures and top entries with Allow. - Website: the privacy policy mentions the opt-in sharing.
- Help: Firewall page, new Community blocklist section with a privacy note.
Page cache: one page at a time, campaign tags, disk use
- Control Panel › Websites › Speed test › Speed settings: Empty one page (shown while the page cache is on) removes one address, typed as
/about/or the full address, from the website's cache and its parked domains' caches. Empty page cache now also empties the parked domains. - Cache rules: the key is scheme, method, host and the address as sent, plus its query string unless it holds only
utm_*campaign tags; those visitors now get the cached page, any other query string still bypasses it unless the website caches query strings. Bypass rules unchanged (logged-in WordPress/WooCommerce cookies, cart, checkout, my-account, admin, search, feeds, POST). - WordPress: the must-use plugin now writes the addresses that changed (a post: its page, the home page and the posts page; a comment: its post; WooCommerce stock: the product) to
~/tmp/.hostfog-purge-<domain>; theme, menu, widget, permalink and plugin changes still empty everything. The server takes the file as the account (mv/head/rmthroughas_user), so a link or a huge file gets root nowhere; an empty file (the earlier plugin) empties the whole cache. - Server Admin › System › Speed defaults: Page cache of new WordPress websites (Off by default): a website on which WordPress is installed from Apps gets that page cache; one that already had the cache on gets the plugin at once. Existing websites never change.
- Disk use: each account's cache now lives in
/var/cache/nginx/hostfog/<account>/<domain>and counts in its disk usage (Page cache for rows on Disk usage); the old<account>--<domain>folders are removed when the cache file is next written. - Help: Speed test page (new Page cache section: rules, Empty one page, WordPress), Speed defaults, Disk usage.
One home for every setting
- Each setting, card and action now has one screen. Billing keeps payments, referrals, orders and invoices; discount codes are made only on Discount codes, VAT number, VAT rate and "prices include VAT" are on Tax & currencies (with the single rate), reminders and suspension on Unpaid invoices. Security overview shows the state and links on: the firewall's mode moved to Web firewall control, the scanner's settings and "Scan every account" are Malware's. Brand name and colour moved to White label, the approval mode to Approvals, requiring two-step sign-in for staff is Two-step sign-in's only. Resource usage shows the server (accounts: Top accounts), the Overview keeps one live tile and the health card, System updates shows the HF Panel version (the rest: Software versions), PHP settings and customers' Domains link to PHP extensions and DNS setup, live chat inserts Saved replies edited on Support tickets, and a reseller's feature allowance is set on Reseller privileges. The other place shows a "Manage in …" link where people look for it.
- Menus: screens that belong together are one menu entry with tabs under the heading — Server health + Services, Resource usage + Capacity forecast, Updates (System updates + HF Panel updates), Software (Software versions + System packages), Config files + Config history, Web firewall (control + site firewall rules); customers: DNS zone editor + DNS templates & import, PHP settings + PHP extensions. Every address keeps working. Customers' Security scan is gone (its findings and per-website firewall were Malware's and Web firewall's):
/?r=site-securityand its path open Malware, and a team role that had it gets Malware and Web firewall. Redirects & IP blocks is Redirects. - Blocked addresses: the Redirects screen's per-website blocks are moved once into the account's IP blocker (Firewall rules) by
hf-api migrate, with the website in the note; the old rows are kept but no longer sent. The API's/accounts/{username}/ip-blocksis now the IP blocker's older address (OpenAPI regenerated); the "IP blocker" plan feature covers the Firewall rules screen. - Removed duplicates: Billing's
dunning,coupon_new,coupon_off; Security overview'swaf-mode,scan-all,scan-settings; chat'scanned_save/canned_delete; Feature lists'reseller; Redirects'block; the Settings fields for brand, approval mode and staff two-factor (stored values kept). New actions:waf-control.mode,approve.mode,reseller-acl.features. 65 unused texts removed, 7 added in every language. - Help: pages updated; Control Panel › Security scan removed. Screenshots to retake: billing-admin, billing-tax, security, waf-control, settings, branding, approve, feature-lists, reseller-acl, monitor, dashboard, server, live-chat (Server Admin); redirects, php, domains, site-firewall (Control Panel).
Stronger isolation under heavy load
- Packages: Priority (Low / Normal / High: the account's CPU and disk weight, 50 / 100 / 200, applied live), Database connections per user, Longest query and Concurrent requests per website; an account's own values in its limits dialog (Top accounts) and in bulk changes. Database limits go on every database user of the account and on users made later; the request cap is Nginx
limit_connin each website (over it: 429). - Security › Account isolation: System services first under load (MemoryLow and a bigger CPU and disk share for MariaDB, Postfix, Dovecot, Nginx and Apache; hf-api in its own
hostfog.slice) and Stop the account under memory pressure (systemd-oomd onhf.slice; skipped, with a warning, where systemd-oomd is not installed). Both off until switched on. - New servers enforce resource limits from the start (
install.sh, cgroups v2); servers installed before keep theirs until Packages › Enforce limits.
WordPress: networks of sites (multisite)
- Control Panel › Websites › WordPress: a Network of sites (multisite) card. Make it a network turns a site into one with subdirectory or subdomain sites (the safe updates' pre-update backup first); a network lists its sites with Add a site (a short name, or a domain of its own added as a parked domain so AutoSSL covers it; a subdomain site can be served as a subdomain website of its own), Archive, Deactivate and Delete, the themes it offers, its web server rules (Set up) and, for subdomain sites, the
*DNS record (added when the domain's DNS is here, otherwise the record to add is shown) and Get the wildcard certificate. Plugins get Activate on every site; updates also upgrade every site's database, the caches clear the network's, and Admin password lists the super admins. The App installer's WordPress form has Network of sites (multisite). - Help: WordPress page section and the tutorial "Run a network of WordPress sites".
Email: Import mail from another provider
- Control Panel › Email › Mailboxes › Import mail: copy a mailbox from the customer's old email provider over IMAP (SSL or STARTTLS) into one of the account's mailboxes, all folders or chosen ones, in the background, with progress, a log, Cancel and a notification and email when it ends. Special folders go to the mailbox's own (sent, drafts, trash, junk, archive); "all mail"-style views are left out. With Skip messages that are already in the mailbox a re-run copies only what is new (Message-ID and date, or date and size). hf-api is the IMAP client at both ends (Mail's client, now also
mail.DialIMAP/mail.DialLocal; this server's mail server through Mail's master user), so no root and no agent command. The old password is sealed until the job ends, then wiped; a stopped worker carries on from each folder's last UID (hf-api mailimport <id>, restarted byhf-api sample). Limits: one import per account,mail_import_maxat once on the server (default 2),mail_import_ratemessages a second, 8 connection tries per 15 minutes, 100 MB per message, 12 hours per job; only public addresses, never this server's own. - Removed: the old mailbox copy of Control Panel › Files › Import from another host (the "Mailbox (IMAP)" form, the agent's
pull-mailandhf-imapsync.py;update.shdeletes the installed copy). That screen now links to Import mail; past mailbox copies stay listed, and one still waiting fails with a pointer to Import mail. - API:
GET/POST /accounts/{username}/email/imports,GET/DELETE /accounts/{username}/email/imports/{id}(OpenAPI regenerated). - Help: new page Control Panel › Email › Import mail; Import accounts links to it for customers moving only email.
Email: outgoing mail relay
- Server Admin › Email › Server mailboxes › Outgoing mail relay: for servers whose provider blocks outgoing port 25, all outgoing mail can go through an SMTP relay: host, port (587 STARTTLS, 465 TLS, 25 or 2525), username and password, and Send test (waits for the relay's answer and shows its reason when it refuses). The password is sealed with the server key and reaches the server agent only on stdin; an empty field keeps the saved one; switching off keeps the settings. The card shows whenever mail is on, also without a domain of the server's own.
- Post-install check:
hf-agent smoke-testtests the relay's host and port when one is set; a blocked port 25 now points to the relay card. - Help: new page Server Admin › Email › Outgoing mail relay; every hosting provider guide links to it.
HF Connect: HF SSH and HF FTP become one app
- One window (
desktop/connect,HF-Connect.exe): Hosts, then for a host its Terminals (tabs, SFTP side panel, port forwarding, keys, known hosts, jump hosts) and its Files (two panes with drag and drop, the transfer queue with pause, resume, retry and a speed limit, permissions; SFTP, FTPS and FTP). The window code, Go methods and tests of both apps moved over unchanged; the files side'sConnect/Disconnectare nowFilesConnect/FilesDisconnect. - Quick connect takes a saved host's name or an
ssh://,sftp://,ftp://orftps://address with user, port and folder (hfconnect.ParseURL); an address no saved host matches (hfconnect.MatchHost) is saved as a new host, a password in it is used once and never stored. - Open files here in a terminal opens Files on the same host in the shell's folder (OSC 7 or the
user@host: dirtitle, else the SFTP panel's folder); Open terminal here in Files opens a terminal that starts in the folder shown. Each uses its own connection, signed in with what was typed for the other (memory only). - Install and upgrade: one Windows setup and portable zip, one
.deband.tar.gz(desktop/build.sh→releases/hf-connect/, withHF-Connect-<v>-SHA256SUMS.txt). The setup removes this user's HF SSH and HF FTP by running their uninstallers silently; the.debreplaceshf-sshandhf-ftp. Saved hosts, keys and settings stay (HostFog/HF Connectand the keychain). - Release:
build.shbuilds HF Connect when the Wails CLI is installed (a failed build stops the release), records its sizes and checksums insite.json(connect) and ships the files ininstall.sh's website downloads; the website's Software page and menu offer HF Connect with checksums instead of the two "coming soon" apps. The help site's SSH and FTP section is HF Connect's (same addresses).
Security: November 2026 free-tool scan fixes
- Sign-in lock-out: wrong passwords sent from elsewhere no longer lock a login's owner out. A browser that signed in before (a one-year
hf_known_<id>cookie, an HMAC with the server key that a new password retires) or, without it, an address that signed in to that login in the last 30 days skips the per-login wait (panels, Files, DB Studio); Mail's mailbox lock skips an address that signed in to the mailbox in the last 30 days. The per-address limits still apply to everyone. Content-Security-Policy: the public pages (/order, /pay, /status, /kb, /licence, unsubscribe) get a full policy (scripts only from the panel or the page's own nonce; inline handlers replaced), andform-actionon the pages and the panels names only this server's own addresses (panels, Files, DB Studio, Mail, billing pages, resellers' own addresses) and Stripe/PayPal instead of any https site. DNS: deleting a record id that isn't in the zone answers "Record not found." (TestSigninKnownDeviceSkipsWait,TestSigninKnownPlaceFilesApp,TestLoginBlockedKnownPlace,TestCSP*,TestDNSRecordRules). - Go 1.26.10: every Go program (hf-api, HF Server, HF SSH, HF FTP and their shared core, the signing tool) is built with Go 1.26.10 instead of 1.24.7, which no longer gets security fixes. govulncheck had found standard-library problems the code can reach (net/http, crypto/tls, crypto/x509, html/template, net/url, net/mail, archive/tar and others); with 1.26.10 it finds none. The
go.modfiles ask for Go 1.26 (toolchain go1.26.10) andbuild.shrefuses an older Go. - Go modules:
golang.org/x/net0.43.0 → 0.62.0,x/crypto0.41.0 → 0.58.0,x/text0.28.0 → 0.43.0,x/sys→ 0.49.0,x/sync→ 0.24.0,filippo.io/edwards255191.1.0 → 1.1.1 and the PostgreSQL driverpgx5.7.2 → 5.9.2 (GO-2026-5004, DB Studio) in hf-api;x/crypto0.33.0 → 0.58.0 andx/net→ 0.62.0 in HF SSH, HF FTP and their core. hf-api'sgo.modno longer swapsgolang.org/x/*and the other modules for GitHub mirrors (that hid them from govulncheck);backend/api/README.mdsays how to use the mirrors for one build whengolang.orgcan't be reached. - Panel backups: opening a panel backup (.hfpa) stops at 4 GiB for one file and 8 GiB in all, so a crafted archive can't fill the disk next to the panel database.
- Lock files are made readable by their owner only (0600); when the panel's lock folder can't be written, an account lock goes into a private folder of the panel's user in the temporary folder instead of a shared name another local user could take first.
- HF Server on macOS: the start-up error box escapes its title and text for AppleScript (a
\or"in a path could break it). govulncheck1.9.0: 0 vulnerabilities the code can reach in every Go module; the only remaining note is GO-2026-5932 (the deprecatedx/crypto/openpgppackage, never imported, no fix).
Fleet: many HF Panel servers from one place
- Server Admin › Accounts › Fleet (cluster licence feature, shown once a server is joined): every server on one list with its health (answering, load per core, memory, disk), HF Panel version and Update available, accounts, last contact and licence state; totals at the top, search and a filter (online, not answering, update available, needs attention), read every 5 minutes or with Check now.
- Jobs on many servers: tick servers and click Create on New job to update HF Panel, run the auto-fixer (new agent command
autofix), restart a service, or copy a package or feature list; each server's answer is kept, the job runs in the background (hf-api fleet, four servers at a time) and can be cancelled. Everything goes through the signed server link; a joined server answers the update and the copies itself (panel-update,panel-push), and itsnode-infonow says its licence state. - Alerts: a server not seen for 15 minutes or with its disk over 90 % (both adjustable) is reported in the notifications centre (
fleet.unseen,fleet.disk).
First build of the 1 November work: fixes, screenshots, tests
- Built and tested: the full
build.shpassed (all 12 steps: every interface test and type check, the Go tests of HF Server and hf-api, the 24 agent checks, every release file). HF SSH and HF FTP compile for Windows and their shared core's tests pass. hostfog help: the newoffline-cacheline had landed inside the HF Tunnel line; the help text reads correctly again.- Licence shop and Change plan: the old plan builder's script was still on the page and is gone; each plan shows what it installs again ("installs Postfix (outgoing mail), Dovecot …"); an order that sends features without a plan is refused with "Choose one of the plans." instead of reaching checkout as an old every-feature order.
- Right-to-left languages (Arabic, Persian, Urdu): the new top bar pushed the page sideways and hid the sidebar in Mail, Files, DB Studio and the panels; it now uses direction-aware CSS, so the layout mirrors correctly.
- Mail: the "Read" button of the message list is translated in all 20 languages.
- Every screen checked in the browser (
tests/e2e-screens, admin, reseller and customer, plus Arabic and phone width): the Manage menu of a row could close by itself just after an action (its content arrived after it was placed, so the first scroll shut it) and is now placed as soon as it is drawn; Discount codes on a server without a way to pay shows a hint with a link to Payment settings instead of a red error; the sample panel's Config files screen lists the service files (dry-run answers). The browser test follows path addresses, collapsible menu groups and New card dialogs, and reports a menu link that opens the wrong screen. - HF SSH / HF FTP: a port forward may listen on port 0 (the system picks a free port).
- Mail, Files and DB Studio checked feature by feature in the browser: Mail no longer carries an old green message onto the next page opened later (e.g. "Settings saved." on Compose); the calendar shows times on the 24-hour clock in English; Folders shows a folder inside another as "Orders / 2026" and no longer repeats "Drafts" in a pill; on phones the contact rows and the PGP key history stay inside their cards. Files: Move takes a folder name without a leading "/" as a folder inside the one shown (it said "Not found: /img"); a share link is only made where it gets an address people can open (otherwise "Share links are not available here." instead of a "made and copied" link with no address); row-menu items are no longer underlined.
- HF SSH / HF FTP checked page by page: the transfer queue has connections of its own (an FTP connection moves one file at a time, so browsing waited for transfers and parallel transfers clashed; Disconnect stopped the queue), handed on from file to file and closed when done; a wrong password is asked again with an empty field; questions (delete, rename, new folder, permissions) use the window's own dialogs instead of the system's boxes; Enter saves the host, key and import forms; Import key file asks for the key's name and passphrase (a key with a passphrase couldn't be imported), and a pasted ssh command's
-ikey can be imported from the host form; closing a save or open dialog is no longer an error; typing reaches the terminal in order; an ended terminal has Connect again; long lists on Keys, Known hosts and Port forwarding were cut off at the window's height; narrow windows keep every row's buttons; "Last week" / "N weeks ago" instead of "Last month". New Go tests for the queue (hfftp), the site manager and the SSH app, run bydesktop/build.sh. - HF SSH and HF FTP look like HF Server: the same 64px white top bar (the HF mark with "SSH" / "FTP", then "HF SSH / <page>"; search and quick connect on the right), the same sidebar with SVG icons, "Version x" at its foot and a new Help page (version, publisher, links to the help site's HF SSH / HF FTP pages), page headings with an icon tile, and HF Server's cards, tables, buttons, pills, fields, notices, dialogs and dark mode.
npm run dev:mockshows each window in a browser with sample data. - Browser tests (
tests/e2e) follow the path addresses (/login,/forgot,/reset) and the New card dialogs and step wizards. - Files links from the Control Panel: the screens under the sidebar's Files heading had addresses under
/files/…, which the panel's port gives to the File Manager, so a domain's Files link (and a reload of FTP accounts, Backups, Disk usage …) opened the File Manager's sign-in page; that heading's addresses are now/storage/…and no screen group may use another app's prefix (a test checks it). - Row menus in wide tables: a menu that builds its content when opened (Accounts' Manage, and the other long lists) was not lifted out of its table box, so typing in it (the Delete confirmation) scrolled the box and closed the menu; it is placed on the screen like the others, and a scroll that moves nothing no longer closes a menu.
- Browser tests: new steps for Memcached on/off, the IP blocker (block, own address refused, unblock), a custom 404 page and back, and Billing's "Before taking real payments" card;
E2E_PANEL_UI=DIRtests a fresh panel build without touching the committed one. - Go code formatted with
gofmt(14 files in hf-api). - Billing: a "Before taking real payments" card lists what is left before payments are real (Stripe live key and webhook secret, PayPal live and connected, selling on) and warns when Stripe and PayPal are in different modes.
- Website: the privacy policy and terms are rewritten for UK law (UK GDPR, the licence usage summary and how to turn it off, Stripe and PayPal, the desktop apps, refunds, liability, English law) with a "Last updated" date — to be reviewed by a lawyer before selling.
- One look everywhere: the HF Server window has the same white top bar as HF Panel, Mail, Files and DB Studio (brand in the corner, then where you are), and all 219 help-site screenshots are retaken from the current apps in light mode; the website's HF Server pictures too.
- HF SSH and HF FTP built, with HF Server's setup: the Windows setup has the same look and steps as HF Server's (its own welcome panel and header for each app, licence, folder, "Open … now"; installs for your user only; Start menu under HostFog; WebView2 set up only when missing), plus a portable zip; Linux gets a
.deband a.tar.gzwithinstall.sh, laid out like HF Server's. Artwork:tools/brand/make_installer_art.py. - Clean-up: each shared file (fonts, logos, icons, LICENSE, the billing modules' API client) is kept once in the source;
sync-shared.shmakes the copies during the build. 10 unused website screenshot pairs and 9 functions nothing called are removed. - Agent checks:
devflow-agenthad only ever been skipped (no rsync on the build machines); its first real run showed two test mistakes — the deploy script ran through the realrunuser(now stubbed throughtimeoutas well) and a check needed/etc/ssh. All 24 checks pass. - Memcached per account: a new Control Panel screen (Databases › Memcached) turns on a private memcached for the account, built like the Redis cache: running as the account, reachable only through a socket in its home (no port, no password), sandboxed, size up to the package's new Memcached memory limit (new packages 64 MB; 0 = none). Figures, Empty the cache, Turn off; stopped while the account is suspended and switched on again after a move. New agent commands
memcached-on|off|info|flushand agent checkmemcached-agent. - Accounts with thousands of customers: the list is paged on the server (100 a page, 25–500 to choose), sorted by any column heading, searched and filtered across every page; ticks stay across pages and Select all N matching ticks every match (Bulk changes then takes the search itself). 2,500 accounts: a page answers in about 30 ms with 31 KB. The other long lists (domains, mailboxes, databases, FTP, resellers, SSL, apps and more) now build each row's menu only when it opens, as Accounts already did. Tests for the last 11 screens without any (Composer, PEAR, PHP extensions, Hotlink and Leech protection, Web firewall and its control, Developer tools, both Notifications screens, the lock screen).
- Dialogs under stress (break-it test):
tests/ui/breakit-dialogs.pynow also covers the IP blocker, Memcached and Error pages and sweeps every New card on every screen of Server Admin and the Control Panel (backdrop click, Tab, 5000 characters, Escape, double-click, 360 px phone). Fixed what it found: the announcement preview sat behind its dialog and a long word widened the page (the preview is now in the dialog and wraps); a running job's terminal (Add swap, installs) now takes the keyboard focus and keeps Tab inside; a dialog's Tab no longer slips out past a field in a hidden part; in Files and DB Studio a double-click on a dialog's button whose request failed at once no longer sends it twice (Move, Rename, Add row, New collection …); Files keeps the cursor where it is when the list behind a dialog refreshes; Mail's event dialog keeps Tab inside and saves once on a double press. - Error pages saved before they reached Nginx: the update republishes them once (
hf-api migrate), so customers no longer need to save them again. - Error pages: new codes 429 Too many requests, 502 Bad gateway and 504 Gateway timeout; the pages now also work through Nginx (site setting
ERRPAGES,error_pageto named locations), so visitors see them for blocked requests, request limits, Apache or PHP down or slow and missing pages on Nginx-only websites. Maintenance mode's 503 page still wins while it is on. New agent checkerrpages-agent. - Token API checked against its own description: a new test calls all 436 operations of the OpenAPI description with the right role's token (answers, error format, paging, read-only tokens, other accounts). Fixed: the SSL, cron and FTP lists were described as lists but answer an object (new
CertificateList,CronJobList,FtpAccountListtypes; the SDKs could not read them);/ssl-certificates/{domain}always answered 404; five lists had no paging in the description; list items of Redis sizes, ticket messages and PHP sites were typed as text (the Go, Python and TypeScript clients now type them); missing MongoDB items and backups answer 404 instead of 422; emptying Redis while it is off answered a server error; a package change through the API reset its PHP workers and app processes; the Rust client did not compile. New: Memcached (/accounts/{username}/memcached, like Redis), the account's IP blocker (/accounts/{username}/ip-blocker), the packages'memcached_mb,redis_mb,php_workersandapp_processes; error pages 429, 502 and 504 documented. SDKs regenerated. - IP blocker: customers block addresses and ranges (IPv4/IPv6, CIDR) on all their websites at once from Security › Firewall rules, each with a note and an expiry (1 hour to 30 days, or never; expired entries go within 5 minutes; up to 500; their own address only with a tick); Block in a website's latest refused requests adds the address for 7 days. Administrators block countries on every website of the server under Site firewall rules (needs the country list). Both are merged into each website's site firewall rules, so websites added later get them too.
- SSL after a reinstall and Let's Encrypt's limits:
hf-agent uninstallnow keeps the SSL certificates in/root/hostfog-certificates.tgz(root-only), and the next install puts them back before it asks for any certificate, so still-valid ones are reused instead of asking Let's Encrypt again (it allows 5 certificates a week for the same names — several reinstalls had left a website without one). When Let's Encrypt answers with that limit, the time it gives is remembered and nothing asks again before then: the SSL/TLS row and Renew now say "Let's Encrypt's weekly limit for these names is reached; it tries again by itself after <time>", AutoSSL waits for it, and Server health (hf-agent healthtoo) lists it. Other certificate failures show certbot's one-line reason, never a traceback. New agent checkletsencrypt-agent. - Every link opens: a new browser check (
tests/ui/linkcheck.py) opens every screen of each role and every link on it, clicks each new-tab and sign-on link 5 times, and checks help-site links against the built help site. Fixed: a used Files or DB Studio link (Reload, Back, a restored tab) in a browser already signed in now opens the app instead of "That link has expired" (Mail too); DB Studio links to a MongoDB database or collection open it (they opened the start page); a button given a full address (Bulk changes › Accounts) pointed at another site; Mail's address may be a fullhttp://one; the website's install page names the current panel addresses with the panel port and its gallery image Mail in 21 languages was missing; the website's/login,/admin,/resellerand/ordershortcuts carry the panel port (they led back to the website); the website build now fails on a broken link (frontend/site/test/links.mjs, also the help site's links into the website). In a sandbox, "Open Mail" signs in for real (the dry-run agent writes Mail's token).
Code review fixes
- HQ staff roles on every panel of the official server: a support or view-only member was limited only on HQ's address and could still change the server in Server Admin there; the role now applies to every panel of the official server. Like read-only staff, every role may still change its own things — password, two-step sign-in (which HQ requires), notifications, help, the tour, signing out (before, a view-only member couldn't set up two-step sign-in).
- Payments totals per currency: the figures added up payments in every currency as if they were the main one; they now count the main currency only, with other currencies shown beside them, and "this month" no longer misses the 1st's early payments on the 31st.
- Releases: keeping the last 10 never removes the copy of the release servers get now (after a roll back it can be an old one).
- Verify a licence says "valid until" (the paid period and its grace days), not "paid until".
- Website: the partner API example sends
Content-Type: application/json; the docs describe the public licence check (/v1/licence/verify).
Build: the API description is written by the build
build.shnow writes the documentation's copy of the API description (frontend/docs/assets/openapi.json) from the code before the docs are built, so a release always carries the current one and no manual step is needed. The Plans & pricing interface test waits up to 5 s for the server's price (a busy machine running every test at once).
Fixed: licences of a partner billed by use counted as expired
- A partner billed by use pays after each month and so has no paid-until date; its servers' licences followed that empty date and were treated as expired (installs and updates refused). While such a partner is active or briefly late, its licences now count as paid through the month after this one; suspending or cancelling the partner still ends them (
LicState). Pool partners are unchanged. Found by the first full run of the Go tests. - Test fixes from that run: the partner API and release-channel tests read JSON with escaped slashes and quotes; the reseller welcome email test reads the mail's text part; one wording for a taken username ("That username is taken.") everywhere.
Build output folders renamed
build.shnow writes the HF Panel server package to releases/hf-panel/ (was releases/server/) and the HF Server desktop installers to releases/hf-server-pc/ (was releases/software/). Existing folders with the old names are renamed on the next build, so a--server-onlybuild still finds the last desktop build. Docs, the build guide and deploy-test (which still finds a package kept under the old name) follow.
HQ for a team: staff roles, SLA timers, activity log, abuse watch, licence server status
- HQ staff (Team): each administrator's role in HQ — Full, Support (answers customers: tickets, chat, help articles, data requests; sees the rest; can't change licences, prices, payments or partners) or View only. Enforced on every HQ request (guard.go,
hqRoleDenies; a database still being updated counts as full, so signing in never breaks). Nobody changes their own role, and one full administrator always stays. - SLA timers on support tickets waiting for an answer: "Reply within 3 h" / "Overdue 2 h" in the staff list — high 4 h, normal 24 h, low 72 h (settings
sla_hours_high/_normal/_low). - Activity log (Team): who changed licences, plans and prices, payments, partners, releases, discount codes and staff roles, with a search.
- Abuse watch (Servers): a key that checked in from more than one public address this week (a copied licence), and addresses that started more than one free trial in 90 days.
- Licence server status (Servers): the signing key, a release to install, today's check-ins, silent servers, the last payment and disk space — each OK, Check or Problem with what to do.
svc_hq_more.go, users.hq_role (added on update); TestHQExtras. Four screens and the ticket timer tried in a browser (desktop and phone). New texts in all languages.
Servers licensed through a hosting company point to that company
- A partner (a hosting company selling VPSs with HF Panel) now has a support email and website (HQ › Partners › Settings). Its licences tell their servers who the partner is (the licence answer's new
partner,partner_email,partner_url, added after the existing fields so servers already installed are unaffected). - On such a server, Server Admin › Licence says "Licensed through <partner>" with the partner's website and email, and its upgrade, plan-change and buying links go to the partner — the customer never lands on the HostFog shop for a licence the partner looks after. A licence bought directly is unchanged.
- Docs: Licensing › A licence from your hosting company. Test: TestPartnerLicenceContact. Tried in a browser. New texts in all languages.
Billing: choose how customers pay — card, PayPal or both
- How customers pay, a single choice at the top of Billing (HQ › Payment settings on the official server): Card (Stripe), PayPal or Both. It sets the two existing ticks every checkout follows (order page, licence shop, account, invoices); a way that isn't set up yet is greyed out, and the server refuses it with what to do first ("Add your Stripe keys below first…"). Payments already set up by card keep working whichever is chosen.
methodsaction,billingMethodsNow; TestPayChoice. Tried in a browser (desktop and phone). New texts in all languages.
HQ: live checks while typing
- HQ's create forms check as you type, like the new account's username: a new licence's server IP (Licensed servers — a public address that already has a licence is named with its key; a local address, LAN, loopback or IPv6, may hold several), a discount code (any case; a free one like it suggested, e.g. SPRING20 → SPRING202) and a new plan's key (starter → starter-2). Same rules and messages as the create actions, which still refuse a duplicate themselves (the code is also unique in the database).
ui/LiveCheck.tsx; the screens answer?check=ip|code|plan&value=(LiveCheck, svc_live_check.go). - Website sign-up emails get no live "already has an account" check on purpose: on a public page it would let anyone find out who is a customer.
- Test: TestLiveCheck. Tried in a browser on all three forms. New texts in all languages.
A new account's username is checked as it is typed; no duplicates that could break the server
- Live username check under the username of the New account form (Server Admin and the Reseller Panel) and the New reseller form: a moment after typing stops it says whether the name is free, and if not why — taken, held by a deleted account's kept backups, reserved, used by the server itself (mysql, postfix …), or the wrong format — with a free name like it to use in one click (sarah → sarah2). Only the newest answer is shown (
ui/UsernameCheck.tsx; the screens answer?username_check=,UsernameCheck). - The server checks the same on every create: new accounts (panel, API, import, orders) and new resellers now also refuse a name the operating system already uses (
UsernameFree); new resellers get the full check (kept backups, reserved names) like accounts. - Duplicate audit: usernames, domains, mailboxes, FTP logins, database users, packages, servers, licence keys and IPv6 addresses are unique in the database itself; app ports are chosen free among every app and every port in use; resellers' own addresses can't be used by another reseller; DNS records that break a zone (a CNAME beside other records, A/AAAA beside an ALIAS) are refused — and now the very same DNS record twice too; an SSH key can't be added twice.
- Tests: TestUsernameCheck (free, taken with a free suggestion, format, reserved, kept backups, a user of the server, the screen's answer). The check tried in a browser (taken → suggestion → free; reserved). New texts in all languages.
Search engines: every page's title, description and keywords, and FAQ results
- Every indexed page of the website (27) now has a title of 45–60 characters led by the words people search for, a description of 135–157 characters and its own keywords — no two pages share a title or description, so they don't compete. New: Sign in, Community, Privacy and Terms titles; all 14 docs pages (on the website) got their own titles, descriptions and keywords (
DOC_SEO, Doc.tsx), e.g. "How to Install HF Panel on a Linux Server". - FAQ results: the Pricing page's questions are now also given to search engines as schema.org FAQPage, taken from the rendered page so they always match what is shown (
faqOf, prerender.tsx). Every other page already had breadcrumbs; the docs pages are TechArticles. - Documentation site (docs.<domain>, 383 pages): every description now between 70 and 160 characters (four fixed, including the provider guides and the glossary).
- Website tests 13/13; docs tests pass; nothing names another panel.
The website, rebuilt around how visitors look for a hosting panel
- Header: an announcement bar for what is new, and drop-down menus — Product, Solutions, Pricing, Resources, Partners, Support — each link with a one-line note (native
<details>: they work without JavaScript and by keyboard; one open at a time, Escape or a click elsewhere closes; inside the mobile menu they open in place). Footer: six columns — Product, Solutions, Resources, Support, Company, Legal. - Home page: "Who it is for" (hosting companies, agencies, developers, businesses), the AI helper, Become a partner, and three cards — support, building on the panel, verifying a licence. Customer logos and quotes will come when there are real ones.
- New pages: Partners (volume discounts from the site's bulk table, the partner API, Stable or LTS updates, billing by use, your brand, how to join), Developers (the API, webhooks, plugins, the partner licence API with an example), and Verify a licence — anyone can check a key before buying a server with it.
- Public licence check on the licence server:
GET /licence/verify?key=…(alsoapi.<domain>/v1/licence/verify) answers whether the key is genuine and working, its status (active, trial, ended), plan and paid-until date — never its owner, email, address or server; 120 checks an hour per address (svc_lic_verify.go, TestLicVerify). - Checked in a browser on desktop and phone (no errors, nothing wider than the screen); titles 45–60 and descriptions under 160 characters; website tests 13/13; no other panel is named.
Short panel badges; Mail and Files without "HF"
- The panels' badges (top left) are now SA (Server Admin), RP (Reseller Panel), CP (Control Panel) and HQ; their full names stay on sign-in pages, menus, the docs and the website, so new customers always see clear words.
- HF Mail is now Mail and HF Files is now Files — in the panels, the apps, emails, the docs, the website and every language (the one text that became the same as an existing one, "Files", now uses that one's translations). DB Studio and HF Server keep their names.
Super Admin is now called HQ
- The publisher's own panel is named HQ (at hq.<domain>; badge "HQ", "Sign in to HQ"), so its name matches its address and is never mistaken for Server Admin. Renamed in the panel, its messages, the agent and DNS list, the docs and project files, in all languages. Internal names (
superadmin-…screens, portal "super") are unchanged.
HQ's address: hq.<domain>
- HQ's default address is now hq.<domain> (hq.hostfog.io on the official server) instead of sup.: clearer, and no common target like admin.. The official install,
hf-agent super-portal on, the DNS record list, the docs and the tests use it; a server can still choose another name withhf-agent super-portal on NAME.
The official server: the business in HQ, the server in Server Admin
- HQ's menu: Licences (Licence server, Licence keys, Plans & pricing, Customers, Announcements, Partners) · Servers (Licensed servers, Installs, Downloads, Releases) · Money (Payments, Payment settings, Discount codes) · Customer care (Support tickets, Live chat, Help articles, Data requests, Status page) · Help (Trash, Help). The hosting Business overview is no longer in it.
- Server Admin on the official server (with HQ's address) is the server only: the screens moved to HQ and the hosting-business ones the official server doesn't use (Business overview, Tax & currencies, Unpaid invoices, Quotes & credit, Affiliates, Domain sales, Licence) are not in its menu (
officialServerAdminMenu). Payment settings, Discount codes and the Status page also answer only on HQ; support, chat, help articles and data requests stay open to resellers and customers. - Licensed servers (third parties): unchanged.
- Test: TestSuperPortal (HQ's new menu; Server Admin without the moved and hidden screens but with its own; Payment settings refused on srv., support tickets not). Two new menu labels in all languages.
HQ › Payments and Releases
- Payments: a payment ledger (
lic_payments) records every licence and partner payment taken or failed, from the Stripe (invoice.paid,invoice.payment_failed) and PayPal (PAYMENT.SALE.COMPLETED,BILLING.SUBSCRIPTION.PAYMENT.FAILED) webhooks — once each, however often a webhook comes again (svc_lic_payments.go). The screen shows what came in this month and in 30 days, failed payments, the licences overdue now (with how they pay), and every payment with filters (paid / failed, Stripe / PayPal, key or email). - Releases: every paid-edition installer uploaded is kept (the last 10 per channel,
lic_releases,licence/releases/). Per channel (Stable, LTS) the screen shows the release servers get now and the earlier ones; Roll back to this makes an earlier one current again — its exact file, version, checksum and notes — so every server of that channel updates to it; Delete removes a kept one that is not current. Uploading stays under Licence keys (a button links there). - Tests: TestSuperAdminPayments (a Stripe payment through the webhook handler, recorded once though sent twice; a failed one; totals, overdue, filter; a PayPal sale once), TestSuperAdminReleases (kept, rolled back to the exact file and settings, current not deletable, last 10 kept). Both screens built and opened in a browser (no errors). 32 new texts in all languages.
Code review and clean-up
- Removed 25 Go functions nothing used — no call, no reference, not even in a string (tests that look functions up by name were checked): old wrappers whose work moved to a newer function (
OrderNew→OrderNewWith,LicOrderCheckout→LicOrderCheckoutCode,PaypalOrder→PaypalOrderIn,StagingCreate→StagingCreateWith,UiHTML→uiHTMLStatus) and helpers left behind by earlier changes (Failf,RouteGroup,ColumnValues,RowsJSON,CurPricesOf,DunningRemind,SVGLine(s),Pill,LicenceLockScreen,LicStripePrice,ListsUnsub,MalwareQuarantineRow,PaypalStatus,PortalGuard,PortalSessionName,CsrfToken,StaffReadonlyGuard,RepliesList,ReplyText). Each was found unused by two separate checks; the one import only they used (crypto/subtlein svc_lists.go) went with them, and the comments that named them now name what remains. Kept: methods that may serve an interface (the WebDAV disk, the PDF writer, the DAV store). - Removed 3 unused shell functions (
step_number,hf_fetch,version_minor) and a comment that still referred to one; the app installers'apps2_setup_*/apps3_config_*are called by name and stay. - Removed the unused
ServerEntrytype of the desktop app. - React and CSS (all frontends — panel, shared, website, docs, webmail, desktop app, Files, DB Studio — 584 files): no unused import and no unused private function or constant (a name counts as used if it appears anywhere else in its file; the panel and the website also compile with
noUnusedLocals/noUnusedParameters). Removed 11 style classes no code uses —signin-menu(the website's old Sign in drop-down) andbtn-darkfrom site.css;bar-chart,bars-x,termbox,tun-osfrom the shared stylesheet and its two copies (panel, webmail);contacts-split,msg-actions,read-topfrom webmail;srv-site(s)from the desktop app. Only the dead selectors went where a rule also styles live ones. Kept classes built at run time ('l' + levelin the docs). The panel's screens and the website's pages render pixel for pixel the same before and after, on desktop and phone. - Checked and kept: every one of the 124 shell scripts passes the syntax check; every page has a route and every interface module is imported; no exported function of any frontend is unused; both
distfolders are used (packaging READMEs, the TypeScript SDK); test data.logfiles are needed; no backup or temporary files. Translations: none removed — texts that look unused are matched against messages the server builds at run time. - Checks after the clean-up: all 895 Go files well-formed, installer and agent checks, translations (0 missing), company rules (0 errors).
sudo bash build.shcompiles and tests it all.
Success and error messages after actions, in every panel
- An error for a field you can't see now shows at the top of the page. When an action failed with a field's message (
FailField), the message went under that field even when the field was hidden — in a closed section, a hidden block or adisplay: nonepart — so nothing could be seen and the top of the page stayed empty. Messages now go only to a visible field; otherwise to the top (isShown,shared/lib/fieldErrors.ts). - Audit of every action (641, all panels): 576 confirm with a message, 41 answer with what the screen shows itself (downloads, previews, the import report, chat), the rest are meant to be quiet (signing out, Log in as, the terminal, steps that open the next page). "Forget an import" was the one that said nothing: it now says "Removed from the list." Every page sends its actions through the same handler that shows the server's messages; long jobs show theirs when the progress window closes; the page scrolls up to them; messages stay until the next action.
- Tests: fieldErrors.test.tsx (inputs in a closed section, a hidden block or display: none count as missing; a visible one and a checkbox take their message; opening the section shows it again) — also run in a real browser here. Checked in the built interface: Check now, Restart, Restore and Refresh usage show their message, also after scrolling down a long page, on desktop and phone.
WhatsApp alerts
- Notifications › WhatsApp: a sixth channel next to email, webhook, Slack, Telegram and Discord — in every panel's Notifications screen (Control Panel, Reseller Panel, Server Admin), switched on by default where all kinds are allowed. It sends through Meta's WhatsApp Cloud API with the business phone number ID, a permanent access token (kept sealed like the other channels' secrets, sent only as a Bearer header) and the number that gets the alerts (written with
+and spaces, kept as digits). Without a template it sends a plain message, which WhatsApp delivers within 24 hours of the person last writing to the business number; with an approved message template (name and language) the alert goes in its one variable{{1}}, on one line and at most 1000 characters, and always arrives. Graph API v25.0 by default; the settingwhatsapp_api_versiontakes a newer one without an update when Meta retires it (svc_alerts_send.go). The API's notification-channel fields and the three Notifications docs pages (with the 24-hour rule) include it. - Test: TestAlertsWhatsApp (the settings check, a text message to
/v25.0/<phone id>/messageswith the Bearer token, a template message, the version setting). 13 new texts in all languages (0 missing).
Reseller welcome email, every screen on a phone, moving a whole server
- Reseller Panel › My brand › Welcome email: a reseller writes the subject and opening its new customers get, with
{username}and{brand}filled in (its brand name when white label is on); the panel still adds the sign-in address, the password link and the DNS settings after it, so they can't be lost. An empty message goes back to the standard email (reseller_profiles.welcome_subject/welcome_intro,ResellerWelcome). - Every screen on a phone: all 154 screens of Server Admin, Reseller Panel, Control Panel and HQ opened at 390px — none wider than the screen. On touch screens, small buttons, copy buttons, one-line fields, dropdowns (also the Accounts bulk bar's) and icon buttons are now 40px tall (finger-sized); desktop is unchanged.
- Moving a whole server: every account moves with Bulk changes › all accounts › Move to another server — and now at most two moves run at a time, the others wait their turn (
moveTurn: one atomic claim,moves.started_at; a move started over 6 hours ago is taken as stalled). Before, each queued move started at once, so moving a whole server would have run them all together. Move accounts explains it in a new "Move the whole server" card with a link to Bulk changes; the new status "Starting" is shown while a move takes its turn. - Tests: TestResellerWelcomeEmail, TestMoveQueue. 14 new texts in all languages (0 missing).
HQ › Announcements, revenue figures, and "Sign out everywhere else"
- HQ › Announcements: one email to licence holders — everyone with a working licence, paying ones, trials, partners, partners on LTS, or the website's confirmed accounts (each with its count). Each address gets it once (any case); Send me a test first; Send to N asks to confirm the number; only emails actually handed over are counted; a line saying why the reader gets it is added; what was sent is listed (
lic_announcements,screen_superadmin_announce.go). - Revenue on HQ's overview: churn (paid licences that ended in the last 30 days, as a share of those paying), trial to paid (trials that ended 30–90 days ago and are paid now) and the monthly revenue of the last 12 months as bars, recorded by the nightly run (
lic_mrr,svc_revenue.go). - Password & security › Other sign-ins › Sign out everywhere else: every other browser and device of the login is signed out at its next click; this one stays (
profileactionsignout_others). Every sign-in was already in the audit log, with a notice for one from a new place. - Checked and already there: alerts to Telegram, Slack and Discord (Notifications), the Control Panel's getting-started checklist, the website health report (Site check).
- Tests: TestSuperAdminAnnounce, TestRevenueFigures, TestSignoutOthers. The pages built and opened in a browser (no errors). 31 new texts in all languages (0 missing).
Partners: release channels (Stable / LTS) and billing by use
- Release channels: each partner follows Stable (default) or LTS (HQ › Partners › Settings). Licence keys › upload takes the channel: the LTS installer is kept beside Stable (
latest-lts, its version, time, SHA-256 and notes underlic_lts_installer_…). A partner's licences on LTS get the LTS installer from/licence/downloadand the LTS release from/licence/release— only once an LTS installer is uploaded; until then, and for every other licence, Stable (LicChannelOf). Panels now send their key with the release check (?key=), so the licence server knows their channel; older licence servers ignore it. - Billing by use: a partner can be billed for what it uses instead of a pre-paid pool — no pool, an optional cap (
usage_cap; issuing past it is refused). The nightly run records each such partner's licence count (lic_partner_usage) and bills each ended month once at its peak × the tier price less the partner's discount (lic_partner_bills): a Stripe invoice (charged automatically) when the partner has a Stripe customer, otherwise "to invoice" for the publisher. Bill last month does it at once. Switching a partner to billing by use says to cancel its pool subscription so it isn't charged twice. - HQ › Partners: each partner's channel and billing as labels; for billing by use, the licences in use, the cap, this month's peak with an estimate, and the last bill; Settings (channel, billing, cap) opens in the row.
- Tests: TestReleaseChannels (LTS only once uploaded; release and download follow the channel; no partner → Stable), TestPartnerUsageBilling (the cap, the nightly count, last month billed once at its peak and price, a pool partner never billed by use). The screens built and opened in a browser (no errors). 25 new texts in all languages (0 missing). Docs: the partner API.
Partners: the partner API and HQ › Partners; guides for hosting providers
- Partner licence API (official edition): a hosting company with bulk licences issues, moves, removes and lists them from its own systems —
GET /v1/licence/partner/licences,POST …/issue {email, ip},…/move {key, ip},…/remove {key}onapi.<domain>(and/licence/api/partner/…) withAuthorization: Bearer hfp_…. Only the key's SHA-256 is stored (compared in constant time); a partner reaches only its own licences (404 otherwise); the pool's limit, public IPv4 and one-licence-per-server rules apply (409 with the reason); removed licences go to HQ's Trash; 401 / 403 / 405 / 429 as documented (svc_partner_api.go; columnsapi_key_hash,api_key_atonlic_partners, added on update). - HQ › Partners: each partner's company, email, plan level, discount, pool (used of bought), status and paid-until, with New API key (shown once; the old one stops) and Revoke, and the API's address with an example (
screen_superadmin_partners.go,superadmin-partners.tsx). The existing bulk discounts stay (10% from 5 servers up to 40% from 50). - Installation › Hosting providers: step-by-step guides for DigitalOcean, Hetzner Cloud, Vultr, OVHcloud, Contabo, AWS (EC2 and Lightsail), Akamai Cloud (Linode) and Oracle Cloud — the server to create, the provider's firewall with the exact ports, reverse DNS, outgoing port 25, then
bash latest --checkand the install — with an overview table. Developer docs: the partner API. - Tests: TestPartnerAPI (over HTTP: no or wrong key, issue until the pool is full, list, move and a move onto a licensed server, another partner's licence, remove into the trash, wrong method, revoked key). The Partners screen built and opened in a browser (no errors; fits a phone). 20 new texts in all languages (0 missing). Not built: release channels and usage-based partner billing.
HQ finished, the account's pages, payment switches, and a pre-install check
- HQ's screens answer only on
hq.(Licence keys, Plans & pricing, Licensed servers, Installs, Downloads, Customers, Trash): elsewhere a 403 that names HQ's address (superElsewhere, both API entry points). Only on the official edition once HQ has its address. - HQ › Customers: every account made on the website — confirmed or not, joined, last signed in, licences (all and working; opens Licence keys) — with totals, an email search (LIKE wildcards taken as text) and a confirmed filter (
screen_superadmin_customers.go,superadmin-customers.tsx). - HQ › Trash: deleting a licence under Licence keys keeps its whole row for 30 days (
lic_trash,svc_trash.go): Restore puts it back exactly (refused when its key is in use again), Delete for good, Empty the trash; older entries go by themselves. The paid edition deletes as before. - Stripe "Offer card payments" tick under Billing (PayPal already had one): every checkout — the order page, the licence shop, the account, invoices — offers exactly what is ticked (
StripeOffered,PayMethods,BillingOn); the order page can't open with nothing ticked. Webhooks, renewals and refunds of card payments keep working with the tick off (StripeOn). - The account: Buy a licence in the account's frame when signed in (the Free trial's first step keeps its simple page); Payments & receipts (each licence: card, PayPal or nothing yet, price, next payment, Manage payment → the provider's page); Security (change the password: the current one first; a refusal comes back to Security).
bash latest --check(precheck.sh, in the release): system, x86-64, root, memory, disk, another control panel, ports 80/443/the panel's, name resolution, the public IPv4 (NAT noted), outgoing port 25 — ✔ / ! / ✘ and "ready" or "fix these first"; changes nothing. From the public installer it runs before the licence question.- Tests: TestSuperPortal (+ the hq.-only rule), TestPayMethodsSwitches, TestAccountTrialFirst (+ Payments & receipts, Security), TestSuperAdminCustomers, TestSuperAdminTrash. The new screens built and opened in a browser (no errors; they fit a phone). New texts in all 20 languages (check-lang: 0 missing). Project docs: PRD (the narrowed plan), ARCHITECTURE (addresses), MEMORY, TASKS.
HQ at hq.<domain> (plan step 3, first part)
- A fourth panel for the publishing company's licence business, the official edition only:
hq.<domain>on the panel's port, its own session cookie (hf_super), administrators only, with two-step sign-in by an authenticator app (refused at sign-in — password and passkey — with a clear message, and on every request: switching two-step off signs out of HQ). It looks and signs in like Server Admin under its own name, badge, tagline and points (guard.go,svc_portals.go,svc_screens.go,svc_signin.go). - Its menu is the licence business: Licence server, Licence keys, Plans & pricing · Licensed servers, Installs, Downloads · Business overview, Invoices, Discount codes · Help. Server Admin (
srv.) loses those entries once HQ has its address (Packages goes back under Accounts; hosting billing stays); without the address nothing moves (svc_web.go). - The official install switches it on (
portal_super=hq.<domain>; a paid-edition install clears it);hf-agent super-portal on|off(agentsuper-portal-on/off, refused on a paid-edition server); the panel names, nginx, certificate and status loops and the DNS record list (hf-dns-setup) include it. - Test:
TestSuperPortal(the address on the official edition only, the cookie, administrators only, HQ's menu, Server Admin's menu with and without HQ's address, a paid-edition server never answering as HQ). The portals agent check passes. Docs: PRODUCTION.md, the command reference. - Next in step 3: the screens still answer on
srv.by their direct address (only the menu moved); then HQ's own Customers, Orders, Payments (Stripe/PayPal switches), Releases, Website and Trash screens.
Website and customer account: the trial flow and the Control Panel layout (plan step 2)
- The website's header: one Sign in button (to the account) and Free trial (to the plan step) instead of the Sign in drop-down; the partner sign-in is gone from the header, the footer, /access, Community and Pricing ("Become a partner" stays: partners go through contact, then HQ).
- Free trial: plan → your details → the emailed code → signed in automatically. The plan step opens signed out (Sign in on it instead of an email); choosing keeps the plan in the session and asks for the account; its code signs in and goes straight on to the server step (
/account?view=order&step=2) — and so does signing in, for someone who already has an account. Sign-up from the trial says it is step 2 and the trial follows. - The account in the Control Panel's layout (
page_account_shell.go): a left menu in groups (the brand, Buy a licence, Licences, Help, Sign out at the bottom; on a phone it slides in from ☰), a top bar (where you are, who is signed in) and a page head with an icon tile. - Your licences as a table: summary cards (licences, active, trials, the next payment), status tabs (All / Active / Trial / Ended) and a search by key, server or plan (plain links and a GET form — no JavaScript needed), and one row per licence: key, plan with tier and what it includes ("Every feature" or N features, names on hover), status, server (IP, host name, HF Panel version, last check-in, or "Not installed yet"), billing (price a month and paid-up-to, trial end or end date) and Manage / Renew; active licences first. The install command is shown for a licence without a server yet.
- Tests:
TestAccountTrialFirst(the whole trial flow from signed out, then the table, its tabs and its search); the existing account tests still hold (the dashboard keeps the keys, "Every feature", the install command and Sign out). The website builds and its tests pass (13/13). Docs: licensing (Your account, The free trial).
A short licensed install command
- The command on a licence (account page, licence emails, Server Admin) is now
cd /home && curl -o latest -L https://cdn.<domain>/latest/licence/HFL-… && bash latest— the key is in the address and nothing else is needed — instead ofcurl -fsSL -o latest "https://api.<domain>/v1/licence/download?key=HFL-…" && sh latest --licence HFL-…(LicInstallCommand; a server without a domain of its own keeps the long form, withbash). - How: the download server answers
/latest/licence/KEY(only a well-formed key, case-insensitive, kept out of the access log; anything else 404) by passing on to/licensed, as before. The licence server writes the licence's key into the paid installer's new lineHF_KEY=""while serving it (licInstallerWithKey: before the package marker only, the package untouched, an older installer without the line served unchanged, an invalid key never written), and the installer hands it to install.sh as--licence— an explicit--licencestill wins.make-latest.shbuilds the line in. - Also fixed in that installer: it took a server with
config.jsonfor installed and ran an update; an installation that stopped (install-unfinished, or no administrator yet) is now carried on, as in the other installers. - Tested:
TestLicInstallerWithKey; the key hand-over (plainbash latest, with other options, an explicit--licence, an old-style call); the download server's address rule (a valid key, lowercase, a short one,../, the plain/latest); the website builds with the new command. Docs: licensing, the licence API, the website's Download page. - Note: the short command needs the paid installer from this release (with the
HF_KEYline). The one-file setup uploads it on the licence server; until then the old installer is served and asks for the key.
The API's "Access denied" page: white, with the HF icon
- A browser opening the API's address (
api.<domain>) gets a white page with the HF mark (a small lock on its corner) instead of the dark page with a plain lock; the request ID and time sit in a light card. A white-labelled server shows only the plain lock (no HostFog mark), and so does any case where the setting can't be read — when in doubt, no brand. The page still names no product, host or client address.TestAPIHostBrowserchecks every placeholder is filled, the lock without a database, and the HF mark on white for a normal server.
Keys pressed during the build or installation
- Fixed: pressing Enter while a step's progress line was running (build.sh, install.sh) moved the cursor down, so the line was drawn again below — the same step shown two, three or four times — and the Enter was kept and answered the next question (an empty answer). Now, while a step line redraws, keys are neither shown nor read (
hf_keys_quiet, logo.sh); when it stops, whatever was typed is thrown away and the terminal is as it was (hf_keys_back, also on any exit). Ctrl+C still works. Tested in a real terminal: Enter three times and stray typing during a step — the line drawn once and the next question gets only what is typed after the step (without the fix: four rows and an empty answer).
A flaky interface test made reliable
test/pages/licence-plans.test.tsx("changes a licence to a custom bundle …") failed now and then when every test file ran at once on a busy machine, and passed when re-run alone: it waited one tick after "Change plan", so the dialog could still be open; the next click then reopened nothing and the test saved the still-open dialog, set to "custom". It now waits for the request, for the dialog to close, and for the reopened dialog to show the licence's own plan. The page was right: each opening builds the dialog fresh from the licence, and an open dialog covers the page for a real user.
Project cleaned
- Go functions: none removed. A first attempt removed 34 functions a text scan found unreferenced and broke the build: the scan dropped everything after
//on a line, so calls on lines with"https://…"were missed (apiHostPort,mongoURIEscape,siteCertWhy,gitRepoCreate,redirectCreate,webhookSaveAPIare used). All 29 files are back exactly as before. A correct scan still lists 32 candidates, but some tests look methods up by name (node_allow_test.go), so they are left for a compiler-checked tool (TASKS.md). - Build output out of the source (as
.gitignorealready said): the built interfaces inbackend/api/web,backend/os/web,server-files/panel/public/uiandserver-files/mail/public/ui(245 files, 5.5 MB) — they were older than this release's interface changes, and a build without Node.js would have shipped them silently; every build makes them fromfrontend/. - One copy of the billing modules' API client:
integrations/common/HfPanelClient.php; the WHMCS and Blesta copies are made byscripts/build/integrations.sh(sync, now creating their folders) before every test and package, and are in.gitignore. - An empty folder removed (
frontend/files/src/ui). No unused frontend modules were found (every source file is imported). Two hand-run tools nothing pointed to are now listed in DEVELOPING (mailchain.py,make-engineering-guide.py). - Kept as they are, on purpose: fonts and icons in each app (each is served from its own address), test fixtures, each package's LICENSE, the website's and docs' small
company.ts/env.d.ts(two separately built sites), the SDK's publisheddist/, test data logs. ARCHITECTURE.mdhas the project layout as a tree.
The company's own domain as a hosting account
- New
hf-agent company-account [USER] [EMAIL](hf-api admin company-account,cli_company_account.go): the panel's own domain becomes a hosting account, so the company's website files, databases and email are managed in the Control Panel like any account's (File Manager, FTP, Git, DB Studio, Email, backups). It creates the account (default username: the domain's first word, cleaned — with "web" after it when that word is reserved, ashostfogis (the panel's own system user):hostfogweb; contactwebmaster@, elsehostmaster@the domain — one email per login) on the package Company (no limits, no price), or uses the account that already has the domain; the domain's server mailboxes and forwarders become the account's (companyAdopt: the same addresses, passwords and mail in/var/mail/vhosts, now listed on its Email screen; ones already listed are left alone); the DNS zone gets the panel's names as before (PanelRecordsIn). It prints the Control Panel address and the password once. Safe to run again. - The publisher's website moves into that account (
company_website_movein hf-agent): its files to/home/USER/web/DOMAIN/public_html, owned by the account, the website's rules (panel pages on the website's address, short redirects, caching) into the account's site, and the separatehostfog-websitesite removed — nginx is tested first and everything is put back if it refuses. The publisher setup then publishes later releases straight into the account (setup-template.sh), and saves the account-safe rules in/etc/hostfog/website-extra.nginx. - Done during the official installation: the publisher setup makes the company account right after the panel is installed and before it publishes the website, which then goes straight into the account. The summary has a COMPANY ACCOUNT block (Control Panel address, username, password once, website folder).
--company-user NAMEpicks the username,--no-company-accountskips it; a re-run or an update uses the account that already has the domain.tests/check-installer.shchecks the order. - Agent: an account's site includes a root-only extras file,
/etc/nginx/hostfog/site-extra/USER--DOMAIN.conf, that no panel screen edits (removed with the domain). - Tested:
TestCompanyUsername,TestCompanyAdopt(mailboxes and forwarders adopted with quota and send-only, a second run adds nothing, the Company package made once); the website move on this machine as root — accepted (files moved and owned by the account, separate site removed, rules in place) and refused (all put back); the agent checks that build account sites still pass. Docs: the command reference.
Uninstall, then install again: a clean cycle
- Uninstall removes all of HostFog's services and service add-ons: the panel's own (
hostfog-files…), customers' app services (hf-node-*,hf-py-*,hf-rb-*) and the settings HostFog added to other services (php*-fpm,dovecot,pure-ftpd,httpd…*.service.d/*hostfog*.conf). Left behind, they pointed at programs and users that were gone, and a service installed again later could fail to start with them. Also HostFog'stmpfiles.dentries, and the swap setting with the swap file. - Uninstall ends with a check: it repairs a half-done package setup or a package source listed twice (the auto-fixer, loaded before
/usr/local/hostfogis deleted) and says "Ready for a fresh installation" — or exactly what to fix first. - The real-server test runs the whole cycle: a new step
reinstallintests/deploy-test.sh(afteruninstall) installs the customer server again on the same machine and checks: apt reads its sources, no half-done packages, no HostFog services or add-ons left, the installer finishes without needing an auto-fix for uninstall leftovers, hostfog-api runs, nginx's configuration is valid, PHP-FPM has its files and runs, every server package has its configuration files, the PHP repository is listed once, the installation isn't left unfinished, and the administrator signs in and creates an account. - Together with the fixes above (packages purged only when installed, folders of installed packages kept, missing configuration files restored, the PHP repository not added twice, the auto-fixer), uninstall → install again no longer leaves problems behind.
The installer's auto-fixer
- New
scripts/install/autofix.sh, used byinstall.sh,update.shand the newhf-agent autofix [LOG]. Before installing it repairs what would stop the installation: a cut-off package install (dpkg --configure -a,apt-get -f install), the same package source listed twice with different keys (HostFog's copy goes, the other stays), HostFog source files apt can't read (set aside), server packages missing their own configuration files (reinstalled with--force-confmiss), automatic updates holding the package lock (waits up to 10 minutes), a full disk (package cache, old logs, journal), under 2 GB of memory with no swap (a 2 GB swap file), no name server (public resolvers added). - When a step fails, the installer reads the error at the end of the log; when the auto-fixer knows it — the Signed-By conflict, a malformed source,
NO_PUBKEY/EXPKEYSIG/ "is not signed" (the sury PHP and Apache keys are downloaded again; the fingerprint-checked nginx, Redis, PostgreSQL and MariaDB sources are set aside for Software versions), the dpkg lock, "No space left", "not valid yet", "Temporary failure resolving", out of memory, missing configuration files — it fixes it, shows "↻ fixed, carrying on" with what it did, and continues the installation by itself (it resumes with the same answers). At most twice, so an error it can't fix still stops with the log as before. - It never removes or changes a package source someone else set up; each fix is listed on screen and in
/var/log/hostfog-autofix.log. The one-off repairs added earlier (dpkg, missing configuration files) are part of it now. - Tested: 10 checks on a pretend server with apt's real messages (conflicting keys, malformed files — HostFog's set aside, others untouched —, missing keys, an unknown error not claimed, summary and log), and end to end with the real failure handler: the Signed-By conflict fixed and the installation carried on to the end with the same answers.
tests/check-installer.shchecks the wiring. Docs: the install guide ("When something stops the installation") and the command reference (hf-agent autofix).
Fixed: hf-agent uninstall left server software without its configuration files
- The cause: the uninstall removed the server software with one
apt-get purgelisting every package HostFog may install. When one name doesn't exist on the system —ferretdbisn't in Debian's own archive — apt refuses the whole command and removes nothing (the error was hidden). The "all remaining data" step then deleted/etc/php,/etc/nginx,/etc/mysql,/etc/dovecot,/etc/bind… under packages that were still installed, and the next installation found e.g.php-fpm8.4without/etc/php/8.4/fpm/php-fpm.conf(apt never puts deleted configuration files back), so PHP-FPM wouldn't start. - Uninstall now purges only the packages that are really installed (looked up with dpkg-query, glob names included), says so when a removal fails (log:
/var/log/hostfog-uninstall-apt.log) instead of hiding it, and deletes a folder only when no installed package still owns it (rm_unowned; kept ones are listed). - Install after an uninstall stopped with "Conflicting values set for option Signed-By regarding source https://packages.sury.org/php/": the extra PHP versions repository was set up a second time (HostFog's
php.list, keydeb.sury.org-php.gpg) next to one set up separately with its own key (debsuryorg-archive-keyring.gpg, sury's own script), and apt refused to read any source. The installer now uses a sury source that is already there and removes its own duplicate — before apt's first update too, so a re-run gets past it (hf_apt_sury_dedupe). Tested with HostFog's source alone (kept), both (HostFog's removed), the other alone (untouched). - Install now finds installed server packages that lack some of their own configuration files — a server cleaned by the old uninstall — and reinstalls them with
--force-confmissbefore installing (nginx, Apache, PHP, MariaDB, Postfix, Dovecot, BIND, Pure-FTPd, fail2ban, Redis, PostgreSQL …). - Tested: an unknown package name no longer stops the removal; a folder an installed package owns is kept; on sample dpkg data only the package missing its files is reinstalled (a healthy one, an unrelated one, a removed one and an obsolete file are left alone); real dpkg data parses.
tests/check-installer.shchecks all three.
Website: titles, descriptions and keywords for search
- Every page's title leads with what people search for and ends with the brand, in 45–60 characters (Google shows about 60): "Web Hosting Control Panel for Linux Servers", "Hosting Panel Pricing from £5.00 a Month", "Free Local Web Server for Windows, macOS & Linux", "Install a Hosting Control Panel in One Command" … The price and the trial length still come from
site.json. - Every description says what the page offers and why to click, in 135–157 characters (Google cuts at about 160).
- New tags: keywords per page (
keywordsinpages/types.ts; never another product's name — RULES.md, rule 3), robots (index, follow, max-image-preview:large; the 404 pagenoindex),og:locale, the share image's size and alt text, and X (Twitter) title, description and image, so links shared in chats and social media show a full card. - Checked on the built site: lengths, no other product named, the social tags present; the site's tests pass (13/13).
An interrupted installation is resumed, not "updated"
- Fixed: if the SSH connection or the internet dropped during an installation, running the installer again took the server for an installed one (its settings file is written early) and ran an update — which never asks for the admin login and leaves out what the install hadn't reached yet (mail, PostgreSQL …).
- Now
install.shkeeps its answers in/etc/hostfog/install-unfinished(root only) before its first change and removes the file only at its very end. Run again — the same command, orinstall.shwithout options — it says "Resuming the installation that was interrupted", carries on with the same answers (domain, email, company, options), checks the finished steps quickly, does the rest and asks for the admin login.update.shrefuses such a server and says to runinstall.sh;get.sh/latestand the one-file installer runinstall.shagain for it, the one-file installer with the domain and email it was given ("Finishing the installation of HF Panel"). A server that stopped before this mark existed is recognised too: HF Panel's files are there but no administrator yet (the admin login is the installation's last step). tests/check-installer.sh: the answers are kept before the first step and removed at the very end, a run without options reads them back, update.sh refuses, get.sh and the one-file installer resume; the saved answers come back as the same arguments (spaces kept). Proven to fail when the clean-up or the update.sh guard is removed.- Half-installed packages after a disconnect: if the connection dropped while apt was installing, a package could be left without its configuration files (seen:
php-fpm8.4there,/etc/php/8.4/fpm/php-fpm.confmissing, so PHP-FPM wouldn't start). A resumed installation now first finishes what dpkg left half-done (dpkg --configure -a,apt-get -f install), and the PHP step puts back the configuration files of any PHP version that lacks them (reinstall with--force-confmiss; Debian and Ubuntu). Tested on a pretend system: only the broken version is repaired, the installer carries on. - The setup summary: the footer said
hf-server help(nowhf-agent help); the API was shown ashttps://api.<domain>:443/v1(now without:443). - Plain errors from
hf-apicommands: a failing command printed "PHP Fatal error: Uncaught AgentError: …", a leftover wording from the PHP era that sent people looking for a PHP problem; it now prints "Error: …" (the program is Go). - Docs: "If the connection drops during the installation" (installation guide, with
screen/tmux), and the production guide.
An interrupted installation is resumed, not "updated"
- The problem: when SSH or the internet dropped during an installation, running the installer again showed "Updating HF Panel" and "successful", but the steps after the stop (mail, PostgreSQL …) were never done and no admin login was asked for. The one-file installer and
get.shtook the server for an installed one as soon as/etc/hostfog/config.jsonexisted — whichinstall.shwrites early. - Now:
install.shkeeps its answers in/etc/hostfog/install-unfinished(root only, one per line) before its first change and removes the file only at its very end. Run again without options it carries on with the same answers ("Resuming the installation that was interrupted…"): finished steps are checked quickly, the rest are done, and it asks for the admin login. The one-file installer ("Finishing the installation of HF Panel …", the domain and email read back) andget.shruninstall.shagain for such a server;update.shrefuses it and says how to finish. A server that stopped before this release (no mark) is recognised too: HF Panel's files are there but it has no administrator yet. - Also from that output: the command line printed its errors as "PHP Fatal error: Uncaught AgentError: …" — a wording left from the PHP version; it now prints "Error: mail is not installed on this server" (
cli.go). The setup summary saidhf-server help(nowhf-agent help) and showed the API ashttps://api.<domain>:443/v1(now without:443). - Checks in
tests/check-installer.sh(the mark is kept before the first step and removed at the very end, the answers come back as the same arguments, update.sh refuses, get.sh and the one-file installer resume); proved to fail when the fix is taken out. Install docs: "If the connection drops during the installation" (andscreen/tmux).
Live server figures
- Server › Resource usage opens with a Live card: CPU, memory, load (with the number of CPUs), network in / out and disk read / write, and a chart of CPU and memory over the last 5 minutes, all updated every 2 seconds. Point at the chart to read a moment; Pause holds the figures. The administrator's Overview has the same card without network and disk. The card reads only while its tab is visible and asks only for readings it doesn't have; with reduced motion nothing animates.
- How it is measured (
internal/panel/live_stats.go): hf-api reads Linux's own counters —/proc/stat,/proc/meminfo,/proc/loadavg,/proc/net/devand/proc/diskstats(whole disks only, so partitions aren't counted twice; loopback left out) — every 2 seconds into a 5-minute buffer. It needs no agent and no root, starts when someone opens the card and stops after 5 minutes without a viewer, so an unwatched server pays nothing. A counter that goes back (a reboot) gives 0, never a spike. The figures come from the same administrator-only route,GET /api/v1/monitor?live=1&since=<ms>; resellers and customers don't get them. A system without/procanswersavailable: falseand the card says so. - Tests:
TestLiveParsers,TestLiveSampler,TestLiveRoute(Go);test/liveStats.test.tsx(figures, since=, Pause, the dashboard form, the unavailable case). 9 new texts in 20 languages; the Resource usage and Overview docs explain each figure.
Project documents
- Six short documents next to
CLAUDE.md, linked from it: ARCHITECTURE.md (products, the request path, addresses, data, editions, frontends, HF Server, build and release, test layers), DESIGN.md (brand, tokens, layout, components, behaviour, writing), PRD.md (vision, users, requirements per product, pricing, non-functional requirements, out of scope, success measures), RULES.md (the hard rules and every security, code, test, shell, release and docs rule in one place), MEMORY.md (state, decisions with their reasons, lessons from the bugs found), TASKS.md (now, next, backlog, done). Each links to the detailed docs instead of repeating them; no company name or domain typed in, and no other products named.
Fixed: a broken /usr/bin/php link (Rocky/Alma), and the update's webmail link
- Rocky / AlmaLinux / CentOS Stream: the layer made
/usr/bin/php(→ the defaultphp<v>) only when-esaid nothing was there — but a broken link (left by a removed PHP's alternatives entry, for example) also says "nothing" while the name is taken, soln -sfailed andphpdidn't work on the command line. It now replaces a missing or broken one (ln -sfn); a working php of the server's own is still never touched (agent/lib/02-os-rhel.sh). Found byos-agentpart 2 on a Debian 13 build machine with such a leftover link; reproduced here with one (failed before, passes now). update.sh: the same pattern for the oldwebmail→maillink — andupdate.shruns withset -e, so a brokenwebmaillink would have stopped the update. Fixed the same way.
All docs checked
- The user docs (docs.<domain>, 374 pages) were built the way the release does and checked: every internal link, page anchor and image works; the docs' own tests pass. Fixed:
- 59 instructions still described adding on the page ("Under New FTP account, type…", "Fill in the form at the top", "the form under the table") on screens that now use a New card and a dialog: they now say "Click Create on the New FTP account card. In the dialog, …", with each card's exact title (New Git repository, New cron job, New mailing list, New PostgreSQL / MongoDB database, Invite a team member, New Node.js app, Add a MIME type …), Add where the card's button says so (Add domain, Add monitor), the account and mailbox wizards' steps, and editing a package or a Node.js app in its dialog.
- 29 places still used the old screen names: Malware & web firewall → Security overview, Top sites by traffic → Top websites, Server & updates → System updates (links and screenshot captions).
- The developer docs (34 files): every link to a file works; LICENSING-MODULES' example is ×2.5 for Unlimited; the engineering guide's mailbox-reset row describes Mail's own steps.
- A scan for anything else this release changed (the old command names,
hf-agent deleteand the uninstall options, an API port, the server package's old name, the licence sign-in link): none left.
Docs brought up to date
- Install HF Server (desktop docs): the Windows setup's Choose Components page (what is always installed, what is ticked by default), the components downloaded and installed during setup with their progress, what happens when a download fails (installed at first start), and the first start on Windows. Command line:
hostfog install <component>…. - Developing (
docs/DEVELOPING.md): the build's steps in order, with the new "Server agent (checks)" step and what to upload to a VPS (releases/install.sh); in "Before you commit", the agent-check runner (as root: every check; the Debian 13/tmpnote; skips on machines that can't overlay/), the break-it test, fuzzing and the load test.
Agent checks on a build machine run as root
access-agent: "utarget-put answers OK" failed on a machine without rclone.utarget_putfirst checks that rclone is installed (the server's installer installs it), but this check replaces the storage script with a fake one, so rclone isn't used: the check's agent stands in for that test while the fake runs. Its default helper path ishelpers/hf-backup2.py(it pointed athf-backup2.pynext to the agent).os-agentpart 2 (a pretend Rocky Linux root made by overlaying/) failed on Debian 13: trixie keeps/tmpin memory (tmpfs), and an overlay shows only the root filesystem, so the check's own script (in its/tmpwork folder) was missing inside the pretend root — "part 2 did not run". The work folder and the source folder are now bind-mounted into it (also covers a separate/homepartition); reproduced with the work folder on tmpfs (old: fails exactly so; new: all good). A machine that can't overlay/at all (WSL, some containers) says "SKIP part 2 (…: <the reason>)"; any other stop is still a failure.run-agent-checks.shshows the lines after each failure too (the reason), not only the FAIL line.- This build reached step 9 with everything before it passing — every Go test included, with Mail's own reset,
linkLabelsand the new Unlimited price.
Lower price for unlimited servers
- The Unlimited tier's multiplier is 2.5 instead of 4, so the Business plan (every module, unlimited accounts) costs £72.00 a month instead of £115.20 — below cPanel Premier plus WHMCS, and every bundle on the Unlimited tier costs 37.5 % less; Solo (×1) and Pro (×2) are unchanged. Changed together so the licence server and the website calculate the same price:
DefaultLicCatalog(licfeatures.go),site.json, the shared recorded prices (frontend/site/test/bundle-price-fixtures.json, checked by the website's tests and Go'sTestLicPriceFixtures),TestBundlePrice, and the licensing docs. A licence server whose prices were already saved in Server Admin › Business › Plans & pricing keeps them: set the Unlimited multiplier to 2.5 there.
Every sign-in checked
- Checked in a browser (built interfaces, the server's answers simulated): the sign-in of Server Admin, the Reseller Panel and the Control Panel (each names its panel; labelled fields with password-manager hints; the cursor starts in the first field; Enter signs in; a wrong password says so; "Forgotten your password?" opens its own page with a way back; the new-password page asks twice; language links; fits a phone; dark mode readable; no script errors), and the same for Mail (with its new reset), Files and DB Studio. All pass.
- Fixed — the sign-ins written by the server (your website's Your licences
/account, buying and managing a licence, the Partner portal,/order…): their labels weren't linked to their fields, so clicking a label didn't put the cursor in the field and screen readers announced the box without its name. Every label written right before its field is now linked when the page is sent (linkLabels,page.go;TestLinkLabels). - Fixed — the website's Sign in page (
/access/) said "We email you a sign-in link" for Your licences; it signs in with email and password now. - The website's Sign in menu: Your licences, Partner portal and System status, all pointing at the right addresses.
Website checked
- Built (all 25 pages prerendered) and crawled in a browser: every page answers, every link and image inside the site works, no script errors, no page scrolls sideways on a phone, dark mode readable; the site's own tests pass (13/13). Fixed: the Features page's Security section showed a broken picture — it asked for
mail-2fa.webp, the file iswebmail-2fa.webp(already so in the original sources).
Every drop-down menu has the panel's look
- Select boxes were styled only inside a form field (
.field select); elsewhere — a table cell (Two-step sign-in's Required / Optional), a toolbar, a card heading — they showed the browser's plain box. Now every select has the panel's look by default (styles/ui.css, under:where()so a select with its own style keeps it): border, rounded corners, padding, the brand-colour focus ring, hover, disabled and dark-mode colours, one grey chevron for every drop-down (lists showing several rows keep theirs), and the open list's options in the card colours where the browser allows it. Form fields keep their width and height. Checked in light and dark, in a table and in a dialog.
Every app has its own forgotten-password steps
- Mail now resets a forgotten mailbox password on Mail's own address, like Files and DB Studio, instead of sending people to the Control Panel: the address → a 6-digit code emailed to the mailbox's recovery email (or the email's link, which opens Mail at the next step,
?reset=…) → the new password, then back to the login with the address filled in. It is the panel's mailbox reset, with the same limits, lock-outs, password rules and "password changed" emails (panel/mail_forgot.go→mail.Forgotter,mail/forgot.go:/api/v1/forgot,forgot/code,forgot/link,forgot/password; the start-up data saysforgot). A mailbox without a recovery email still gets nothing; its owner is told, as before. 14 new Mail texts in 20 languages;TestForgotRoutes,test/forgot.test.tsx; a compile-time check that the panel's bridge fits Mail's interface (main.go). The Mail docs page explains the steps. - Checked: Server Admin, the Reseller Panel and the Control Panel each have their own (a reset asked on a panel only finishes there, for that panel's logins; the email's link opens the login's own panel), as do Files, DB Studio, team members and the website's account page.
Every action updates the page it was made on
- One rule for all three panels (
handle()inPanelApp.tsx): after any action that worked — a button, a dialog's Save or Create, a row's Delete, a switch, a live job — the page's data is read again, so the change shows at once without reloading the browser. Before, only answers that pointed back to the page did that; an action that only answered "ok" left the page as it was. A failed action keeps the page as it is, so what was typed stays to be corrected; the message at the top always stays. A page that keeps its own live state can passnoReload(ActOptions). Checked in the built interface (New FTP account, New cron job: the action, then the page's data, and the dialog closes); tests intest/app.test.tsx.
The screen refreshes when a live job's terminal closes
- After the live output dialog closes (refreshing tunnels and certificates, installing or updating apps, WordPress tools and every other
_liveaction, in Server Admin, the Reseller Panel and the Control Panel alike), the screen reads its data again, however the job ended — done, finished with errors, or closed by hand — so what the job changed shows at once; the message at the top stays (act()inPanelApp.tsx). Before, it only did when the server's answer asked for a reload. New test intest/app.test.tsx. - The last lines no longer show twice ("0 tunnel(s) ready" twice): the closing read of the log could run alongside a regular one, both starting at the same place. The dialog now reads the log one request at a time (
LiveDialog,ui/Dialogs.tsx).
Installer summary box
- No row runs past the box any more (
hf_fline,scripts/install/logo.sh): a value too long for it is cut, counting characters not bytes, and ends in "…". The paid-edition note on an official package is short enough to fit and says what to do: "paid edition · official: HOSTFOG_EDITION=official". - CPU: the model's bracketed detail ("(Haswell, no TSX, IBRS)") is left out instead of being cut mid-word.
- Company: the official package installed with
HOSTFOG_EDITION=officialsuggests the publisher's own name from the build (HostFog Ltd, e.g. "HostFog Ltd") instead of the domain's first word.
Fixed: the panel's Go tests (their first full run)
- The account page tests (
page_account_test.go) took "Your licences" as the sign of being signed in, but the sign-in page says "Your licences, billing and servers." too, so "still signed in after sign out" and "after a wrong password" could never pass: they look for the Sign out button now (accountPageSignedIn). The trial's dashboard shows the install command HTML-escaped, and the test now compares it escaped. TestPathAddressesIntegrationsent its requests to the API handler (405 for anything not an API call); it goes through the panel's page handler now (ServePanel, viateamCall).TestSuperAdminRoutesOnlyOnOfficialexpected the old Business layout (Packages, Invoices and Licences as tabs); it checks today's: every business screen an entry of its own on the official edition, Overview first in the paid edition's Billing group.TestAPIHostBrowserrefused any "{" in the "Access denied" page, but the page's CSS has braces: it checks that the{id}and{at}placeholders were filled.TestCompanyPortsDefaultsexpected an ownapi_port(2087) to vanish from the API's address; as intended, a server that keeps its own API port shows it, and 443 shows none (both checked now).TestOpenAPIDocsCopy: the API description's generator still wrote the API's address with:443, while the docs copy had been changed tohttps://api.yourdomain.com/v1; the generator writes it without a port too.
Fixed: Mail's TestTextToHTML with newer Go
- The tests that check the Go port against PHP's recorded answers find each answer by a hash of the test's input as JSON. The answers were recorded with a Go whose
encoding/jsonwrote an invalid UTF-8 byte as the escape\ufffd; newer Go writes the replacement character itself, so the same input hashed differently andTestTextToHTML(input"x\xffy") found no answer.phpGolden(mail, panel, sieve) now tries the old form when the key it computed has no answer (goldenFile). The Go code under test was right all along; this test had never run before.
Fixed: the panel's Go code didn't compile
apihost.go: the edit that gave browsers on the API's address an "Access denied" page (with a request ID) had cut off the end of the file, removingapiHostTunnelGet(the old/v1/tunnel-getdownloads) andapiHostJSON(the API host's JSON answers, also used bypayhost.go), which also leftnet/urlunused. Both are back exactly as before.page_licence.go: the redirect of a licence bought through the account's wizard read the licence withthanks.Str(…), butthanksis a plain map there: nowAsString(thanks["email"])/AsString(thanks["key"]).
Fixed: the panel's type check
Wizard'sonFinishwas typed(e: FormEvent) => void | Promise<void>, stricter than its callers (the account, domain and mailbox wizards passform.submit, which returns the save's answer; New database's handler takesFormEvent<HTMLFormElement>), sonpm run typecheckstopped the build. It is now(e: FormEvent<HTMLFormElement>) => unknown(its result was never used), and the wizard's own submit handler takes the form's event.
The build tests the server agent too
- New build step "Server agent (checks)" (after the API's Go tests):
tests/checks/run-agent-checks.shruns all 24 checks of the root agent (tests/checks/*-agent.sh: accounts, mail, DNS, security, malware scan, backups, isolation, limits, Git deploys, templates, WordPress updates …), each in a throw-away folder or user. A check that needs a tool the machine doesn't have says "skipped"; one that needs root is listed as not run when building as an ordinary user (sudo bash tests/checks/run-agent-checks.shruns them all); any failure stops the build. - Fixed in the checks: six places tested nothing because the helper's file name was missing (
"$AG/helpers/"instead ofhf-scan.py,hf-backup2.py,hf-cronwrap— in malware-agent, support-agent, access-agent, sitefw-agent); the malware scan, email-to-ticket scan and cron wrapper checks now run for real and pass.devflow-agentandtemplates-agentexit 2 (needs root) like the others and say "skipped" when rsync or php is missing;confhist-agentandsitefw-agentpass as an ordinary user too (only root can make a file root's, and "never runs as root" can only be tried as root).
Windows setup downloads and installs the chosen components while it installs
- After copying HF Server, the setup's last step runs
hostfog install --from-setup: it downloads and installs what was ticked on the Components page (Apache, Nginx, PHP, MariaDB …) right away, each checked against its checksum, and the setup's details list shows every step and the download progress in 10 % steps (ShowInstDetails show). It needs an internet connection: whatever can't be installed then is written back toinstall-components.txt, and HF Server installs it when it opens (as before). hostfog install <component>…(new): the same without the window, for scripts too (backend/os/app_first_install.go; the app's log lines go to the terminal throughApp.echo).TestCliInstallArgscovers its arguments.
Fixed: editing a Python or Ruby app, and the tests the build stopped on
- Editing an existing Python or Ruby app (
?edit=<name>, the app's Settings) showed nothing: their form moved into the New dialog, which didn't open for editing (Node.js already did). Both now open it with "Edit Python app" / "Edit Ruby app" (editParam="edit"); checked in the built interface. - The test helper sets the real address:
renderPage/renderWithwith aurlnow put the browser's address there too (setAddress,test/helpers.tsx), since screens read?new=and?edit=from the address; without a url it is/, so one test's address can't leak into the next. - Ten panel tests looked for the add forms inside the page, but dialogs are drawn at the top of the page: cron, Git, handlers, Node.js, Python, Ruby, help articles and the feature-list alert now find the form in its dialog (by the dialog's title) or in the document.
Tests: unit, integration, test-first fixes, break it, load
- Break it (
tests/ui/breakit-dialogs.py, Playwright against a running panel): a script typed into a field, Enter on a middle step, a double-click on Finish, opening and closing fifteen times, Tab past the last field, a drag ending on the backdrop, 5000 characters, browser Back, a 360 px phone. It found two bugs, fixed test-first:- Enter in a field of a wizard's middle step did nothing (a browser only submits on Enter when the form has a submit button; Next isn't one): Enter now moves to the next step (
ui/Wizard.tsx;test/wizard.test.tsx). - Tab left the add dialogs (keyboard and screen-reader users ended up on the page behind):
NewDialogkeeps Tab and Shift+Tab inside, and only the dialog on top acts on keys (ui/NewDialog.tsx; newtest/newDialog.test.tsx, which looks for the form's Create button inside the dialog — the New card has one too).
- Enter in a field of a wizard's middle step did nothing (a browser only submits on Enter when the form has a submit button; Next isn't one): Enter now moves to the next step (
- Unit tests:
test/bundlePrice.test.ts(the licence price formula: tiers, bundle discounts, unknown keys, rounding like the server); GoTestEmailInUse,TestLicFeatureGetsAndPlanIncludes(session_review_test.go). - Integration test:
TestPathAddressesIntegrationsends real requests through the panel's handler — a screen's path, the same screen under another group and the classic/?r=answer alike; a non-screen path and a three-segment path are 404;/is the dashboard. - Fuzz tests (Go's own "break it": random input, never a panic, only valid answers):
FuzzRouteOfPath(the address parser),FuzzReadFirstInstall(the setup wizard's component list).saTestAPItakestesting.TBso fuzz targets can use it.go testruns their seed cases;go test -fuzz=FuzzRouteOfPath ./internal/panel/fuzzes. - Load test (
tests/load/loadtest.py, standard library only): simultaneous users with kept-alive connections over public pages, screens and API calls; prints requests per second, p50/p95/p99/max and errors per address, and exits 1 over the error-rate or p95 limits (for CI).
One command removes HF Panel: hf-agent uninstall
hf-agent uninstallnow removes HF Panel and everything it set up on the server — every hosting account (websites, databases, mail, DNS zones, backups), the server software, database files, certificates, logs, package sources, the swap file and the firewall rules (SSH stays open). It runshf-uninstall --complete: you typeDELETE EVERYTHING, and nothing is saved.hf-agent deleteand the--purge/--packagesoptions are gone from the command (the uninstaller keeps them for the test scripts). The command's help,hf-uninstall --help, the panel's Settings card, the docs (Uninstalling, Server commands, Settings, FAQ, release notes, the website's guide) and the engineering guides say so;tests/deploy-test.shrunshf-agent uninstallon both test servers and checks everything is gone and SSH still runs.
Windows setup asks what to install
- The setup wizard has a Choose Components page after the licence: HF Server, then Web servers (Apache — always —, Nginx), Programming languages (PHP — always —, Node.js, Composer), Databases (MariaDB, PostgreSQL and Redis unticked by default) and Tools (phpMyAdmin), each with a description. The setup file stays small: the ticked ones are written to
install-components.txt, and HF Server downloads and installs them, checked against their checksums, the first time it opens (backend/os/app_first_install.go, logged on the Control panel; one that fails can be retried on the Components page).TestReadFirstInstallcovers the list.
The Windows installer has its own look
- The HF Server setup wizard (NSIS,
packaging/windows/installer.nsi) now shows HF Server's own artwork: on the welcome and last pages a side panel with the HF mark, "HF Server — Local web server" and the HostFog name at the bottom (welcome.bmp), and on the other pages the HF Server logo at the top right (header.bmp). Both are drawn at twice their size so they stay sharp on high-DPI screens (ManifestDPIAware);scripts/build/software.shcopies them next to the script. The pages and wording are unchanged.
Every dialog checked and given one look
- All 41 add dialogs of the three panels were opened in the built interface, photographed and checked for size, sideways scrolling, fields sticking out and a missing footer. One look for every dialog (
Dialog,Wizard,NewDialog, confirmations): a title bar with a line under it, the content, and a grey footer line for the buttons flush with the dialog's edges (the step wizards' Back / Next too). Add forms in a dialog are 640 px wide. - Fixed on the way: a page's card shown inside a dialog drew a box inside the box with a second title (Add a record, Invite a team member, Import accounts, Create licence) — now just its content; full-width submit buttons (New mailing list) are ordinary buttons on the right; a field made of parts in a row (name @ domain) no longer wraps; and forms the server makes for a new item (
?edit=new,?new=1: New feature list, New article) were titled "Edit …" — they now say "New …".
The New … dialogs look like dialogs
- A screen's add form in a
NewDialogis now laid out for a dialog (styles/ui.css,.new-dialog-*): a title bar with the close button on the right and a line under it; the fields in two even columns (one on a phone), with the inline width limits of the page layout lifted; empty hint lines and the form's old card heading hidden (the dialog's title says it); and the save button alone at the bottom right on a grey footer line, flush with the dialog's edge — the same ending as the step wizards. Checked on FTP account, Web Disk login, Reseller, Cron job and SSH key.
Fixed: dialogs that never showed, and the tests the build ran into
- Dialogs opened from a New card could be invisible: the card a form used to sit in is hidden once the New card takes its place, and the dialog was drawn inside that card, so it opened hidden (Email's New mailbox, New database, FTP, cron …).
DialogandNewDialognow draw at the top of the page (a React portal into<body>), whatever their place in the page; checked in the built interface — every New card's dialog is on screen. The close button of a NewDialog sits on the right of its title. - The ten tests
build.shstopped on follow the new layout: they open the New account / New announcement dialog before looking at its choices, the feature-list editor test opens with?edit=new, a price shown both in a wizard's summary and in its last step is matched withgetAllByText, the licence wizard stays on step 1 while the email and IP are empty (the browser's own message, not a custom one), and the super-admin test no longer looks for the Business tabs (they are sidebar entries).
Fixed: HF Server's Windows icon
- The Windows exe (title bar, taskbar, Explorer) still showed an old icon — three bars and a dot — because its icon comes from
backend/os/rsrc_windows_amd64.syso, which only go-winres rewrites, and builds without go-winres kept the old file. The four icon images in that file (256, 48, 32, 16 px) are now the current HF mark frompackaging/winres/(replaced in place, with the resource entries' and the icon group's sizes updated; read back and checked), andscripts/build/software.shinstalls go-winres (pinned v0.3.3) when it is missing, so the file is made fresh on every build.
hf-agent is the only server command
- The old names
hf-server,hf-adminandhf-panelare no longer installed as links:install.shandupdate.shmake only/usr/local/sbin/hf-agentand remove the old links from servers that had them, andupdate.shmoves cron lines that still called an old name ($HF/bin/hf-server licence sync…) tohf-agent.uninstall.shremoves every old name too;tests/check-installer.shknows the new deletions.hf-agent help: api-portal now showshttps://api.<domain>/v1(the API has no port of its own). The desktop app's downloads keep theirhf-server-…names.
Fixed: the Linux checksums step
scripts/build/software.shwroteSHA256SUMS.txtfromHF-Server-* hf-agent*: the command rename (hf-server → hf-agent) had also changed this file pattern, which means the desktop app's Linux downloads (hf-server-<v>-linux-<arch>.tar.gz,hf-server_<v>_<arch>.deb), sosha256sumfound nothing and the build stopped. Back tohf-server*. Every other place the rename touched was compared with the original sources: this was the only download name it changed.
Fixed: the desktop app's interface didn't build
frontend/os/vite.config.tsusedsharedResolve/sharedFswithout importing them from../shared/vite(its Vite import line differs from the other apps', so the import was never added), andbuild.shstopped at "Software interface (tests, build)" withReferenceError: sharedResolve is not defined. Added; all seven apps' Vite configs and the five apps' sources were bundled as a check.
Code review: unused code removed
- Go (a scan of every package for unexported functions and variables nothing refers to, tests included): 17 functions removed — mail:
firstCol,intsToAny,likePattern,trimSpaceRight,isOneOf,listValues,sieveDefaults; panel:isV1Path,deliverShellArg,phpQueryPairs,statusHostRequest,kbCategory,shellAPI,unusualQuery,(*tokenReq).bodyForm; desktop app:poolPorts,apiError,composerCreate— and 5 variables (the four unused vCard patterns,dnsSplitRx, and with itscreen_dns.go'sregexpimport). A second scan found nothing left unused by these removals. Kept: the protocol constant tables (curl error codes, MongoDB opcodes, the IMAP status fields), which document the whole table even where one value is unused. - TypeScript: every app type-checked with
noUnusedLocalsandnoUnusedParameters: nothing unused; one exported helper nothing imports removed (hashFor, desktop app). - Shell:
pkg_remove(agent/lib/01-os.sh) anddbt_db_of(33-dbtools.sh) removed; helpers called by a built name (apps2_setup_$app,apps3_config_$app) and the OS layer's functions the installer uses were checked and kept.tests/checks/os-agent.shandsecurity-agent.shpass. - Python: nothing unused (the e2e flows are registered by
@flow).
The server package is hf-panel-<version>.zip
build.shnow saves HF Panel's server package asreleases/hf-panel/hf-panel-<version>.zip(it washf-server-<version>.zip, the same name as the HF Server desktop app's downloads). On the download server it is stillhostfog-<version>.zip, the name installed servers ask for (get.sh, update.sh, self-update), so nothing changes for them.tests/deploy-test.shfinds an older release under any of the three names; the build guide, PRODUCTION.md and the engineering guide follow. The desktop app's downloads keep their names (hf-server-<version>-linux-amd64.tar.gz,hf-server_<version>_amd64.deb).
Clean-up
- 284 interface texts nothing uses any more removed from the panel's dictionary and all 20 languages (texts of screens and buttons replaced over time;
_keys.json8341 → 8057). Texts that look like fragments glued together at run time were kept.tests/check-lang.allowrenumbered to the new text numbers;check-lang.pypasses. - Dead code: the empty
SaTabs(and its nine uses),useOfficialandFeatureGrid(used nowhere) removed. - Docs: the desktop app's "HTTPS and phones" page showed a byte-for-byte copy of the Sites screenshot; it now uses
sites.webpitself (the screen it describes) and the copy is gone. The engineering guide no longer describes the Business tabs. - Looked at and kept on purpose: each app's own copy of its fonts and licence texts (every app is built and served on its own), the installer test fixtures that are identical between versions, the Windows icon set
packaging/winres(go-winres reads it), the built interfaces committed for the Go programs to embed, and the billing modules' synced copies ofHfPanelClient.php.
Cleaner screens
- The New card is one compact row: an icon tile, the title with its line under it, a pill if any ("1 / 25 used"), and a short Create (or Add) button on the right — no head line, and the title is no longer repeated on the button (the button keeps the full name for screen readers:
aria-label). Several New cards share one row (Status page: Report an incident · Plan maintenance). On a phone the button takes the full width. - No half-empty rows: when a card leaves a two-column grid (its form moved into a dialog), the card left beside it takes the whole width (Quotes, Cron jobs …).
- Support tickets: the old "New ticket" toggle bar is gone; the New card opens the form.
Every list screen: the list, and a New card that opens the add / edit dialog
- Editing in the same dialog:
NewDialogalso opens for editing — while the address has the screen's edit value (?edit=<id>,?e=<id>,?kind=…;editParam) or the screen's own "editing" state is set (editingNow/onEditDone), titlededitTitle, and closing it goes back to the list. Screens whose blank form comes from the server open it withnewParams(?edit=new,?new=1). - Now the same layout (heading → New card → list): packages, Node.js apps, feature lists, DNS templates, help articles, DNS records, commission rules (affiliates), uptime monitors, own backup storage, backup destinations, discount codes, status page (Report an incident, Plan maintenance), licensed servers, Apache handlers and MIME types, announcements — on top of the 30 screens before. Their old New / Add buttons in the headings are gone; the tests that clicked them use the card's button or the dialog's form.
- Left as they are on purpose: catalogues where each item has its own Install button (Apps, Docker apps), and actions that belong to one row of a list (staging copy of a site, IP pool of a server, SSL for a domain), domain search (Domain names & SSL), the transfer and import flows, and settings pages (Abuse desk, Email to ticket).
- Preview:
hfpanel-all-panels-preview.htmlshows every screen of the three panels, built from the current source and rendered on the sample data of their tests.
One layout for every screen that creates something
NewCard(ui/NewDialog.tsx): the "New …" card — icon, title, the button, an optional line and a pill on the right — placed right under the screen's heading (and under its tab bar, if it has one), whatever the page writes it. Every screen that creates something now starts the same way: heading and description, then its New card, then its content.NewDialoguses it; Accounts (with its "accounts left" pill), Email (with "used / limit"), Domains, Databases, Licence keys and Plans & pricing switched to it — Plans' New plan button moved out of the heading into the card.- Domains: Add domain is the New card; the tab card below keeps only Add a subdomain and Park a domain.
- Checked in the built interface on eight screens: the card sits right under the heading (under the tabs on Email and Databases), and emptied cards are hidden.
A "New …" card at the top of each screen
- Every screen whose add form opens as a dialog shows a New … card right under its heading — the card's icon and title, and the button that opens the dialog — the same way Accounts shows New account (
NewDialogputs it there;iconandhintset the card). The card the form used to fill on its own is hidden, so no empty boxes are left. Checked in the built interface on FTP accounts, Cron jobs, SSH keys and Team.
Fixed: Next on the step before the last saved the wizard
- React reused the Next button as the Finish (submit) button, so the click that opened the last step also submitted the form — a New account was created from step 3. The two buttons now have their own keys (
ui/Wizard.tsx);test/wizard.test.tsxchecks that Next onto the last step doesn't save, a required field keeps its step, and Finish saves once. Found by running the built interface in a browser.
Add forms open as dialogs across the panel (30 screens)
ui/NewDialog.tsx: a screen's own add form shown in a dialog, opened by a button or the New menu (?new=<name>), closed by itself once the save succeeds (ACT_EVENT, fired by PanelApp'sact()after every action). The form inside is unchanged — same fields, save and field errors — and stays mounted while closed, so the existing tests that submit it still work.- Now dialogs: reseller, staff login, server, notification rule, server mailbox, support ticket, FTP account, Web Disk login, cron job, mailing list, SSH key, protected folder, PostgreSQL and MongoDB database, Git repository, Python and Ruby app, copy a website, copy to another account, website from a template, team invite, Dynamic DNS name, Import accounts, New quote — with the step wizards already done (account, plan, licence, mailbox, domain, database), 30 screens. All of them are in the New menu.
- Left as they are for now, because their form is also the edit form or appears once per item: packages, discount codes, uptime checks, Node.js apps, staging, app and Docker installs, handlers, IP pools, backup destinations, announcements, SSL, domain registration, transfer, DNS records.
Email account, Add domain and New database open as dialogs
- New mailbox (Address → Password & size), Add domain (Domain, with the account for staff → Folder) and New database (the quick database: name, and the account for staff) are dialogs opened by their button or the New menu (
?new=mailbox,?new=domain,?new=database); the inline forms are gone. A one-step Wizard is a plain form dialog without the step strip. - Path addresses only once the server sends the groups:
href()writes the classic/?r=<route>untilBoot.groupsarrives, so a panel that doesn't answer paths yet keeps working links (the router test covers both).
The New menu, and New account as a wizard
- Each screen's add form opens from its own New … card (below); there is no separate create menu in the top bar or the sidebar. A link with
?new=<name>opens a screen's dialog directly. - Wizard keeps every step in the form (hidden when not shown), checks a step with its inputs' own rules (required, pattern, email …) before Next, and after a save the server refused opens the step holding the field with the error.
- New account (Server Admin and Reseller Panel) is a four-step wizard dialog: Customer → Package & server → Domain → Password & welcome, opened from a New account button or the New menu; the inline form is gone. New plan and Give a licence open from the New menu too. Texts translated in 20 languages; the accounts test walks the wizard.
Licence keys: "Give a licence" is a wizard
- The free-licence form on Licence keys is now a three-step wizard opened from a Give a licence card: 1 Who (email, server IP, note), 2 What it includes (every feature, one of your plans, or panels and modules ticked one by one with Select everything / Select all, plus the account tier), 3 Send (email the key and install command, and a summary of what the licence holds). The
freeaction takes the optionalplan,featuresandtierand applies them withLicSetFeatures, the same as Change plan (TestLicencesFreeWithPlan); the test of the page walks the wizard.
A wizard dialog, first used for new plans
ui/Wizard.tsx: a reusable step-by-step dialog for the panel — a numbered step strip (done steps can be clicked to go back), one step's fields at a time, Cancel / Back / Next / Finish, a step that checks itself before Next, Enter on a middle step goes forward instead of saving, and an optional running summary under the strip. It wraps the page's own form and save, so field errors and tests keep working.- Plans & pricing › New plan / Edit plan is the first wizard: 1 Basics (name, key, description), 2 Select panels & modules (one card per feature with what it unlocks and its price; Select everything at the top and a Select all box per group, half-ticked when only some are chosen, with counts — plus single ticks; the account tier; at least one panel is required before going on), 3 Price & visibility (worked-out or fixed price, order, website, most popular, and a line summing the plan up). The worked-out price stays in view on every step. Next candidates: New account, New reseller, Add domain, Import accounts.
Plan builder: what each choice gives you
- In the licence shop, the buy wizard and Change plan, every ready plan shows Includes: its panels and modules, every panel and module shows what it gives — how many panel screens it unlocks, what the installer sets up for it (Postfix and Dovecot for Email, BIND for DNS, PostgreSQL and MongoDB for DB Studio, Docker, the HF Tunnel server, rclone …) and whether it opens API access — and a Your plan includes box above the price lists the chosen features and the services that will be installed, kept current as you tick (
page_licence_plans.go licFeatureGets,licPlanIncludes,licServiceNames;TestAccountWizardTrialchecks it).
Shared front-end components: `@hf/shared`
- The helpers the front-end apps had copied between them —
fieldErrors.ts(5 copies),a11y.tsanda11y.css(3),fieldErrors.css(3),Icon.tsx,icons.ts,Lockup.tsx,i18n.tsand the small apps' stylesheet (2 each),keys.ts(2), the test helperaxe.ts(4) — are one module each infrontend/shared/, imported as@hf/shared/lib/fieldErrors,@hf/shared/ui/Icon,@hf/shared/styles/a11y.css…: 30 copies removed, 50 files' imports rewritten. Each app stays a build of its own (ownnode_modules, own lockfile):frontend/shared/vite.tsgives its Vite config the alias and makes the packages the shared files use (react, axe-core) resolve from the app's node_modules, and eachtsconfig.jsonmirrors that withpaths. The panel keeps its ownIcon.tsx,icons.tsandi18n.ts(they differ from the small apps'). Verify in each app:npm run typecheck && npm test && npm run build.
A shorter, clearer sidebar
- Collapsible groups. Each sidebar heading (Server, Business, Accounts, Hosting, Email, Security, System, Support …) folds: the group of the screen you're on is open, the others closed until you open them (remembered in the browser). Ninety entries no longer scroll past; Ctrl+K still jumps to any screen (
shell/Shell.tsx SideGroups). - Clearer names where two entries read alike: Malware & web firewall → Security overview (next to Malware and Web firewall control), Top sites by traffic → Top websites (next to Top accounts), Server & updates → System updates (next to HF Panel updates), Licences → Licence keys (next to Licensed servers), and, from the previous entry, the Business group opens with Overview and the licence server's dashboard is Licence server. Routes, addresses and docs links are unchanged; the labels are translated in 20 languages.
Business / Billing group: Overview first
- The Business screen (accounts, revenue, churn and plans over 6, 12 or 24 months) is now called Overview and is the first entry of its group — Billing on a paid server, Business on the official edition. On the official edition the licence server's own dashboard (route
superadmin) is called Licence server instead of Overview, so the two no longer share a name. Docs and the page titles follow.
Business group: every screen in the sidebar, no tabs
- On the official edition the Business group now lists all eight screens — Overview, Installs, Downloads, Licensed servers, Packages, Invoices, Licences, Plans & pricing — as entries of their own, and the tab bar that linked them at the top of each screen is gone (
svc_web.go PanelMenu;SaTabsin the interface renders nothing).TestLicPlans…checks the menu entry instead of the tab.
Screen addresses as paths
- Every panel screen has a path address:
https://srv.<domain>:<port>/security/malware-admin,/accounts/accounts,/email/mail-log,/account/profile… —/<group>/<route>, the group being the sidebar heading the screen sits under (the administrator's sidebar first, then the reseller's, then the customer's), or/<route>for a screen under no heading (/login). The interface writes these addresses in every link and in the address bar (frontend/panel/src/lib/router.ts:routePath,routeOfPath,canonicalize; the groups come with the start-up data asBoot.groups), the server answers them (internal/panel/routes_paths.go,svc_index.go servePage), and a path with another group or no group still opens the screen. The classic/?r=<route>&…keeps working everywhere — bookmarks, emails, the server's own links and redirects — and is shown as its path once the interface runs. Tests:TestRoutePaths,test/router.test.ts.
One email address per login
- A panel login's contact email must be unique — creating a hosting account, a reseller or a staff member, changing an account's contact email (Server Admin › Accounts, or the account's own Profile), the token API's account creation, a paid order's new account and an imported account are all refused with "That email address already belongs to another account." when another login (any role, any status) has that address, compared case-insensitively (
Ctx.EmailInUse,svc_hosting.go). Password resets and sign-in help find a login by its email, so two logins with one address were ambiguous. Existing duplicates are not changed; they only block a new one. Mailboxes, domains, databases, FTP, Web Disk, team logins and the rest already refused duplicates (TestAccountCreateRefusalscovers the new case).
Commands renamed: hf-agent is the administrator's command, hf-agentd the root agent
hf-agentis now the administrator's command line (hf-agent dns-setup,hf-agent licence sync,hf-agent company-mail apply,hf-agent apache-fix…); it washf-server. (The old names were links at first; see "hf-agent is the only server command".)hf-agentdis the root agent the panel runs through sudo (agent/hf-agentd,/opt/hostfog/bin/hf-agentd); it washf-agent.update.shmoves a running server over: it points the panel'sagentsetting and/etc/sudoers.d/hostfogathf-agentdbefore anything calls the agent, and removes the old root binary so the name is free for the command's link. Every script, test, document and interface text uses the new names (tests/checks/os-agent.sh,security-agent.shandcheck-lang.pypass;security-agent.sh's four hf-cpanel.py checks named the helpers folder without the file and always failed — fixed); the panel interface must be rebuilt (build.sh) for its texts to show them.
Web server profiles: "server_tokens directive is duplicate" fixed
- Applying a web server profile left nginx.conf's own
server_tokens off;in place, so nginx refused to start or reload with"server_tokens" directive is duplicate in /etc/nginx/conf.d/00-hostfog-profile.conf:4. The agent'ssedthat comments out nginx.conf'sserver_tokens,keepalive_timeoutandclient_max_body_sizeused/as its delimiter while the replacement text held the profile's path (/etc/nginx/conf.d/…), so it failed with "unknown option to s" and changed nothing (agent/lib/91-webprofile.sh;|is the delimiter now). The profile apply also repairs a duplicate on its own: whennginx -treports one, the other copy in nginx.conf is commented out and the test runs again.install.shno longer re-enablesserver_tokens off;in nginx.conf on a server that already has a profile.
The API has no port of its own
https://api.<domain>/v1on 443, and nothing else to open. The separate API port (2087,COMPANY_API_PORT) is gone:build.shasks only the panel port now (--api-portremoved;COMPANY_API_PORTstays 443 inconfigand is not asked), every address the panel, the agent, the installer, the update, the CLI tools, the SDKs, the WHMCS and Blesta modules and the docs print leaves the port out (company.APIHostPort,Ctx.apiHostPort,api_urlin the agent,api_addrin the installer), the firewall and SELinux steps open only 80, 443 and the panel port, and the PHP client never writes:443. A server whose config already has anotherapi_portkeeps it (its addresses still show the port);update.shstill pins old servers to the port they always used.- A browser on the API gets "Access denied". Opening any API address in a browser (a navigation, or a request that accepts HTML, without an Authorization header) gets a dark "Access denied" page (403) that says the address is an API for programs and points at the panel's sign-in; programs,
fetch/XMLHttpRequest calls with a token, the desktop app, webhooks, cluster and licence checks are unchanged. Tests updated (allinone_test.go,apihost_test.go,company_ports_test.go,tests/checks/apihost.py,check-installer.sh,real-install.sh, the integration tests).
Customer account on the website
/account: sign up, sign in, forgotten password, and every licence in one dashboard. A customer creates an account with their email and a password, confirmed by a 6-digit emailed code (the email decides which licences the account sees, so it is confirmed first); one account per email — a sign-up with an email that already has one is refused with "An account with this email already exists". The dashboard lists every licence bought with that email — plan, status, paid-until, server, monthly price — each with Manage, which opens the existing/licence/manage(renew, change plan, move server, payment) without an emailed link. Forgotten password: a code, then a new password. Buy a licence on the dashboard is a three-step wizard — plan (or the free trial), server IP and coupon, review and pay: a trial starts at once (the account's email is already confirmed), a paid order goes to Stripe or PayPal and the licence appears on the dashboard pending, then active and emailed when the provider confirms; the shop's thank-you address sends a signed-in customer back to the dashboard, which shows the new key with its install command. Every licence lists the panels and modules it includes. Routeaccountis public inroutes_table.go(without it the page framework asked for a panel sign-in). Rate limits per IP on sign-up, sign-in, codes and resets; passwords bcrypt-hashed; a new session id at sign-in. New tablesite_accounts(schema.sql, created byupdate.sh);internal/panel/page_account.go; the website's Your licences link now opens/account; nginx passes/accountthrough on the website (website.nginx.conf, setup-template.sh) and the pay portal serves it; docs in Licensing › Your account; testsTestAccountSignupAndDashboard,TestAccountSignInAndReset,TestAccountWizardTrial,TestAccountWizardBuy.
Sharing needs your own MySQL root password
- HF Server refuses to share a site while MySQL root still has the default password (
password), as it already did with no password: anyone can guess the default. Change it in Databases › Root password.shareAllowedinbackend/os/share.go;TestShareNeedsRootPasswordcovers it; docs updated (sharing, free links, databases, the share tutorial).
DMARC policy "reject" by default
- New DMARC records use
p=reject(wasp=quarantine): mail that fails both SPF and DKIM in the domain's name is refused instead of sent to spam — full protection against spoofing, and no more "enable a reject policy" warnings in DMARC checkers. Applies to the server's own domain (OwnZoneReports,hf-dns-setup/hf-server dns-setup), customers' mail domains, the Deliverability fix button and the DNS hints in Email › Domains. The panel's own earlier record on the server's domain (p=quarantine, strict or relaxed) becomesv=DMARC1; p=reject; rua=mailto:dmarc@<domain>; adkim=r; aspf=ron update; a record someone wrote by hand is left alone. Domains whose DNS is elsewhere: copy the new value fromhf-server dns-setup. Set SPF and DKIM first.
Forgotten password in Files and DB Studio on their own address
- "Forgotten your password?" stays on
fm.<domain>anddbs.<domain>— the sign-in pages of Files and DB Studio no longer send people to the Control Panel (cp.<domain>/?r=forgot). The reset runs in the app itself: username or email → the emailed 6-digit code → the new password (a choice of login when one email has several), then back to the sign-in page. Same reset as the panel's (one email with code and link, the same limits, lock-outs and password rules; hosting accounts only). New addressesPOST /api/v1/forgot,/forgot/code,/forgot/passwordin both apps (internal/files/forgot.go,internal/studio/forgot.go, panel sideinternal/panel/files_forgot.go); tests in Go and Vitest.
Mail on mail.<domain> in the installer summary
- Mail address in the one-file installer summary — the publisher's server summary and the website's
/mailredirect now point athttps://mail.<domain>(Mail's address since 1.0.0) instead ofwebmail.<domain>.
Short default addresses, billing pages and payment webhooks on their own names
- A new install uses short names: Server Admin
srv.<domain>, Control Panelcp.<domain>, Reseller Panelrs.<domain>, Filesfm.<domain>, DB Studiodbs.<domain>;api.,status.andwebmail.stay. The publisher's download server iscdn.<company domain>(installer, updates, the desktop app's downloads). An updated or reinstalled server keeps the names it has (portal_*settings are never renamed;update.shstill gives DB Studiodb.<domain>where it had none). - New
pay.<domain>(hf-server pay-portal on [NAME]|off, settingportal_pay): only the public billing pages —/order,/pay,/licence,/licence/manage,/licence/partner,/licence/paypal-return, their forms and the styles and icons they load — on port 443; anything else answers 404. Invoice and order links use it once set. - New
hook.<domain>(hf-server hook-portal on [NAME]|off, settingportal_hook): only the Stripe and PayPal webhooks (/stripe,/paypal, also the/v1/webhooks/…and old/billing/…paths) on port 443, each refused without the provider's valid signature; anything else answers a JSON 404. The Billing screen shows it as the webhook address. Both come with the separate panels by default (with the plan's API and billing), are added to existing servers byupdate.shlikeapi.<domain>, join the certificate and the DNS list.
DNS: the panel's own names in its zone
- When the server hosts the DNS zone of the panel's own domain, the zone now gets an A record for every panel name
(server., panel., reseller., files., db., webmail., api.) and the HF Tunnel wildcard (
*.port.<domain>) by itself, on zone creation and on the next panel update (DNS defaults version 4). Records the owner changed by hand are kept.
1.0.0 — first production release
Runs on Ubuntu 22.04, 24.04 and 26.04 LTS, Debian 12 and 13, and Rocky Linux, AlmaLinux and CentOS Stream 9 and 10 (x86-64). The server side is Go only: no PHP panel, webmail or phpMyAdmin (PHP stays for the customers' websites).
Brand
- New HF Panel logo and app icon everywhere — the terracotta app icon (three bars and a dot) is the icon of every app: HF Server on Windows (.ico, exe resources), macOS (.icns) and Linux, the panels', webmail's, Files', DB Studio's, the website's and the docs' browser icons, the installable app, notifications, the WHMCS/Blesta module logos and the social preview image. The horizontal logo (SVG, with a white-word-mark version for dark mode, PNG fallbacks) heads every app as one lockup — the "HF Panel" logo followed by a small uppercase badge for the role or app (ADMIN, RESELLER, CLIENT, MAIL, FILES, STUDIO, SERVER, DOCS; the website has none) — and the sign-in, status, order, licence, knowledge base and rating pages, the e-mail header and the developer guide. An uploaded white-label logo still replaces it. Assets are regenerated by
tools/brand/make_brand.py.
Installer screens
- Every step has its own progress bar. A running step fills its own 0 → 100 % bar (from the measured expected time, waiting at 99 % until it really ends) and a finished step shows a full bar and 100 %; the overall progress is one separate line under the step list (
Duration : 4m 2s). On a log or pipe each step is one plain line (✔ 03/13 PHP done 100%) and the run ends with the overall line. Used byinstall.sh,build.sh,update.sh,uninstall.sh,make-setup.sh,scripts/build/frontend.shandscripts/build/integrations.shthroughscripts/install/logo.sh; test:tests/checks/logo-ui.sh.
Rocky Linux, AlmaLinux and CentOS Stream 9 and 10
- One OS layer for the installer,
update.sh,uninstall.sh,hf-serverand hf-agent (agent/lib/01-os.sh): the OS family and version, packages (pkg_installand friends; callers name the Debian package,pkg_maptranslates it for dnf), service names (svc_name), the firewall (os_fw_*: ufw, or firewalld with the same calls, rule list in the format the Firewall screen reads) and SELinux./etc/hostfog/os.envtells hf-api the family (internal/osinfo). - The same layout everywhere (
02-os-rhel.shos_rhel_compat): on the rhel family httpd runs from/etc/apache2, each PHP version from the extra PHP repository (its release package installed only after a signature check; AppStream PHP 8.3 otherwise) gets/etc/php/<v>,php-fpm<v>andphp<v>-fpm.service, BIND reads/etc/bind, MariaDB/etc/mysql/mariadb.conf.d, Pure-FTPd/etc/pure-ftpd/conf;a2enmod,apache2ctl,phpenmodwork; the web servers run as www-data. Replaced originals are kept as*.hostfog-orig. - SELinux stays enforcing — booleans (web servers may connect to apps and databases, send mail, read account folders; FTP may write account files; BIND may write its zones), labels for account folders and HostFog's paths, the panel/API/Apache ports and a small policy module; permissive or disabled servers work too.
setenforce 0is never used. - EPEL and CRB are added; the installer's summary shows the family and the SELinux mode;
get.shand the one-file installers accept these systems. Not available there yet: MongoDB databases, Docker, the web application firewall and other sources under Software versions (docs: System requirements). Dry run of both families:tests/checks/os-agent.sh(also incheck-installer.sh); the first real-server test plan is indocs/PRODUCTION.md§6.
Disaster recovery and search
- Config history (Server Admin › System › Config history) — every server config file hf-agent writes (Nginx, Apache, PHP-FPM pools and php.ini, Postfix/Dovecot maps and settings, BIND zones and includes, cron files, its own units,
/etc/hostfog/*) keeps the version it replaces and the new one, with when, who (the panel user, passed ashf-agent --actor, also to the other servers of a cluster) and which command, in a root-only, content-addressed store (/var/lib/hostfog-confhist; 50 versions or 90 days per file, 200 MB in all). One shared helper inlib/00-core.sh(render, filter_file and the agent's EXIT trap), so every writer is covered. The screen lists changed files and versions, compares two versions (unified or side by side, made on the server, shown as text), exports a version and restores one with the service's own check (nginx -t,apache2ctl configtest,php-fpm -t,postfix check,doveconf -n,named-checkconf), automatic revert on failure and a reload. Customers' own files (.htaccess, an account'sphp.ini, a website's own Nginx directives), keys and password files are never kept. Hook eventconfig.restored; read-only APIGET /v1/config-history,/versions,/diff(docs: Config history). - Panel backup (Server Admin › System › Panel backup) — every night the panel packs its database, settings file, server key and system files into one file encrypted with an administrator's passphrase (scrypt, AES-256-GCM; never made without one), copies it to the chosen S3-compatible or SFTP/rclone backup storage under
_panel/, keeps N copies, shows the state on the dashboard and raises the alertpanelarc.failedand the hookspanelarc.finished/panelarc.failed.hf-server backup-panel,backup-panel-listandrestore-panel <file|name|remote> [--system]bring a lost server back (docs: Panel backup). - Panel-wide search — the header's "Jump to a page" box also finds accounts, domains, mailboxes, databases, invoices, tickets, resellers and IP addresses (grouped, arrow keys, debounced, indexed; a reseller sees only their own customers). API:
GET /v1/search?q=.
Database tools (MySQL/MariaDB and PostgreSQL)
- Database tools (Control Panel › Databases › Database tools, per database) — the slowest queries, normalised (values become
?) with calls, average and longest time and rows examined, and Explain with suggestions (a table read in full, unused indexes, sorting or grouping without an index, the columns worth an index), run as a read-only throw-away database user as the account with a 10-second limit; optimise / repair / analyse / check one table or all (VACUUM ANALYZE / ANALYZE on PostgreSQL) through the agent as a throw-away user of that database only, one task per database at a time, with a warning from 500 MB and the result per table; dumps now and on a schedule (every hour / day / week, keep N, gzip,--single-transaction/ pg_dump) stored in/var/backups/hostfog/<account>/dbdumps/, optionally copied to the customer's own backup storage, restored into the same, another or a new database with the account's rights, downloaded; a size and growth chart (one sample a day). Package limit "Scheduled dumps (0 = unlimited)" (default 5). - Database tools (Server Admin › Hosting) — MariaDB's slow query log on/off with a threshold (kept in
61-hostfog-slow.cnf), read every hour byhf-api dbtools(new cron line, added byupdate.sh), PostgreSQL'spg_stat_statementsswitched on with one restart, the slowest queries of every account and failed dump schedules. New agent commands inlib/33-dbtools.sh; hook eventsdb.dump_finished,db.optimised; APIGET|POST /v1/accounts/{username}/db-tools/dumps,POST …/dumps/{name}/restore,GET …/db-tools/slow-queries(docs: Database tools).
Bulk changes of accounts
- Bulk changes (Server Admin and Reseller Panel › Accounts › Bulk changes) — pick accounts by filter (package, reseller, server, status, PHP version, feature, created date, disk used over X), by ticking them on Accounts, or with a CSV file of
username,field,valuelines, and one change: PHP version (all websites or one), package, suspend / unsuspend with a reason, feature list, own resource limits, backup schedule, WordPress auto-update policy, sending limits, an announcement audience group (new audience "group"), malware scan, free SSL, or a move to another server (queues the existing move). A dry-run preview comes first: per account what would change, what is already so and what is refused and why (a package too small for the disk used or the domains, databases and mailboxes, a feature in use the new plan lacks, a PHP version the server lacks …), the counts and an estimated time; the run must match the preview. It runs as a background job (hf-api bulk, started by the new agent commandjob-bulk): N accounts at once (1–8,bulk_parallel), never two accounts of one server at the same time, each account checked again before its change, cancel / resume, picked up again by the 5-minute job after a restart. Per-account result log, an email summary to whoever started it, hook eventaccounts.bulk_changed. Resellers reach only their own customers and only the changes their privileges allow; limits, sending limits and moves are for administrators (docs: Bulk changes).
Addresses
- Panels on the panel port only — Server Admin, Control Panel and Reseller Panel open at
https://server.<domain>:8443,https://panel.<domain>:8443andhttps://reseller.<domain>:8443; the same names without the port send the visitor to the website. - The API on its own port —
https://api.<domain>:443/v1, for programs only (a browser gets 403 there, and on 443 and the panel port); the installer andupdate.shopen the API port in the firewall. The oldhttps://<panel>:8443/api/v1keeps working.
Speed (before/after numbers in docs/PERFORMANCE.md)
- hf-api starts in under half a second — the compiled SQLite engine is kept in
/var/cache/hostfog/sqlite(sqlite-rootfor root; made by the installer andupdate.sh, used only when owned by that user and not writable by others), so the service and every command from cron (hf-api usageevery minute) no longer spend about 1.4 s of CPU compiling it.HF_SQLITE_CACHE=offturns it off. - Large servers — lookup indexes for every per-account table (
hf-api migrateadds them to existing databases): the accounts list with 1,000 accounts went from 17.6 s to 0.16 s. Idle database connections stay open, 8 MB page cache per connection. - Fewer server agent runs — WordPress sites and Backup settings ask the agent once for all accounts on this server (
wp-scan-many,backup-list-many) instead of once per account; read-only status answers (services, PHP versions, resources …) are reused for 5 seconds, and any change empties that cache. - DNS checks in parallel — DNS setup and deliverability checks ask their DNS questions together (8 at a time) and once per page.
- No page waits for another site — the WordPress screens show the remembered newest WordPress version and check wordpress.org in the background.
- Compression — the panel's and the token API's JSON answers are gzip-compressed for clients that accept it (the domains list of 3,000 domains: 1.2 MB → 71 KB); nginx compresses the panels' scripts, styles and fonts (
/etc/nginx/conf.d/hostfog-gzip.conf). - Server defaults — OPcache room for 20,000 scripts and 16 MB of strings (
90-hostfog-opcache.ini; still checks files every 2 s); MariaDB buffer pool sized to about 15 % of memory once (61-hostfog-perf.cnf, applied without a restart; your own value is kept). - Interfaces — the panel and Mail load their fonts from the server (no requests to other sites; webmail CSP
font-src 'self'); the panel loads a page's code when the pointer rests on its menu entry; the accounts list no longer redraws every row while typing; Files draws folders with thousands of entries in steps; HF Server stops asking for its state while the window is hidden and reads process paths on Windows once per process. - Builds —
build.shbuilds the four server interfaces side by side (HF_JOBS, default the CPU count up to 4);scripts/build/frontend.shskipsnpm ciwhenpackage-lock.jsonand Node.js are unchanged (HF_NPM_CI=1forces it).
Visitor statistics without scripts or cookies
- Visitor stats (Control Panel › Websites) — counted every hour from each website's access log by
hf-api visitor-stats(new line in/etc/cron.d/hostfog;update.shadds it): visitors, page views, requests, bandwidth, bots (by user agent, counted apart), top pages, referrers (domain only), countries (from the site firewall's country list, else "unknown"), devices, browsers, systems and response codes; any date range in the last 13 months, the last 48 hours by hour, SVG charts with a table under each, and Download CSV. Nginx and Apache log formats; the log is read in 8 MB pieces as the account (agentweblog-read), with its place kept per file and rotation (copytruncate or a new file) followed through the.1file. - No address stored — a visitor is a keyed hash of address and browser with a random daily key; the codes and keys are deleted after two days. Days are kept 13 months, hours 48 hours (
web_stats,web_stats_hour). - Top sites by traffic (Server Admin and Reseller Panel › Accounts), the package feature Visitor statistics in Feature lists, and
GET /v1/accounts/{username}/domains/{domain}/statsin the token API and SDKs.
Site checks and the visual cron editor
- Site checks (Control Panel › Websites › Site checks) — every website is crawled from the server on a schedule (weekly by default; the customer chooses daily, weekly or off) by a goroutine of hf-api (
API.SiteCheckLoop, one check at a time, locksitecheck.lock): at most 500 pages, 2 requests a second (slower for a robots.txtCrawl-delay), 10 minutes, robots.txt respected, the same host only, through the uptime monitor's address guard (public addresses, plus the account's own websites on this server on ports 80/443). Found: broken internal links and files (4xx/5xx), broken external links (HEAD, at most 100), mixed content, certificate problems (chain, name, ending within 14 days), TLS 1.0/1.1 still accepted, no http → https or www redirect, redirect chains, pages and images over 1 MB, missing titles and meta descriptions. Each run is compared with the one before (new / fixed); the newest 20 runs per website are kept; a summary email at most once a week and only when the problems changed; "Check now"; package switch Site checks; hook eventsite.check_finished; token APIGET|POST|PATCH /v1/accounts/{username}/domains/{domain}/site-check(docs: Site checks). - Visual cron editor (Control Panel › Software › Cron jobs) — the schedule is built from every N minutes / every N hours at a minute / daily / weekly on chosen days / monthly on a day, or a custom 5-field schedule; it is shown in words ("Every Monday at 03:00") with the next 5 run times in the server's time zone (action
cron.preview). Command hints (a%sign,sudo, relative programs, paths outside the home folder) and a picker of common commands (WordPress cron with wp-cli, the Laravel scheduler, a PHP script of a website with its PHP version). Run now, history and monitoring stay. The expression ↔ builder conversion is the same in Go (svc_cronbuilder.go) and TypeScript (lib/cronBuilder.ts), checked against shared fixtures.
Site firewall rules, cron job monitor and capacity forecast
- Firewall rules per website — new screen Control Panel › Security › Firewall rules (and Server Admin › Security › Site firewall rules for any website): allowed and blocked IP addresses and ranges, "only the allowed addresses", blocked countries (from a CSV country list the administrator imports, an Nginx geo map), a request limit on login pages (WordPress, /admin…, own paths; Nginx
limit_req, 429) and bot protection (a short JavaScript cookie check answered by hf-api for suspicious or all visitors; known search engine crawlers let through, optionally only after a reverse DNS check). Rules are tested withnginx -tand rolled back when refused (hf-agent sitefw-set,sitefw-log,sitefw-geo,sitefw-crawlers;lib/86-sitefw.sh); refused requests are summarised per website. Hook eventfirewall.rules_changed; token API/v1/accounts/{username}/firewall-rules. - Cron job monitor — monitored jobs run through
hf-cronwrapas the account (installed in/usr/local/hostfog/bin): last run, exit code, duration and the first 16 KB of output per run (50 kept per job), Run now with its output, history, and a notification plus hook event when a job fails (cron.failed) or does not start within its schedule and grace time (cron.missed). Token API/v1/accounts/{username}/cron/{id}/runsand/run. - Capacity forecast — new screen Server › Capacity forecast: disk space and files per partition, memory and load sampled every 5 minutes (180 days); a weighted line through the daily peaks predicts when each runs out ("Disk / full in about 9 days" on the overview), the accounts growing fastest, and notifications at 14, 7 and 2 days (
capacity.low, hookserver.capacity_low). Token APIGET /v1/server/capacity.
Two-step sign-in policy, abuse desk, IPv6 and DNSSEC
- Two-step sign-in policy — new screen Security › Two-step sign-in: Optional or Required for administrators, team members, resellers and customers, with a grace period (default 7 days) during which a banner counts down; after it the password is followed by the new Set up two-step sign-in screen (authenticator app or passkey, then recovery codes) and nothing else opens. A login with only a passkey is asked for it after the password (or a recovery code). Resellers can require it for their own customers. Administrators and resellers reset a person's two-step sign-in (activity log, hook
twostep.reset). Files and DB Studio follow the same policy. The old staff switch on Settings follows it. Hookstwostep.policy_changed,twostep.reset. - Abuse desk — new screen Security › Abuse desk: complaints from
abuse@<domain>(a blind copy into the mailboxhf-abuse@<domain>, fetched every 5 minutes:hf-agent abuse-intake,abuse-fetch; nothing deleted), spam-feedback reports (ARF) read field by field, the public form/?r=abuse-report(5 an hour per address) andPOST /v1/abuse. Each is read for IP, domain, web address and time and linked to the account; statuses new / waiting for customer / resolved, notes, customer notices from editable templates, replies to the reporter, one-click suspensions (account, outgoing mail, website, mailbox). Resellers see their customers' complaints when allowed. Token API/v1/abuse…; hooksabuse.received,abuse.status_changed,abuse.action. - IPv6 addresses — new screen Hosting › IP addresses: each server's IPv4 and IPv6 (or why it has none), IPv6 pools per server, and an IPv6 address of its own for a website or a whole account: added to the interface and kept across reboots (
hf-agent ipv6-addr, servicehf-ipv6-addrs), Nginx listens on it (site-ipv6), the AAAA record is published. Hooksipaddr.assigned,ipaddr.released. - DNSSEC — the DNS zone editor shows the signing status, the BIND version and BIND's key report (
hf-agent dnssec-status); signing is refused on BIND older than 9.16. For domains bought through domain sales the panel sends the DS record to the registrar itself, removes it before signing stops, and checks daily. Token APIGET|POST|PUT /v1/accounts/{username}/dns/{zone}/dnssec; hooksdnssec.enabled,dnssec.disabled. New tables (twostep_owner,abuse_complaints,abuse_events,ipv6_pools,ipv6_addrs) are made byhf-api migrate(or on first use).
Reseller white-label addresses and team access
- Your panel address (Reseller Panel › My brand) — a reseller's own panel address (
panel.theirbrand.com) served on the panel port with the panel itself, the bare name on 443 sent to their website (or the panel port), optional webmail and files names, one free certificate for the names that point here (hf-agent panel-alias-sync, now withpanel/webmail/fileslines); live DNS (A/AAAA or CNAME) and certificate check with Check now; once verified, links and emails for their customers use it, emails carry their brand and sender name, passkeys work on it. New privilege Own panel address; removing the address or the privilege falls back to the server's address at once. Tablereseller_domains. Token APIGET /v1/resellers/{username}/domain,POST …/domain/check; hooksreseller.domain_changed,reseller.domain_verified. - Team access — new roles Websites & DNS and Files only; team members make their own API tokens on My login (their role at each call, dead once suspended or removed; table
team_tokens); package field Team members (0 = unlimited) (-1 = none;hf-api migrateturns older packages' 0 into -1 once). Token API/v1/accounts/{username}/team(list, invite, change, suspend, remove); hooksteam.member_invited,team.member_changed,team.member_removed.
Currencies, tax, unpaid invoices and affiliates
- Exchange rates — Tax & currencies has an exchange-rate table; with conversion on, a package without its own price in an accepted currency is sold at the converted price (a typed price always wins). Rates can be fetched every night from an https address of your choice (JSON with
rates, any base). Hookbilling.fx_updated. - Tax outside the EU and the UK — optional rates per country (sales tax, GST) instead of "outside the scope"; your own VAT number check — an https address asked instead of VIES for EU and UK numbers.
- Unpaid invoices — new screen Billing › Unpaid invoices: overdue totals per currency, age groups, every open case (hand-made invoices, failed card and PayPal payments); configurable reminder days (e.g. 1, 3, 5, 7) with a pay-now link, a reminder before the due date, card payments tried again in Stripe, suspension after the grace period and automatic unsuspension on payment. Closing accounts is off by default and only happens when the administrator sets a number of days and confirms it. Hooks
billing.reminder_sent,billing.payment_retried,billing.suspended,billing.terminated. - Affiliates — new screens Billing › Affiliates and Control Panel › Affiliate: applications and approval, referral links with a 30–90 day cookie and click counts, commission rules (percent or fixed, first payment or every payment for N months, per package), a hold period before approval (refunded payments earn nothing), fraud checks (own link or IP, same card, many sign-ups per IP), payout requests with a minimum, paid by hand or as account credit. New tables
affiliates,aff_clicks,aff_commissions,aff_payouts,aff_rules(made byhf-api migrateor on first use). Hooksaffiliate.*. - Token API —
GET /v1/invoices,/v1/invoices/{id}(with lines, VAT breakdown, seller and buyer),/v1/tax-report,/v1/dunning,/v1/affiliates,/v1/affiliates/{username},/v1/affiliate.
Discount codes, announcements, help, welcome wizard and personal data
- Discount codes — new screen Billing › Discount codes: percent, amount or free months (trial days at checkout); first payment, some months or every payment; start and end dates; uses in total and per customer; packages, billing periods and (publisher's server) licence plans; new customers only. The order page and
/licencehave Apply (the price is worked out on the server); Stripe gets the code's coupon (a code with rules only our page can check has no promotion code), PayPal a plan priced with it; hand-made invoices take a code as a discount line, and invoices of paid orders show the discount while it runs. Usage report and CSV; every use incoupon_uses. Token API/v1/coupons…(administrators); hookscoupon.created,coupon.used. - Announcements — audiences (all, one package, one reseller's customers, accounts with a feature), banner and / or email, banner end date, send later (cancel until then), simple Markdown with a preview, counts (emails sent, banners seen and closed). News emails keep the unsubscribe link; service notices always go. Token API
GET|POST /v1/announcements; hookannouncement.sent. - Knowledge base — article visibility (public, signed-in customers, staff only), categories of your own, resellers' own articles for their customers (and hiding the server's), a public help page
/kbper server or reseller brand, help links in live chat replies (Link a help article), token APIGET /v1/kb/search; hookkb.article_published. - Welcome wizard — customers get a short setup list after their first sign-in (domain with a live DNS check, mailbox, SSL with Issue it now, website, backups), saved per customer, closable and reopened from the menu; administrators switch it off or change the steps and their order. Hook
onboarding.completed. - Your data — customers download a ZIP of their personal data (profile, invoices with PDFs, tickets, chats, activity log, a backup link), emailed as a time-limited link, and ask to delete their account: confirmed by email, a waiting time (default 14 days) with cancel, then approved by the host on Data requests (or automatic); a final backup is made first and kept for the days the administrator sets. Hooks
privacy.export_ready,privacy.deletion_requested,privacy.deletion_done. New tables (coupon_uses,announce_targets,kb_categories,onboard_state,data_requests) and columns are added byhf-api migrate.
Terminal, SSH keys and own backup storage (Control Panel)
- Terminal in the page — the web terminal is now xterm.js on the Terminal screen, connected over a WebSocket through hf-api (
/api/v1/terminal/wson the panel port; nginx passes the upgrade, added to existing servers by the update) to a shell on a pseudo-terminal held by hf-api:hf-agent term-shellruns it as the account in the same sandbox as its SSH shell (never root), in a systemd unit of its own. Copy and paste (buttons, Ctrl+Shift+C / Ctrl+Shift+V), resizing, light or dark look; one-time tickets bound to the panel session, same-origin only; session time limit, idle timeout and terminals per account on Settings (defaults 120 min, 15 min, 2); start and end in the activity log (never what is typed); hooksterminal.started,terminal.ended. Accounts on a joined server keep that server's terminal in a new tab. - SSH key manager — keys are checked (ed25519, ECDSA, RSA of 3072 bits or more; no options) and show their fingerprint; import several keys or the ones already in
~/.ssh/authorized_keys; Make a key pair on the server as the account (the private key shown once to download, never kept); customers switch SSH and SFTP logins on or off when their plan allows it (feature Switch SSH access on and off; sshd'sDenyGroups hfnossh). Hooksssh.key_added,ssh.key_removed,ssh.access_changed; token API/v1/accounts/{username}/ssh/…. - Own backup storage — new screen: customers add their own S3-compatible storage or SFTP server (secrets sealed with the server key, SFTP host key pinned), test it, choose a schedule and how many days copies are kept, copy a backup now, list the copies and restore from them. Every connection is made as the account; a copy brought back is checked before it can be restored. Part of the Backup storage licence module; each plan sets its number of places on Backup storage (default 2, 0 = none) and the Feature lists can leave it out. Hooks
backup.target_added,backup.target_removed,backup.remote_uploaded,backup.remote_failed,backup.remote_restored; token API/v1/accounts/{username}/backup-targets….
Apps, staging and Git previews
- More one-click apps — Moodle and Ghost (a Node.js app in
~/apps/ghost, run as the website's Node.js app) join Joomla, Drupal, Laravel, Nextcloud and the rest. Each app shows its requirements (PHP, Node.js, database) and its install steps; downloads come from the official release addresses and are checked against their SHA-256. New Server Admin screen Apps catalogue: hide apps from customers (sites that run them keep them). Uninstalling an app now makes a backup of its files and database first (~/backups/apps/). Eventapp.installed. - Staging for any website — a name of your own (
test.yourdomain.com), password-protected and kept out of search engines by default, the address replaced in the database of an app that isn't WordPress only when ticked; Pull from live; after Push to live the copy's protection never stays on the live site. Eventstaging.pushed. - Site templates and cloning — Copy a website now chooses the database of apps that aren't WordPress or Laravel (the new database, user and password are shown to enter in the app), replaces the address in a non-WordPress database when ticked and issues free SSL at once. Site templates (Control Panel): save a website as a template (files and database, uploads optional,
wp-config.php/.envstored without the database password, kept in~/templatesreadable by the account only, up to the package's new Template size (MB, 0 = unlimited)), make a new website from it or from the host's shared templates with{{site_title}},{{admin_email}},{{site_url}}and the address filled in (WordPress: blogname, admin email, new salts through wp-cli as the account). Staff (Site templates in Server Admin and Reseller Panel) offer shared templates (kept root-only in/var/lib/hostfog/templates; an administrator's to everyone, a reseller's to their customers) and copy a website into another account they manage (files read as one account and written as the other). Eventssite.cloned,template.created,site.from_template; APIPOST /v1/accounts/{username}/clone,/templates,/templates/{id}/site. - Git deploys with preview links — a repository's live branch deploys to a website on every push and runs its deploy script; every other branch (or the chosen ones) gets
<branch>.preview.<domain>, removed when the branch is deleted or after the days you choose; build logs on the new Deploys & previews screen. Package limit Git previews (0 = unlimited).hf-api previewsruns every 5 minutes. Eventpreview.deployed. - API —
GET /v1/accounts/{username}/apps,…/apps/installs, and…/staging(list, create, push, pull, delete).
WordPress safe updates
- Safe automatic updates — new screen WordPress updates (Control Panel, Reseller Panel, Server Admin): per site Off, Notify only or Automatic, daily or weekly in a window of hours (each site gets its own minute, so runs are spread). Before anything changes the site is backed up (files and database, a "pre-update" backup kept 7 days) and its home page plus up to 4 chosen pages are checked; each update — WordPress minor releases by default (every version optional), plugins, themes — is installed on its own with wp-cli as the account and the pages are checked again. A failed check (no answer, an error status, PHP error text, the title gone, a much smaller page) puts the backup back at once and holds that version, so it is skipped until a newer one comes out. Items can be marked "never update".
- Bulk view — every WordPress site with its version, waiting updates, last result, security problems and update mode; update, check or set the schedule of the ticked sites; administrators set the policy new sites get. One run per site and per account at a time, at most 2 per server at once (
wp_update_parallel). - Security check — installed WordPress, plugin and theme versions are compared with the publisher's vulnerability list: "X sites have a plugin with a known security problem", with Update now when a fixed version exists and Deactivate otherwise. The official edition imports the list (JSON) and serves it at
/licence/wp-vulns; paid servers fetch it once a day. - Emails, hooks and API — one summary email a day (what updated, what was rolled back), events
wordpress.updatedandwordpress.rollback, and the token APIGET /v1/accounts/{username}/wordpressandPOST /v1/accounts/{username}/wordpress/{site}/update.update.shadds the 15-minute jobhf-api wpupdate 0to existing servers.
HF Tunnel
- Clear messages in HF Server — a failed Share shows why in the window (red message) and what to do: tunnel server stopped or port blocked (with the address and port), DNS, certificate, a changed server key, a link already connected elsewhere, a plan without Tunnels, a deleted or read-only API token.
- No long waits when api.<domain>:443 is blocked — HF Server remembers for 10 minutes that the API address didn't answer and uses the panel's old address at once; connections give up after 15 seconds instead of 30.
- Wildcard certificate for tunnels — with a wildcard certificate for
*.<tunnel domain>every tunnel is on HTTPS at once (no certificate per link, so no Let's Encrypt weekly limit). - Licence — without the Tunnels module desktop links say they are off, and a tunnel refresh keeps the tunnel sites off (the list and keys are kept).
- frps' web port answers nginx only — the tunnel server's internal web port (7080) listens on 127.0.0.1 (set on existing servers by the next update).
Live chat
- Chat with your customers — customers chat with their host (the administrators, or the reseller who owns the account) from a Chat with us button on every page of the Control Panel; staff answer on Server Admin / Reseller Panel › Support › Live chat: conversations by status with unread counts, the customer's account, domain and package, Assign to me, Close, Convert to ticket, canned replies per staff member, Available / Away. The menu and the top bar show how many chats wait; a new message plays a sound (can be muted) and, with the tab in the background, shows a browser notification. Read-only staff logins can answer; team members with the Support role can chat.
- Offline becomes a ticket — while nobody is online (no staff with the panel open and Available, or outside the optional opening hours) a customer's message becomes a support ticket (the usual emails) and the chat says so.
- Website widget — optional (Settings tab):
<script src="https://<panel>/chat/widget.js" defer>lets website visitors chat with the administrators after giving a name and email; offline, their message is emailed to the support address. The publisher's website loads it (site.jsonchat_widget) and passes/chat/to the panel. - Live, without another service — Server-Sent Events from hf-api through the panel's existing nginx locations (
X-Accel-Buffering: no); plain text only, 2,000 characters, 10 messages a minute; hookschat.started,chat.message,chat.closed. Tableschat_convs,chat_msgs,chat_canned,chat_presence(made on first use on older databases). Part of every licence, with support tickets.
Help desk: email to ticket, canned replies, satisfaction ratings
- Email to ticket (Server Admin › Support › Email to ticket) — mail to
support@<domain>(and to a reseller's own support address while their brand is on) also goes to the intake mailboxhf-support@<domain>(new agent commandsupport-intake, the same Postfix blind copy map as the abuse desk); the 5-minute job reads it with the abuse desk's ownabuse-fetch(nowabuse-fetch support: 5 messages of up to 15 MB per call, marked read, never deleted). An email from an account's address (or a team member's) opens a ticket and gets an acknowledgement; a reply to a ticket email is added to its ticket — matched by a signed token in the Reply-To (support+t12.<token>@…; Postfix getsrecipient_delimiter = +when it has none) and in the Message-ID / References, or by "[Ticket #12]" from the ticket's customer; staff may answer by email too. Unknown senders wait for verification (staff assign them to an account or put them aside) or are refused (setting), told once a day at most. Automatic emails (Auto-Submitted, Precedence, out-of-office headers and subjects, bounces, mailing lists, the panel's own addresses and Message-IDs), duplicates and senders over the hourly limit are skipped and logged, never answered. HTML mail is kept as plain text; quoted earlier emails are cut at the reply marker. Attachments: 10 per email, size limit (default 10 MB, 25 MB in all), each checked with the malware scanner (new agent commandsupport-scan: ClamAV when ready plushf-scan.py) and kept only when clean; downloads only. Tablesticket_files,support_inbox,support_mail_log; columnstickets.source,ticket_msgs.via(docs: Email to ticket). - Canned replies in one store — the tickets' saved replies and live chat's canned replies are one list (
ticket_replies, new columnuser_id): shared replies of the team (administrators, or each reseller) and each staff member's own, multi-language, with{customer},{domain},{ticket}(and{name},{brand}) filled in; inserted with one click in tickets and chats. Chat's old replies are copied in once (chat_cannedkept). - Satisfaction ratings (Server Admin and Reseller Panel › Support › Satisfaction ratings) — after a ticket is closed or a chat ends (when staff answered), the customer rates it 1–5 with one click: links in an email (public page
rate, saved by the page itself so mail scanners don't vote; 30 days) and a prompt on the ticket and in the chat window, optional comment. Report: average, response rate, per staff member, per month, lowest ratings with comments; a reseller sees only their customers'. Hook eventsticket.rated,chat.rated. Tablesupport_ratings. - Resellers get Support tickets in their menu.
Malware quarantine and clean-up
- Quarantine that can be undone — a quarantined file moves to
/var/lib/hostfog-quarantine/<account>/(root-only, outside every home) with its original path, owner, permissions, modification time, SHA-256, detection and time; nothing is left in its place (or, when chosen, an empty PHP file for websites that include it). Put back restores it exactly and refuses when a file of that name is there again unless Overwrite is ticked. Quarantined files are deleted for good only after the days set (default 30) or with Delete now. - Clean in place — PHP code put before, into the first line of, or after a file's own code (
eval(base64_decode(…))injections) and spam redirects in.htaccessare removed after the original is kept in quarantine; an infected WordPress core file is replaced by the clean copy of the same version (wp-cli as the account). Files marked safe are never touched. - Policy — per server and, when allowed, per account: Report only, Quarantine or Clean and quarantine; daily, weekly or no scan; skipped paths.
- Malware screens — Control Panel › Security › Malware (findings with severity, Clean / Quarantine / It's safe / Delete now / Put back, the quarantine, the own policy) and Server Admin › Security › Malware (the server policy, every account's counts with Scan now and its own policy, all findings and the quarantine). A weekly email report goes to customers and the host only when something was found or done; new events
malware.foundandmalware.cleanedfor webhooks and script hooks. The EICAR test file is found without ClamAV.
Uptime monitoring
- More than websites — monitors for HTTP(S) addresses (status code range, a word that must or must not be on the page, timeout, follow redirects, a warning N days before the SSL certificate ends), TCP ports and hosts (a TCP connect), every 1, 5 or 15 minutes. Packages set the number of monitors (0 = unlimited) and the shortest interval; new websites can be watched automatically.
- Scheduler inside hf-api — checks run every 10 seconds for the monitors that are due, 8 at a time with a little jitter; the schedule is in the database, so a restart carries on (
hf-api sampletakes over when hf-api is not running). A monitor is down only after 2 failed checks 30 seconds apart. Checks reach public addresses only; an account's own sites may reach this server's own addresses on ports 80/443. - Incidents and alerts — incidents with start, end and cause; alerts by email (the account and up to 5 contacts per monitor), in the panel and through the notifications centre, events
monitor.down/monitor.upfor webhooks and script hooks; reminders every N hours while down, a "back up" message with the downtime, and maintenance windows that pause alerts. History: every check 48 hours, hourly figures 90 days, incidents a year. - Screens — Uptime monitor (list with uptime over 24 h / 7 / 30 days, response-time sparkline and last incident; a detail page with a response-time graph, a table view and incidents), Uptime overview for administrators and resellers, and a "customer websites down" count on the dashboard.
- Public status page per account — optional, off by default:
https://<panel>/status/<token>(and.json) with the chosen monitors by name, 90 days of daily bars and the incidents of the last 30 days; no addresses or error details; cacheable for 60 seconds. - Token API —
GET/POST /v1/accounts/{username}/uptime,GET/DELETE /v1/accounts/{username}/uptime/{id}(with history).
Status page
- Email updates with double opt-in — visitors of the public status page sign up with Get updates; a confirm link is emailed first, then every incident and maintenance update (unsubscribe link in each email; at most five sign-ups an hour per IP). Atom and RSS feeds at
/status/feed.atomand/status/feed.rss. - Each cluster server checked — joined servers are checked every 5 minutes too; a part down on one server shows "Some problems", and the page can list each server with its 90-day uptime (off by default).
- Banner inside the panel — open incidents and maintenance (in progress or within 48 hours) show above every page, with a link to the status page; reseller customers get the reseller's own address and brand.
- Edit after posting — title, affected parts, impact and the maintenance window; event
status.incidentfor webhooks and script hooks; APIGET /v1/status/page(no token) and/v1/status/incidents(administrators).
Email: sending limits, spam filters, domain checks
- Emails per hour and per day — every account has both limits: its own (Server Admin › Mail health › Sending limits per account, or
PUT /v1/accounts/{username}/email/limits), else its package's (Packages: Emails per hour, new Emails per day), else the server default (Mail health › Sending limits). They count every recipient sent by a mailbox login (SMTP) and by the account's websites (PHPmail(),sendmail) and mailing lists; over a limit, mail is deferred (450 4.7.1, sendmail exit 75) and goes out later. The account owner, its reseller and the administrators are told once (alert centre events Sending limit reached / Sending put on hold), hooksmail.limit_reachedandmail.spike_held, and Mail health lists the top senders (this hour and 24 hours) and the last day's events with Let it send again. - Spike guard in the mail path — an account sending in one hour more than 5 times its busiest hour of the last 7 days (at least the guard's minimum) is held at once, on the message that crosses it, instead of at the next 5-minute check.
- Policy service in Go —
hf-api mail-policy(servicehostfog-mailpolicy, hardened systemd unit) replaces the Python policy script: the Postfix policy socket plus the local socket forhf-sendmail, counters saved in/var/lib/hostfog/mail-policyevery 30 seconds and on stop./etc/hostfog/mail-limitsnow holdsUSER HOUR DAYlines and@guard on|off MIN FACTOR; new agent commandmail-guard. - Spam filters per mailbox — Email › Spam filters: settings for all mailboxes or one mailbox on its own; Strength (Low / Medium / High / custom score), What happens to spam (Spam folder, or keep it in the Inbox with
[SPAM]in the subject), Obvious spam (delete, or refuse it), allow and block lists — all applied by each mailbox's Sieve script (Dovecot, as the mail user).update.shenables Sieveeditheader. A Spam folder review list (last 30 days) with Not spam (taught to SpamAssassin as good mail and moved to the Inbox) and Spam (taught as spam): agent commandmail-junk-learn. - Guided domain email check — Email deliverability now checks MX, SPF, DKIM, DMARC (with a reporting address), the reverse DNS of the sending IP, TLS (STARTTLS) on the domain's mail server and IP and domain blocklists, each pass / warn / fail in plain words; Fix it for me writes the SPF, DKIM, DMARC or MX record into the zone when this server runs the domain's DNS, otherwise the exact record to add is shown with Copy. API
GET /v1/accounts/{username}/email/checks/{domain}andPOST …/fix.
Server balancing
- Automatic moves in a cluster — Server Admin › Accounts › Server balancing: limits for disk, memory, load per core and accounts per server; suggested moves by size and usage to servers with room; approve / skip or automatic mode; moves run one at a time in a maintenance window with a backup first, Move accounts, a check on the new server and a rollback when the check fails; customers are emailed before (hours set) and after. Off by default, dry run on. Event
account.moved(also for manual moves); API/v1/server/moves/….
Build
- The API and panel ports are asked —
build.shasks the API port and the panel port after the domain (--api-port,--panel-port; default 2087 and 8443) and saves them inconfig(COMPANY_API_PORT,COMPANY_PANEL_PORT). They are the publisher's addresses (https://api.<domain>:<port>/v1,https://server.<domain>:<port>) and the default ports of every server installed from the build: built into hf-api and HF Server, filled into the installer, agent, commands, docs and website; the installer savesapi_port, opens both ports in the firewall and shows them in the summary. A server's ownpanel_port/api_portstill wins. HF Server uses the API address with its port again (https://api.<domain>:<port>/v1), falling back to the old panel address as before. - build.sh looks like the installer — the logo, a check of everything the build needs (Go, Node.js, NSIS, shfmt, the keys) before it starts, one line per step with a spinner and its time, the last lines of the log when a step fails, and a summary of the files made with their sizes. Before building it asks the company, its domain, the version code and what to build (Enter keeps each saved value; a change is saved to
config,release-files/VERSIONand the version constants of the programs and package files);--yesasks nothing. Output goes toreleases/build.log(--verboseshows it on the screen).
Default mailbox
- admin@<domain> always exists — the installer and
update.shmake the mailboxadmin@<domain>of the server's own domain when it is missing (the website's contact address and a ready webmail login), whatever the role addresses forward to; the password is shown once and kept in/root/hostfog-mailboxes.txt. - Server mailboxes screen — Server Admin › Email › Server mailboxes shows the mail of the server's own domain: how to sign in (Mail at
webmail.<domain>, the full address, the password file), each mailbox with Webmail (opens it signed in), New password (shown once), size limit and delete, new mailboxes, and the forwarders with the role addresses and where they go. The install and update summaries say where to sign in. New agent commandsmail-roles-show,-passwd,-quota,-del,-unset,-sso.
Names that are not web addresses
- 403 Forbidden for browsers on ftp., smtp., imap., mta-sts. and the nameservers (ns1., ns2.) — these names are for FTP, mail apps, DNS and MTA-STS, so a browser opening them on port 80 or 443 gets "403 Forbidden" instead of being sent on to the panel or webmail (agent
closed-web, run byportals-web, so install, update and every portal change keep it current). Let's Encrypt checks still work, the names join the server's certificate once they point here, and an MTA-STS policy put at/etc/nginx/hostfog/mta-sts.txtis served athttps://mta-sts.<domain>/.well-known/mta-sts.txt. mail.<domain> still opens webmail. - api.<domain> is for programs only — a browser opening a page there (no Authorization header) gets "403 Forbidden"; tokens, signed webhooks, the desktop app, the tunnel download and Dynamic DNS updates work as before.
Panel style sheets after an update
- Every panel address shows an update at once — the built interface's hashed files are kept by browsers for a year, while app.css and the build list are checked again on every visit (
Cache-Control: no-cache, from nginx and hf-api alike). An address visited before an update, such as panel.<domain>, server.<domain> or reseller.<domain>, or a CDN in front of it, no longer keeps an old style sheet. A missing built file answers 404 instead of the sign-in page. update.sh adds the rules to existing servers.
Token API
- Much more of the panel by API — packages (create, change, delete), resellers (create, suspend, limit, delete), and per account under
/v1/accounts/{username}/…: domains (add website, subdomain or parked domain, folder, PHP version, remove), DNS records, mailboxes (password, quota, autoresponder), forwarders, MySQL databases, users and privileges, PostgreSQL, SSL (issue, renew, https redirect, remove; AutoSSL run), backups (make, restore, download link, delete), cron jobs, FTP accounts, and for administrators services, health and the version check. Every change runs the panel screen's own code: same checks, limits, reseller privileges, plan features, approvals and activity log. - OpenAPI 3.1 —
GET /v1/openapi.json(no token) and the API explorer in the documentation, made from the same list as the router. - Idempotency-Key and a request limit — a POST sent again with the same key within 24 hours gets the first answer instead of being done twice; each token may make 600 requests a minute (
api_rate_limit), shown inX-RateLimit-*headers. - Client libraries for Go, Python, TypeScript, PHP, Java and Kotlin, C#/.NET, Ruby, Rust, Swift and Dart/Flutter in
integrations/sdk/, plus thehfpanelcommand for shell scripts (bash and curl;hfpanel accounts create --username …, every endpoint by name) — all generated from the OpenAPI description, so a new endpoint reaches every language at once, with the same safe automatic retries, Idempotency-Key, typed errors and request-limit headers, a README, an example and tests against a shared mock server each (bash integrations/sdk/test.sh).
Plugins and event hooks
- Event hooks — Server Admin → System → Plugins & hooks: the panel raises events where things really happen (accounts created, suspended, switched back on, deleted, moved to another package or given a new password; domains added and removed; mailboxes and databases created and deleted; certificates issued; backups finished or failed; staff sign-ins; the panel updated), each with a JSON payload that never holds a password or secret. Webhooks send the events you tick to an
https://address (private and local addresses refused, also where a name points when it connects), signed withX-HF-Signature: sha256=<HMAC of the body>, in the background with 3 retries, 10 seconds per attempt, the last 100 deliveries shown, "Send test", and the signing secret shown or renewed on request. Script hooks: executable files root puts in/etc/hostfog/hooks/<event>.d/run as root with the payload on stdin (30 seconds each; only root-owned files nobody else can change), logged to/var/log/hostfog-hooks.log; a failing hook never fails the person's action. - Plugins — a folder
/usr/local/hostfog/plugins/<id>/with aplugin.json(name, version, hooks for events, menu entries for administrators, resellers or customers, a folder of pages), installed from a.zipon the screen or withhf-server plugin install <file.zip|https://…>(andlist,enable,disable,remove). Every zip is checked before anything is written (no path leaving the folder, no symbolic link, a strictplugin.json) and unpacked by the agent as root. Enabled plugins add their entries to the panel menu, opening their pages in the panel (served behind the sign-in at/plugins/<id>/, for the roles they name). Plugins run as root through their hooks: there is no signature check, so the screen and the command warn before installing. Developers' documentation: Event hooks and Plugins, with an example plugin intools/examples/plugin-hello/.
Domain sales
- Sell domains and SSL certificates — Server Admin → System → Domain sales (off by default): choose a domain reseller account — Namecheap, ResellerClub / LogicBoxes or OpenSRS — save its API settings (kept encrypted with the server's key), test the connection in the provider's sandbox, sync the price list and set your markup or fixed prices per domain ending and currency. Customers get Domain names & SSL: search and register domains, transfer theirs in with the transfer code, buy a paid domain-validated certificate (installed and renewed by the panel; free AutoSSL stays the default), and manage nameservers, contacts, transfer lock, transfer code, WHOIS privacy, automatic renewal and DNSSEC. Orders become invoices paid with the card on file, account credit, PayPal or bank transfer; renewals are invoiced and charged automatically with reminders 30, 7 and 1 days before the end; failed orders (no funds at the reseller, taken meanwhile) are shown to the administrator with Retry and Cancel, and every provider request is logged with secrets masked. The public order page can register the customer's domain with their hosting.
Calendars and contacts
- CalDAV / CardDAV for every mailbox on the webmail's address (
/dav/, well-known redirects, SRV/TXT records in each mail domain's zone), signed in with the mailbox password or — with two-factor login — app passwords made in Mail's settings. Mail gets a Calendar (month/week/day, all-day, repeats, reminders, invitations sent and answered) and a richer Contacts page (groups, several addresses and phones), on the same store phones and computers sync; the old webmail address book moves into it automatically.
Mail delivery log, PGP, keyboard and accessibility
- Mail delivery log — Control Panel → Email → Mail delivery log (also Reseller Panel and Server Admin): search what happened to each message — sent, received, deferred, bounced or rejected —, to whom, when and what the other server said, read from the mail server's log every 5 minutes and kept 30 days (1–90, set by the administrator). Customers only ever see their own domains' mail (a forged sender never shows someone else's), resellers their customers', administrators everything.
- PGP in Mail — Tools → PGP keys: make or import a key in the browser (the server only keeps it locked with your passphrase, which never leaves the browser), encrypt and sign when writing, decrypt and check signatures when reading, keep correspondents' keys, find them by address, and publish your public key in the Web Key Directory of your domain.
- Keyboard shortcuts, welcome tour, bulk review — press
?in HF Panel or Mail for the shortcuts (g d,/,c,r,j/k…); new customers, resellers and administrators get a short welcome tour (closed once, gone on every browser); bulk actions on Accounts show the accounts they will change before they run. - Accessibility (WCAG 2.2 AA) — a visible focus ring with enough contrast, "Skip to content", labelled fields and buttons, dialogs that keep the keyboard focus, the current page marked for screen readers, and no motion for people who ask for less, in HF Panel, Mail, Files and DB Studio; checked with axe-core in the tests and in the browser.
- 21 languages — HF Panel, Mail, Files, DB Studio and the emails they send now also come in Italian, Dutch, Polish, Russian, Ukrainian, Chinese, Japanese, Korean, Indonesian, Vietnamese, Bengali, Urdu and Persian, next to English, Spanish, Portuguese, French, German, Turkish, Arabic and Hindi. Urdu and Persian are laid out right to left like Arabic, and dates in emails follow each language (year first in Chinese, Japanese and Korean).
tests/check-lang.pychecks every dictionary for missing texts and placeholders and runs inbuild.sh.
Account isolation
- Account isolation — Server Admin → System → Account isolation: a private filesystem view for cron jobs, the SSH shell, apps and each account's own PHP service (only its home, the system programs and the database sockets — no other homes,
/etc/hostfogor keys), a private/tmpper account, hidden processes (/procwith hidepid), an optional jailed SSH shell (SFTP-only stays the default) and a strict PHP function list. Every change is checked first on each server without changing anything (sandbox test per account, cron jobs using paths outside the home …) and applied only after a clean check;update.shonly runs that check and writes/var/log/hostfog-isolation-report.log. - Disk limits and usage graphs per account — packages get disk limits (MB/s and IOPS) next to CPU, memory and processes; Docker apps now count against the account's limits (re-create a running app to move it). CPU & memory (customers) and Resource usage (administrators, per account) show CPU, memory, disk speed, IOPS and processes measured by the account's cgroups, and the "limit hits" (memory, processes, CPU, disk), which customers are told about through their alerts (or by email, at most every 6 hours per kind). Every PHP pool also gets
allow_url_include=Off, strict session ids, theX-PHP-Originating-Scriptmail header andTMPDIR=~/tmp.
Resource limits and usage graphs
- More limits, per package and per account — packages also limit PHP workers per website pool and processes per Node.js / Python / Ruby app; every limit field says "0 = unlimited". An administrator can give one account its own limits (Top accounts → Charts → Limits of this account; empty = the package's). Limits go to the account's systemd slice (
hf-<user>.slice, cgroup v2) on create, package change, suspend / unsuspend and a full restore, and survive a reboot;limits-setis idempotent (nothing reloads when nothing changed). A server with cgroup v1 says "Resource limits are not supported on this server" instead of failing. - Usage every minute, kept for a year — a new minute job (
hf-api usage, added to cron byupdate.sh) reads every account's cgroup counters with one agent call per server and keeps a point a minute for 24 hours, an hour for 30 days and a day for a year (usage_series, rolled up and pruned in the same transaction). - Resource usage (customers) — the Control Panel screen (was CPU & memory) shows CPU, memory, disk I/O, processes, disk space and bandwidth over 24 hours, 7 days, 30 days or a year against the limits, each chart with "Show as table"; a notice when the account is near or at a limit, with Upgrade plan when billing is on.
- Top accounts (staff) — Server Admin and Reseller Panel → Accounts → Top accounts: accounts sorted by CPU, memory, disk I/O or limit hits, each account's charts and limits (resellers see their own customers); Accounts → Manage → Resource usage and limits opens it.
- At the limit for 15 minutes — or a process stopped for lack of memory: the customer and the host (reseller and administrators: Customer account at its limit) are told once a day per kind, and the hook event
account.limit_reachedis raised.GET /v1/accounts/{username}/usageaddsresources(limits, now, series;?period=24h|7d|30d|1y).
Website speed tools
- Speed settings per website (Control Panel › Speed test) — page cache (off by default; nginx FastCGI cache for Nginx-only sites, proxy cache for Nginx + Apache), pages with a query string cached or not (not by default), gzip/Brotli compression on/off and browser cache days (none, 1, 7, 30, 365); each saves at once, Empty page cache clears it, the
X-HostFog-Cacheheader shows HIT/MISS. Agent: new state keysCOMPRESS,BCACHE,CACHEQS(empty = server default,lib/13-speed.sh). - One-click fixes in the speed test — Fix it switches on the page cache, compression or browser caching, or makes WebP copies; the result now also lists compression, HTTP/2 and HTTP/3, the page cache answer, the page's Cache-Control and the image formats. The test only sends requests to a loopback or public address (
speed_ip; private ones needspeed_allow_private). Fixed: a page cache of more than 1 minute counted as off in the score. - Speed defaults (Server Admin › System) — page cache space per account (shared by its cached sites, at least 32 MB each), default browser cache days and query-string caching; saving rebuilds the websites (agent
speed-defaults). - Image optimiser at low priority — the tools run with
nice 19and idle disk priority. - API and hooks —
GET|PATCH /v1/accounts/{username}/domains/{domain}/speed,POST …/cache/purge,POST|GET …/speed-test; eventssite.cache_purged,site.speed_changed,speed.tested,images.optimised. Packages: the Redis field reads Redis memory (MB, 0 = off).
Web server profiles and reseller depth
- Web server profiles — Server Admin → System → Web server profiles: Nginx, Apache and PHP-FPM tuning for the whole server (workers and connections, keep-alive, the default upload size, gzip, Brotli and HTTP/3 where the server's Nginx has them, a rate limit per visitor, Apache's workers, PHP-FPM's process manager and default limits, OPcache) as Default, High traffic, Low memory or your own values. Every apply runs
nginx -t,apache2ctl configtestandphp-fpm -tfirst and puts every file back when one fails; the history can apply an earlier set again. Apache modules can be switched on and off (the ones the setup needs are locked), and a website can get its own Nginx directives from a strict allow-list (upload size, timeouts,expires, plain headers …). - Reseller privileges, pools and own brand — per reseller the administrator chooses what they may do (create, suspend, delete accounts, change plans, DNS of their accounts, Log in as, see bandwidth, own feature lists, set limits, own brand), their pools (accounts, disk, monthly bandwidth) and whether they may oversell — checked on the server for every call. Resellers get My brand: their own name, colour, logo, help link and login message shown to their customers, their own panel address (with its certificate) and their own nameservers, which their customers' zones use (the glue records to create are listed), plus lower disk quotas for single customers.
Billing: VAT, currencies, credit and quotes
- VAT rules, more currencies, account credit and quotes — Server Admin → Tax & currencies: the seller's country and VAT number, VAT per customer (the seller country's rate at home — 20% in the UK —, reverse charge for EU and UK businesses with a VAT number checked with VIES — accepted and flagged when VIES cannot be reached —, EU consumers at their own country's rate with the One Stop Shop), an editable rate table, rounding per line or on the total, prices with or without VAT (card and PayPal then add the VAT), VAT lines and notes on invoices, and a tax report per period and country with CSV. Accept more currencies with a price per package in each (never converted); customers pick theirs when ordering and keep it, and Stripe and PayPal charge in it. Server Admin → Quotes & credit: account credit (added by hand, credit notes against invoices with their own number and PDF, overpayments, referral rewards; used automatically on the next invoices, on card payments through the Stripe balance) and quotes for customers or prospects (PDF, email, a link to accept that turns the quote into an invoice). Everything is off until set up, so existing billing is unchanged.
MongoDB databases
- MongoDB databases for customers — a new Control Panel page (Databases → MongoDB databases, and an overview tile): databases with their size and collections, database users with
readWriteorreadper database, new passwords, connection strings, export/import (mongodump archive), DB Studio, and remote access on port 27017 (TLS, only from the IP addresses the customer lists). Served by FerretDB (MongoDB wire protocol, data in the server's PostgreSQL; servicehostfog-mongo), installed by default and added to existing servers byupdate.sh; shown in Software versions and Services. Packages get a MongoDB databases limit (0 = none; existing packages start at 0, so turn it on per package); backups, single-database restore, moves, suspend and delete include them; staff Databases pages list every engine; the token API has/v1/mongo(docs/API.md).
DNS tools
- Dynamic DNS, DNS templates, zone import and export — Control Panel → Domains → Dynamic DNS: names like
home.example.comthat a router or script keeps pointing at a changing address, each with a secret token (shown once), updated with the dyndns2 protocol routers speak (https://api.<domain>/nic/update, basic auth) or a simplehttps://api.<domain>/v1/ddns/update?token=…[&ip=](both also on the classic panel address); IPv4 and IPv6, a history per name, an optional email when the address changes, rate limits per name and per client address. DNS templates & import: apply a record set (Google Workspace, Microsoft 365, mail on this server, a website here or elsewhere, and the host's own) with a preview of every record added and removed, import a BIND zone file (validated, previewed, added / matching records replaced / whole zone replaced), paste many records at once, and download the zone as a BIND file. Server Admin and Reseller Panel → DNS templates: make templates for customers (placeholders{ip},{mailhost},{value}…), hide or copy the built-in ones. Everything goes through the normal zone publishing and the activity log; Dynamic DNS is a feature of its own in Feature lists.
Web Disk and share links
- Web Disk — a new Control Panel page (Files → Web Disk, and an overview tile): open the account's files as a network drive from Windows ("Map network drive"), macOS (Finder → Connect to Server) or Linux (GNOME/KDE,
gio, davfs2) athttps://files.<domain>/dav/(WebDAV, HTTPS only). The owner makes Web Disk logins (name@domain, their own password, kept to one folder, read-only or read-write); the account's own login works only when switched on (never with two-factor login on). Every file operation runs as the account's own user in the File Manager's helper, kept inside the login's folder (long-lived helpers per account, reused across requests and stopped when idle); uploads stream and stop at the account's disk limit (507); locks for clients that need them; failed sign-ins are limited per address and per login. Packages get a Web Disk logins limit (default 5, 0 = none). Activity log: sign-ins, uploads, deletes, moves (gathered per minute), every change of a login. - Share links in Files — a file's or folder's menu (or right-click) → Share link: a public link
https://files.<domain>/s/<token>(a folder downloads as a zip) that lasts 1 hour to 30 days or never, with an optional password and download limit; revoke any time. Links are listed in Files (toolbar → Share links) and on the Control Panel's Web Disk page; the public page is a small page in the server's brand with its company footer. Creating, revoking and every download are in the activity log.
Backups
- Incremental backups, more destinations, restoring chosen files — Server Admin → System → Backup storage: a snapshot of every account each night that stores only what changed (restic, one encrypted repository per account) with a full backup once a week; per hosting plan how many nightly snapshots are kept, how many a customer may keep on their own schedule and how long download links work. Off-server copies can now also go to SFTP servers (host key pinned on the first test), Backblaze B2 and Google Drive (connected with Google's sign-in for devices), optionally encrypted, each with a Test connection button; their passwords, keys and tokens are stored encrypted with the server's own key (
/etc/hostfog/secrets.key). Customers browse any backup or snapshot, tick files, folders, databases and mailboxes and restore them together — back where they were or intoweb/restored/<date>/(mail into a "Restored" folder) —, make snapshots, set their own daily or weekly schedule within their plan, download snapshots and make download links that work without signing in.update.shinstalls restic and rclone and adds the hourlyhf-api backup-schedule.
Apps and WordPress
- One-click apps with updates — the App installer now installs Joomla, Drupal, Laravel (a new project), Grav, MediaWiki, DokuWiki, phpBB, PrestaShop, OpenCart and Nextcloud completely, with a new database and the admin login from the form (Matomo is unpacked with its database ready; WordPress as before). Every release is pinned and checked (SHA-256 or the official checksum file; wp-cli and Composer check their own downloads); passwords never appear in the process list. Installed apps show their version; Update backs up the files and the database first and puts them back if anything fails, a dry run shows the plan without changing anything, and customers are told once per new version. Laravel and Drupal get their public folder as the website's root. The server's default PHP gets the GMP extension (Nextcloud) on install and on update, every PHP version added later too.
- WordPress Toolkit (customers: WordPress; admin: WordPress sites) — every WordPress site in an account — installed by HostFog or uploaded — with its version, plugins and themes: update everything or one plugin, check WordPress and plugin files against the official copies (finds hacked files), switch WordPress/plugin auto-updates, maintenance mode and debug mode, set a new admin password, clear caches. Everything runs live in the terminal dialog. wp-cli runs as the account with plugins and themes switched off, so a broken or hacked plugin can't stop it or run during a check. Admins get a list of every WordPress site on the server with the ones behind on updates first, and "Open toolkit" logs straight into that customer's WordPress page. New: a security hardening checklist per site (file editing off, wp-config.php permissions, XML-RPC blocked, the "admin" username check, new security keys) with one-click fixes, known plugin vulnerabilities from the publisher's vulnerability list, search and replace for a new address (dry run, database backup first), and updating the plugins and themes of every site at once — for a customer's sites, and for administrators across the whole server.
Developer tools
- Perl modules, PEAR, Composer and PHP extensions per account — four new Control Panel screens under Software: CPAN modules into
~/perl5(cpanm --local-lib) and PEAR packages into~/pear, both installed as the account and followed live; a Composer runner per website (choose the folder,install/update/dump-autoload/validate,--no-dev, the website's PHP version, time and memory limits, live output; onecomposer.pharfor the whole server); and extra PHP extensions switched on for one account only, from the ones the administrator offers per PHP version (Server Admin → System → Developer tools, which also sets Composer's limits). Each screen is a feature of its own for Feature lists; every change is in the activity log. PEAR comes from the operating system's own packages, so it never brings a second, newer PHP version along or changes thephpcommand.
Notifications centre
- Alerts where people look — everyone gets a Notifications page (customers and resellers next to Password & security; administrators also Server Admin → System → Notifications): channels by email, webhook (JSON signed with HMAC-SHA256), Slack, Telegram (bot token + chat id) and Discord, each with Send test; which events go to which channel; quiet hours in the person's time zone (urgent events may still come through); the latest deliveries. Events: website down / back up, disk space or file limit at 80 / 90 / 100%, SSL certificates ending in 14 / 7 / 1 days, CPU / memory limit reached, backup failed, sign-in from a new IP address, malware found, new support ticket and replies, and for administrators service down / back up, a long mail queue and the server IP on a blocklist. The same event is sent once per window, each channel at most 20 messages an hour (adjustable), failed sends are tried again (1 min … 6 h) and every delivery is logged; the administrator's page has the settings, the latest events and every delivery with "try again". Webhook, Slack, Discord and Telegram requests only go to public addresses; tokens and secrets are sealed in the database. Other features add events with one call (
Ctx.Notify). Every change is in the activity log.
Website protection: hotlink, leech, web firewall per website
- Hotlink protection — Control Panel → Security → Hotlink protection (and an overview tile): per website, stop other websites from showing its pictures, videos and downloads (default types jpg jpeg png gif webp svg mp4 mp3 pdf zip; pages can never be protected). The website,
www., its parked domains (which follow its settings) and up to 20 allowed websites with their subdomains always work; visitors without a referrer allowed or refused; others get 403 or go to a chosen address. Written as Nginx rules at server level (valid_referers), so it works in every web server mode; each change is tested (nginx -t,apache2ctl configtest) before the reload and the old rules come back if the test fails (agentsite-hotlink). - Leech protection — Control Panel → Security → Leech protection: per password-protected folder (Directory privacy), block a login used from more than N different addresses within an hour, for a chosen time, with 403 or a redirect address, and email the owner. A watcher in the 5-minute
hf-api samplereads the website's access log (agentleech-log); the block is a.htaccessrule (Nginx-only websites: the login leaves the password file until the block ends); "Unblock" ends it at once. - Web firewall per website — Control Panel → Security → Web firewall: the firewall on or off per website, the requests it caught (blocked or logged, 25 a page, from the website's ModSecurity log: agent
waf-events), and exceptions for false alarms: rule ids and now also paths (an Apache<Location>the firewall skips, agentwaf-path). Server Admin → System → Web firewall control: whether customers may change their firewall — a server default and a choice per package (staff always can, also through "Log in as"); the Security scan screen follows the same choice. Every change is in the activity log.
Team members (sub-users)
- Let other people into a Control Panel — Control Panel → Account → Team (and an overview tile): the owner invites people by email with a role — Full access, Websites & files, Email only, Databases only, Billing only, Read-only, or Custom (the screens you tick). They choose a password from a one-time link (48 hours), sign in on the same panel address with their email address, and have their own two-factor login and passkeys (My login); the owner can require two-factor login for the whole team. A member sees and can do only what the role allows — checked in the server on every screen, action, download and app sign-in (deny by default) — never the owner's own login, API tokens or the team settings. Files, DB Studio and webmail open only for the matching roles. Everything a member does is in the activity log with their name (also in Files and DB Studio). The owner changes roles, suspends, removes and sends links again; packages get a Team members limit (existing packages 0, new ones 3); staff see each account's team under Accounts; API tokens can be scoped to a role. "Log in as" by staff is unchanged.
Feature lists
- Choose which tools each hosting plan has — Server Admin → Accounts → Feature lists: named sets of customer features ("Email only", "Developer" …) given to packages (also in the package editor). Every customer screen is a feature (worked out from the panel's screens, so new ones appear by themselves, on by default), plus parts of screens: forwarders, autoresponders, spam filters, subdomains, parked domains, the IP blocker, WordPress installs, remote MySQL. A feature that is off leaves the customer's menu, the overview's tools and the search; its screens and actions answer 403, the token API refuses it, and Files / DB Studio refuse to open (DB Studio needs at least one kind of database). Changes apply at once. Resellers make their own lists for their customers, within the allowance the administrator gives them. Every change is in the activity log. Nothing assigned = every feature on, so existing servers are unchanged.
The server's own company name, the publisher in config, the "server is ready" email
- Company name at install — the installer asks the server's company name with the domain and email (
--company "…"; Enter keeps the suggestion, the domain's first label with a capital, e.g. Example for example.com). It is thecompany_namesetting (Server Admin → White label changes it): footers "© <year> <company>", the name the server's emails are signed with, the "server is ready" email, the install reports. The brand stays HF Panel until the White label screen changes it.update.shgives a server without a company name its own brand name (not the product's). - "Your server is ready" email — at the end of an install the administrator gets one email with the sign-in addresses, the admin username (never a password), the DNS records to add, the reverse DNS (PTR) to set and the next steps, in the administrator's language (
hf-api notify server-ready, once per server;--force,--dry-run,--to). Install only; a failure never stops the install (it becomes a to-do item with the command). - The publisher in one place —
source/config(COMPANY_NAME,COMPANY_DOMAIN, e.g. HostFog Ltd and hostfog.io) is the only place the publishing company is set: hf-api and the desktop app get it at build time (-ldflags -X,internal/company), the interfaces through Vite, and the scripts, deploy files and the website throughHostFog Ltd/hostfog.comfilled in bypackage.sh,make-setup.shand the website build. It is used only for what every server reaches at the publisher (licence server and shop, downloads, free tunnels, website). A server's own addresses, brand, email senders and footers come from its install answers. The download server's files arescripts/install/deploy/get-site.*, the website's nginx filefrontend/site/deploy/website.nginx.conf(built intodeploy-out/).tests/check-company.shfinds any literal name, domain or fixed IP left. - Edition never assumed — only an explicit
editionofficialis the official edition (licence server, licence shop, HQ); a missing or unknown value is the paid edition. The installer always writes it (officialonly for the official package withHOSTFOG_EDITION=official),update.shwritescommercialwhen it is missing, and convertingconfig.phpkeeps it. The publisher's own server: checkhf-api config get editionsaysofficialbefore updating (docs/PRODUCTION.md). - Restarts without a gap —
hf-api restart(used by the installer,update.shand the agent) hands hf-api's socket to the new process, which answers before the old one finishes its requests and exits (the service isType=notify; reload = handover). Panel pages no longer answer 502 for a moment while settings change or during an update.
New names: db., the mail names mail., smtp. and imap.
- DB Studio is at
db.<domain>(wasstudio.<domain>) — the app keeps its name; nginx, the certificate, the DNS list, the installer and the Control Panel's links use the new address.hf-server db-portal on [NAME]|off(the oldstudio-portalstill works).update.shmoves a server that already hadstudio.<domain>.dbandstudioare reserved account and tunnel names./studio/on the panel's port is unchanged. - The mail server has three names, all from the domain entered at install:
mail.<domain>is the MX of the server's own zone and of every customer domain whose mail is on the server, the name Postfix greets other servers with (and its banner) and the one the server IP's reverse DNS (PTR) must name; mail apps send throughsmtp.<domain>(587 STARTTLS, or 465 SSL/TLS) and fetch mail fromimap.<domain>(IMAP 993, POP3 995) — the autoconfig/autodiscover XML, the Email accounts screen, the dashboard, webmail's settings, the welcome email and the help article give those. All three are A records of the server's zone (hf-server dns-setup), on the mail certificate and in nginx, and Postfix takes mail for each. Choose another MX name with--mail-hostat install, or on an installed serverhf-server mail-name mail2.example.com; each name switches over by itself once its DNS points at the server. Customer domains whose MX points elsewhere are not touched, and customers' ownmail.<their domain>names stay as they are. mx.<domain>is no longer used — new servers never have it. On existing serversupdate.sh(agentmail-rename, thenmail-old) moves everything the panel manages frommx.<domain>(servers from before 1.0.0) orsmtp.<domain>(1.0.0 test installs) tomail.<domain>— Postfix, the certificate, the MX/autoconfig records of the zones it manages, in its database and in BIND — each name at once when it already reaches the server, else by itself once its DNS points here; then it removesmx.<domain>from the server's zone, the certificate, nginx and Postfix, unless a hosted domain whose DNS is elsewhere still has its MX onmx.<domain>: thenmx.is kept,update.shand Server health list those domains ("change their MX to mail.<domain> at their DNS provider; mx.<domain> will be removed on the next update once none use it"), and the next update checks again. Set the server IP's reverse DNS (PTR) tomail.<domain>at your VPS provider, or mail may land in spam:update.shprints the exact value (and says when it still namesmx.orsmtp.), Server health shows a warning with the fix and Mail health / Deliverability check it.- Port 465 (submissions, TLS from the first byte) next to 587, with the same rules — signed-in senders only, each only as its own address; the firewall opens it.
- Send-only mailboxes — Email accounts → Manage → "Send only": the mailbox signs in and sends, but mail to it is refused with a 5xx "This address does not receive mail - write to support@<domain>" (for no-reply addresses).
- Role addresses of the server's own domain —
postmaster@,abuse@,hostmaster@,webmaster@,dmarc@,tls@andsecurity@<domain>forward to the administrator (Server Admin → Settings: "Role addresses forward to"; default the admin email given at install). New servers get them,update.shadds the missing ones; an address that exists stays as it is. When the target isadmin@<domain>, the installer makes that mailbox (its password in the summary and in/root/hostfog-mailboxes.txt). The server's own zone gets DMARC (rua=mailto:dmarc@<domain>) and TLS-RPT (rua=mailto:tls@<domain>) records, and/.well-known/security.txt(Contactsecurity@<domain>, Expires a year out, renewed daily) is served on the panel's addresses. - The publisher's own mail (official edition only) —
scripts/install/deploy/company-mail.conf(local parts only: mailboxesadmin,support,no-replyandupdates— the last two send-only — and forwards ofsales@,billing@,info@… toadmin@) is applied to the server's own domain by the official edition's installer andupdate.sh, and byhf-server company-mail apply [file]; only missing addresses are made, passwords are shown once and kept in/root/hostfog-mailboxes.txt. The official edition's system emails come fromno-reply@<domain>with Reply-Tosupport@<domain>, release and update emails fromupdates@<domain>(Settings: "Update emails from"). - The old address keeps working —
https://panel.<domain>:8443still answers/api/v1/…,/licence/…,/billing/…,/node/…and/?r=tunnel-getexactly as before (installed desktop apps, billing scripts and licensed servers need no change).
The API's own address: https://api.<domain>/v1
- Every machine address under one name — the token API (
/v1/accounts,/v1/status,/v1/sync/sites,/v1/tunnels…, e.g.https://api.hostfog.io/v1/accounts), the desktop app's free links (/v1/desktop/…), the tunnel downloads (/v1/tunnel-get), the payment webhooks (/v1/webhooks/stripe,/v1/webhooks/paypal), the cluster (/v1/node/agent) and, on the official edition, the licence server (/v1/licence/check,trial,event,pubkey,release,download). On port 443 and on the panel's port (https://api.hostfog.io:8443/v1). Only machine requests are answered there (tokens, the device key, signatures, licence keys): no pages, no session cookies, no interface calls. The full old → new list:docs/API.md. - The old addresses keep working —
https://<panel>:8443/api/v1/…,/billing/webhook,/billing/paypal-webhook,/node/agentand/licence/…answer as before, without redirects, so installed desktop apps, billing scripts, webhooks set up earlier, cluster servers and licensed servers need no change. - New code uses it, with a fallback — the HF Server desktop app, the paid edition's licence check, updater, installer and install reports, and a main panel talking to its cluster servers try the new address first and the old one when it doesn't answer (servers not updated yet). The panel shows the new address on the API tokens screen and for the Stripe and PayPal webhooks (with the old address noted as still accepted), in the one-line tunnel commands and in the paid installer's download command.
- Set-up —
api.<domain>comes with the separate panels likefiles.anddb.(hf-server api-portal on [NAME]|off);update.shadds it to existing servers with separate panels. Add its DNS record (hf-server dns-setuplists it, the installer shows it before the install); the panel's certificate takes the name once it points here. - The browser apps stay on their own addresses — the panel, webmail, Files and DB Studio keep
/api/v1on their own names: a shared address would need cookies for the whole domain, which tunnel links (customer-controlled subdomains) could read or plant.
HQ, install reports and install emails
- Install emails — the licence server emails the licence owner when their server finishes installing (the addresses to sign in at) or when an install fails (the step and how to run it again); HQ → Settings can also email owners about updates and the administrator about new installs and failures. A report sent twice sends one email.
- Install reports (paid edition, part of the licence terms) — a paid-edition server with a licence key tells its licence server when an install starts, finishes or fails (with the step that stopped and the last log line, secrets masked), when an update finishes or fails, and when HF Panel is uninstalled: the key, version, host name, the server's own domain and company name, OS name and version, CPUs, memory and disk size (
POST /v1/licence/eventon the licence server's API address, else the old/licence/api/event). Sent in the background with a 6-second limit; an install, update or uninstall never waits for it or stops because of it. The official edition never reports. What is sent and why:docs/DOCUMENTATION.md, "Install reports". - HQ (official edition, administrators only) — the publisher's licence server gets Overview (licences, active servers, trials, paying, monthly revenue, downloads, installs and their success rate, charts per day, version adoption, recent installs and failures), Installs (every report, searchable, with each server's full history), Downloads (every installer download and release check, served or refused, with the reason) and Licensed servers (last check-in, version, accounts, OS, servers silent for more than a week), each with a CSV download; each licence shows its server's company and domain (from the install reports, with the history of company name changes) and can be searched by company. The licence server records downloads, reports and one check-in per licence per day; old records are removed nightly. Paid-edition servers have none of this.
- Licences for local addresses — an administrator of the official edition can make a licence (trial, paid until a date, or lifetime) for any address, including private, loopback, link-local, CGNAT and IPv6 ones, and move a licence to one; licensed servers report their own addresses with each check-in so a server behind NAT passes. The shop, trials and owners still need a public IPv4 address. Such a licence is only as unique as its address: for test and LAN installs.
Security fixes
- File Manager confinement — the File Manager's helper is now Go (
hf-api fm, run by the agent as the hosting account; the PHP helperhf-fm.phpand its php-cli call are gone, andupdate.shremoves the old file). Every path is resolved on file descriptors under the account's home —openat2withRESOLVE_BENEATH | RESOLVE_NO_MAGICLINKSon Linux 5.6+, otherwise one name at a time withO_NOFOLLOW— instead of PHP'sopen_basedirstring check:.., absolute paths, symlinks out of the home (or looping) and NUL bytes can't lead outside, and each change acts on the descriptors it checked, so swapping a folder for a symlink in the middle of an operation can't redirect it. The helper refuses to run as root. Saves stay atomic (temporary file in the same folder, now fsync'ed, old permissions kept, renamed into place) and never write through a hardlink. Unzip refuses, before writing anything, zip-slip names, symlink entries pointing outside the target, more than 100 000 entries or more than 10 GB, and stops an entry that inflates beyond its declared size; extracted files are written to a temporary file and renamed into place (never into a hardlinked inode, never through a symlink to outside the home). Small fixes on the way: tail of an empty log answers instead of failing, a zip that fails leaves no partial archive, unzipping an empty file no longer deletes it, copying a folder keeps its relative symlinks. - Signed releases — releases are signed with the HostFog release key (ed25519). The installer (
latest) checkshostfog.zip.sha256.sigbefore it unpacks anything and installs the paid edition only when the package's signedMANIFESTmatches every file; the panel checksrelease.json.sig; the server agent checks the signedMANIFESTagain before an update runs. - Database restores — staging copies, site clones, staging pushes, backup restores and server moves import MySQL dumps with client commands and
LOAD DATA LOCALswitched off, without database switches, and with every foreignDEFINERreplaced by the database's own user. PostgreSQL backups are restored as the account through a role acting as the database owner, never as the superuser. - The server's own names — a website, parked domain, remote domain or new DNS zone can't take the panel's, portals', mail, webmail, FTP or nameserver names, or a name under the tunnel domain.
- Apache — hidden files and folders (
.env,.git/…) are refused in Nginx + Apache mode too (/.well-known/stays open), and a website's.htaccesscan no longer pass PHP settings to PHP-FPM or pick the script another account's PHP-FPM pool runs. - MariaDB listens on 127.0.0.1 unless an account allows remote database access.
- Downloads checked — wp-cli must match the checksum its publishers put next to it.
- CSV exports neutralise formulas — the activity log and the HQ installs/downloads/servers exports (which carry data reported by remote servers) now prefix any cell beginning
=,+,-,@, a tab or a carriage return with an apostrophe (numbers kept as they are), so a spreadsheet can't read an exported value as a formula (CSV/formula injection), matching the VAT report's existing guard. - Two-step codes are single-use even under a concurrent replay — an authenticator code and a backup/recovery code are now spent with a compare-and-swap, so two sign-in sessions submitting the same code at the very same moment finish at most one. The per-session file lock already serialised one session's requests; this closes the window where two separate sessions for the same login (the attacker holding the password and one live code) could each read the same
totp_last/code list and both be accepted. Covers the account login, team-member login and Files/Studio login paths. - Affiliate payouts can't be doubled by simultaneous requests — a payout request takes the affiliate's account lock and is exactly the commissions it claims, and "paid"/"reject" are compare-and-swap on the payout's status in the same transaction as the credit, so two clicks or two administrators at once credit an affiliate once.
- Panel-backup restore can't plant a setuid-root file — restoring a panel backup unpacks the system archive as root; it now refuses any member with a setuid, setgid or sticky bit (only plain files and folders under the panel's own places are accepted) and extracts dropping those bits, so a tampered backup can't drop a setuid-root program into
/etc/hostfog. - Config-history restore follows no symlink — putting a saved config version back writes the file through its folder's descriptor with
O_NOFOLLOWand verifies the result is the same regular file, so a service user whose folder HostFog manages (e.g. the DNS zone folder) can't swap the target for a symlink and have root write through it. - A cap on open live streams — the live-chat event streams and the web-terminal connection are limited per session, per website-chat visitor, per address and in total; a request over the limit is answered 429 (and retried shortly), so one browser or address can't tie up the server with endless streams.
- Backup restores check they fit — before a full account backup is unpacked (including one fetched from the customer's own remote storage) its unpacked size is checked against the free disk, so a small archive can't be crafted to fill the disk.
- Smaller fixes — admin emails with an apostrophe;
latestpasses its options on toupdate.sh; the trial email is sent as proper JSON; licence checks use IPv4; uninstalling without--purgekeeps mailing lists and webmail data and switches off the webmail's master login; PHP-FPM keeps the distribution'sProtectSystemwhere the agent runs as its own service; one list of what never goes into a package (development files,keys/).
For developers
- Source reorganised —
frontend/(every interface),backend/(panel, mail, agent, desktop app) andinstaller/; one controller per panel screen; the old PHP pages are removed. Servers are unchanged:/usr/local/hostfogand the package keep their layout.
Review fixes: panel screens (abuse … mail health)
- Git version control: a new repository's deploy folder is checked like a changed one (a website folder inside web/, never under repositories/), before anything is cloned.
- Settings forms (AI helper, White label, invoice details, licence shop, email to ticket) check every field first: a refused form no longer keeps part of the changes (an AI helper key saved while the model was refused, for example).
- DNS templates: a too long description is marked on the description field, not the question's.
- Accounts › bulk actions: a malformed ticked-accounts list is refused instead of being read as account 1.
- Tax report and discount-code CSV files: cells starting with a tab or carriage return get the protecting apostrophe too, as the activity log's export does.
- Firewall: the activity log names the protocol actually opened.
Review fixes: panel services (help desk … reseller addresses)
- Domain sales: a paid domain renewal or certificate order that failed at the provider (or timed out) no longer gets a new order and invoice — charged to the card on file — every night until the administrator retries or cancels it; the customer can't order the same thing again meanwhile either.
- MongoDB databases: the connection string the API returns works with every password (the random ones often have @, # or %, which broke it).
- Panel backup: a wrong passphrase on a small archive says so, instead of "the file is cut short".
- Transfer from another host: an old server's name that resolves to a private address written as IPv6 (::ffff:10.x …), or to the shared 100.64/10 or multicast ranges, is refused like the other private addresses.
- Emails: a small-print link with a query in its address keeps a working address.
- Pages with the brand mark: a brand starting with an accented letter shows that letter instead of a broken character.
Review fixes: panel screens (mail limits … WordPress updates)
- Node.js, Python and Ruby apps: a website runs one app of any kind; the Node.js screen no longer puts an app on a website that already runs a Python or Ruby app, nor the Python screen on one with a Ruby app.
- Redirects & IP blocks: when the web server refuses the new rules, the website's rules go back exactly as they were — a replaced whole-site redirect or a removed rule is no longer lost.
- Packages: renaming a package to another package's name is refused on the name field instead of "Something went wrong".
- Status page and HF Desktop share link settings: a refused field no longer leaves the other settings half-saved (the status page switched on, for example).
- Domain sales: exchange rates sent through the API with upper-case currencies are used (they were stored but never found).
- SSL/TLS: a wildcard certificate is refused for an account on another server of a cluster, as the screen says.
- Servers: a joined server that has not reported its panel name shows the address it was joined with instead of an empty name.
- Move accounts: warnings stored by an older version show as a list instead of breaking the page.
Review fixes: panel services (abuse desk … server health)
- Cron job monitor: a job whose command has a % sign (or is too long to be monitored) is no longer reported as "Cron job did not run" after every scheduled time — it runs without the monitor and leaves no record.
- Activity log: the "Support" filter no longer lists changes made by the customer's own team members (they show as "You").
- HF Server desktop links: the server reads at most 64 KB of a request to its token-less desktop addresses, so a huge request can't use up its memory.
Review fixes: panel services (screens … WordPress vulnerabilities)
- Resource usage: the account's disk space limit is shown and a full (or nearly full) disk raises the notice; the limit was always read as "unlimited".
- Visitor statistics: a reading of the access log that stops half-way (a server error) no longer counts the lines it already read a second time on the next run.
- Visitor statistics: a typed date range that lies in the future ends today instead of after it.
- Sign-in: a "set up two-step sign-in first" hold left in a browser's session no longer holds the next login made there.
- Staff tags: a tag "0" is kept when the account's tags are shown.
Review fixes: server agent, installer and OS layer
- Rocky Linux, AlmaLinux, CentOS Stream: the DNS server now serves the hosted zones — the installer took a comment in
named.conf.localfor the include of the zone list, so no zone was loaded;update.shadds the missing include on servers installed before. - Rocky Linux, AlmaLinux, CentOS Stream: the shared Redis on port 6379 is no longer switched on (every account runs its own instance); it was started at install and could not be switched off under its Debian name.
- Rocky Linux, AlmaLinux, CentOS Stream: the jailed SSH shell (Account isolation) finds the SFTP server in
/usr/libexec/openssh, so it can be switched on and SFTP starts in~/web. - Rocky Linux, AlmaLinux, CentOS Stream:
update.shinstalls restic and rclone with dnf when they are missing; uninstalling removes the installer's/usr/bin/phplink; the extra PHP repository's release package is refused whenever its signature check says "NOT OK". hf-server files-portal off/db-portal off/api-portal offname the panel's port when the server keeps the build's default (they printedhttps://panel.example.com:/files/).- Git: a branch name starting with "-" is refused for checkout and clone.
- DNS cluster:
dns-secondary-setupincludes the zone list when no file includes it yet (that line never ran).
Review fixes: hf-api services (Web Disk, Files, sessions) and HF Server
- Web Disk: a login kept to one folder can delete and rename its own subfolders called web, logs, tmp or .ssh (only the account's own home folders of those names are protected).
- Web Disk: a file helper started just as the idle sweep ran is now stopped once idle, instead of being left running for good.
- Sessions: a damaged session file is refused instead of ending the request with a server error.
- HF Server: the mail catcher no longer closes the whole app when a program sends "MAIL FROM:" (or "RCPT TO:") with no address.
Review fixes: panel UI
- Forms in row menus (accounts, servers, domains, SSL/TLS, email, server mail, malware): a refused field keeps the menu or dialog open with the message on the field, instead of closing it and losing what was typed.
- Forms no longer empty a field the server refused when it isn't secret: the redirect type (301/302), discount codes, the SSH key name and the abuse note stay as typed.
- A field error is shown once, under its field, even when the page has its own error text; the error texts on My brand, Feature lists, Reseller privileges and Server profiles are translated and read out by screen readers.
- Opening a screen just after the panel was updated reloads the page once instead of showing a blank screen; a screen that still can't load says so.
- Leaving a screen while it reloads no longer shows the old screen's data on the new one.
- Escape and Tab act only on the top dialog when dialogs are stacked; Shift+Tab stays inside the dialog.
- Terminal: when the terminal can't be loaded it says so instead of "Connecting…" for ever.
- Cron jobs: the "next runs" preview no longer shows error notices while a schedule is half typed.
- Cron monitor: Save can't be pressed twice.
- Import: .tar.zst backups can be picked in the file dialog.
- Status page: the component names and states in the banner are translated.
- Abuse templates: each template's fields have their own ids (labels pointed at the first template).
- The "More" menu buttons of servers and of Node.js, Python, Ruby and staging sites have a name for screen readers.
- Prices and revenue on Accounts, Resellers, Packages and the overview are shown in the billing currency (they always said £).
- A lost connection during an action is reported in the panel language; the Business charts and the busiest accounts' period on Resource usage are translated, and so are the agent errors on Perl, PEAR, Composer, PHP extensions and Developer tools.
- Copy buttons show "copied" only when the browser really copied the text, and leave no error behind when it refuses.
- Docker apps: pressing Install twice no longer starts two installs.
- IP addresses: each server's pool fields have their own labels; Server health marks and the activity log search have names for screen readers.
Review fixes: framework (routing, token API, agent calls, CLI jobs)
- Token API: tokens of a login that had ever changed its password were refused as "This login is suspended." (also new tokens made after a reset); they work again. A suspended login's tokens are still refused.
- Speed test: a website's compressed answer is unpacked only up to the body limit (256 MB), so a small "gzip bomb" can't use up the panel's memory.
- Server status shown on the screens: after a change that failed half-way, or one that took an uploaded file (database import, plugin), the status read during it is no longer reused for a few seconds.
hf-server panel-restore: when the copy can't be put in place of the database it says so and exits 1, instead of "Restored." with nothing changed.- The 5-minute job (
hf-api sample) runs once at a time: a run still busy makes the next one skip, so queued emails and moves are never handled twice. - Malware scan and off-server copies: "running" is cleared however the run ends, so the Security and Backups screens don't show a scan or copy running for ever after an error.
- Licence shop: after paying with PayPal the buyer comes back to the shop's own address (as with a card), not the panel's.
Review fixes: Mail, Files, DB Studio, HF Server app, website, docs and integrations
- Mail: Contacts, Calendar and Encryption keys show an error when they can't be loaded, instead of an empty page; a form error already shown next to its field is not repeated in a notice; opening an encrypted message with an unlocked key reports a failure instead of doing nothing; the "back" link and two encryption warnings are translated.
- Files: a slow folder listing can no longer replace the folder opened after it; a share link's Copy says "Copied" only when it was copied.
- DB Studio: while another table or collection loads, the previous one's rows and buttons are not shown (they acted on the new name); an index error clears when another collection is chosen; the "not valid JSON" message is translated.
- HF Server app: Quit reports a failure instead of closing the window; "Stopped sharing." appears only when sharing really stopped; a failed attachment download says so; a slow table page can no longer replace a newer one.
- Website and docs: Copy buttons say "Copied" only when copied and get their label back after quick presses; docs search reports an index that couldn't be loaded and tries again on the next search.
hfpanel(bash SDK): an idempotency key or token with a quote or backslash is sent as given; one with a line break is refused.
Fixed in the review of the Go backend
- Installer — switching a server to the Go-only set-up converts the separate-panels site too, so the PHP pools are removed and webmail doesn't answer 503 until the next update; the webmail data is handed to hf-api before it is restarted; the autoconfig snippet is correct after the first update, not the second; the mail user's real uid/gid is used (a pre-existing
vmailuser with another uid left mailboxes Dovecot refused); a package built with a relative folder no longer misses the Go programs; a panel database created by root is handed to the panel; uninstalling no longer tries to remove the Node.js settings folder. - Payments — a Stripe or PayPal webhook no longer leaves account locks open (every later account creation blocked until a restart); a Stripe event with no customer id no longer matches the first account without a card.
- Webmail — a mail server that is down or restarting at sign-in is reported as unreachable instead of as a wrong password (which also counted towards the lockout). An e-mail with absurdly deep HTML can no longer crash the server; replying to a crafted HTML e-mail no longer takes minutes; filter rejections are reported at once instead of after 20 s.
- Checks — a website that closes the connection without answering is reported as "Empty reply from server", as curl reports it.
- Downloads — cancelling a backup or database download no longer leaves the request (and a root agent process) stuck, which locked that user out until a restart.
- Sessions — old session files are removed again (PHP's own clean-up no longer runs); uploads over 16 MB no longer leave a copy in /tmp.
- File Manager — saves and uploads are written to a temporary file and swapped in only when complete, so a full disk can't truncate a file; folders over 5,000 entries say so.
- Settings — the Go server re-reads its configuration when it changes (separate panels, mail/FTP names after SSL), so no restart is needed.
- Server scripts — the live log and mail queue views work on large logs and queues; backups refuse to run when MariaDB isn't answering instead of silently skipping every database; long jobs no longer hold the agent lock, so the panel stays usable during nightly backups; a failed update can't leave a server half-switched.
- Interface — a failed install dialog can be closed; a failed form keeps what was typed and stays on its tab; server messages are translated; the language switch keeps the current view.
- Desktop app — the two addresses it reads server status and sites from exist again.
- Weekly restore test — now runs (an old comparison bug meant it never did).
The backend in Go
- One Go program,
hf-api— HF Panel, Mail and Files are now answered by one Go server (backend/api, servicehostfog-api, userhostfog): every screen, sign-in (password, two-factor, passkeys), the pages outside the app, the webhooks, the token API and the webmail. It ships in the package for amd64 and arm64 (pure Go, no C compiler) and installs as/usr/local/hostfog/bin/hf-api. The interfaces stay React + TypeScript; nothing changes for the people using them, and nobody is signed out by the update (Go reads the same session files). - One API at
/api/v1/— every app answers JSON at/api/v1/…: a screen's data isGET /api/v1/<screen>, a buttonPOST /api/v1/<screen>/<action>; the webmail's calls are/api/v1/<name>[/<sub>]; Files keeps its/api/v1. Billing systems and scripts use the same addresses withAuthorization: Bearer hf_…. Answers, status codes and the token API:docs/API.md. - The panel's jobs are
hf-apicommands —hf-api migrate,cron,sample,licence,admin(behindhf-server), and the agent's background workershf-api scan,move,pull,import,offsite, with the same arguments, output and exit codes as the PHP scripts they replace. - Checked against the PHP panel, then PHP removed — each screen, call and job was compared with the PHP code it replaces: the same answers, database changes and server commands. The PHP panel and webmail are gone from the source and from the package; behaviour that must not change is kept by Go tests with recorded answers.
- Go only —
install.shsends the panel's port, the separate panels,/mail/,webmail.<domain>, customers' webmail domains and the mail apps' autodiscover addresses to hf-api, makes no PHP-FPM pool for the panel or the webmail, and runs cron,hf-serverand the agent's workers ashf-api <command>. PHP stays only for the customers' websites. - Settings in JSON —
/etc/hostfog/config.jsonand/etc/hostfog/mail.json(root:hostfog 0640) replace the PHP filesconfig.phpandwebmail.php. Scripts, the installer and the agent read and change them withhf-api config get|set|set-text; nothing runsphpto read a setting any more. - DB Studio replaces phpMyAdmin — the panel's database buttons open DB Studio, HostFog's own database browser for MySQL/MariaDB and PostgreSQL (a hidden localhost-only MySQL user per database, a PostgreSQL login role valid for 4 hours). phpMyAdmin is no longer installed: no download, PHP-FPM pool,
hfpmauser or nginx location;update.shremoves it from older servers. - Updating a server from before 1.0.0 —
update.shchecks first that the new hf-api runs on the machine, then convertsconfig.phpandwebmail.phpto JSON once (same owner and mode; the old files are kept as*.php.converted), rewrites nginx and cron to hf-api, removes the panel's and webmail's PHP-FPM pools, removes phpMyAdmin (its pool, files, user, nginx locations, one-click sign-on database users and setting) and the oldshared/folder. Sessions survive; a second run changes nothing (docs/GO-MIGRATION.md).
New control panel interface
- Every screen redrawn in React — the Server Admin, Reseller Panel and Control Panel now run as one fast app (React 19 + TypeScript): moving between pages no longer reloads the whole page, forms show their errors next to the field, risky actions ask for approval in a dialog, installs show their live log in a dialog, and the top bar has a notifications bell (the same notifications the phone app shows). 92 screens moved, including sign-in (two-factor, passkeys), the forgotten-password pages and the mailbox pages opened from webmail ("Forgotten mailbox password" and "Not spam" from the daily spam summary). Pages look and read the same as before, in all 21 languages, light and dark, right to left in Arabic, Urdu and Persian, and on phones.
- Same rules underneath — every screen keeps the same server rules as before, so access levels, ownership checks, read-only staff, two-step approvals, password re-checks, rate limits, one-time links and codes and the activity log are unchanged. The interface's own API answers only the panel's own pages (same origin, a CSRF token on every call).
- Nothing to install on the server — the interface is built into the package; servers never need Node.js. A few addresses that are not screens stay plain pages on purpose, answered by hf-api (one-click unsubscribe, one-click webmail sign-in, single sign-on links, the phone app's push and offline pages, the tunnel download, the order and licence shop pages).
- Fixed on the way — the browser checks of the app name (Node.js, Python, Ruby apps) and WordPress admin username fields work again in current browsers (their patterns were refused as invalid); on phones, Support tickets, DNS cluster, Mailing lists and PostgreSQL no longer make the page scroll sideways (wide tables and long commands scroll or wrap inside their card).
Fixes
- Deleted accounts' backups and usernames — when an account is deleted, backups left under its name are moved aside to
/var/backups/hostfog/.deleted/<username>-<time>(root only) before an account of the same name is made again, so a new account never sees an earlier one's backups; a username whose deleted account's backups are still kept (deletion at the customer's request, until the purge date) stays taken until they are purged. The administrator decides what happens to the set-aside copies; nothing is deleted by itself. - Translations — about 800 interface texts that were still in English in the other 19 languages (server messages such as "No such domain." and "Forbidden", the Cloudflare, DNS setup, Node.js, Ruby, PostgreSQL, Redis, transfer and tunnel screens, Mail's calendar and PGP labels, the activity log's "You", the confirmation questions and progress titles of about 140 buttons, the mail health rows, the uptime alert titles, the registrar providers' fields, the software version notes) are translated;
tests/check-lang.pynow also checks the source: everyt('…'),confirmquestion, progress title and empty-list text in the interfaces and every literal message the Go code sends must have a text in its dictionary (tests/check-lang.allowlists the product names and examples that are no texts). - Overview capacity card — the "Disk full in about N days" card is left out when nothing runs out soon instead of being shown empty (the welcome wizard's card the same way).
- Dropdown menus in wide tables — a row's menu opened inside a table that scrolls sideways is placed on the screen (below the button, above it when there is no room below, with a height limit when neither fits), so the table's scrolling box no longer cuts it off.
- Uninstall —
hf-server uninstallalso removes the Nginx file of the names that are not web addresses (hostfog-closed.conf), the panel's IPv6 boot service (keeping the websites' own addresses re-added at boot by a plain service when the websites stay) and takes the monitored cron jobs out of the cron monitor's wrapper so they keep running;--purgealso removes the site firewall, WebP and Dovecot files.hf-server licence syncdecides whether email and DNS were set up by what HostFog set up (the applied feature list and the install options), not by which packages happen to be installed.bash latest/ get.sh remove/home/hostfog-installafter a successful install or update; setup.sh says why the paid edition could not be built. - New names — the server panel is now HF Panel (with Mail for webmail and HF Tunnel for tunnels); new installs use HF Panel as the default brand, and a brand you already set is kept.
- hf-admin is now hf-server; the old name still works.
- One colour scheme everywhere — the control panels, webmail, desktop app, website, status, maintenance and suspended pages, emails, tunnel page and browser terminal now share the same HostFog colour (#d87858) and the same greys; the desktop app icons, website pictures and social preview image use it too, in light and dark. Webmail avatars use the panel's avatar colours.
- Webmail redesign — new sign-in page; a cleaner inbox with sender avatars, Today / Yesterday / This week groups and an icon toolbar that wakes up when messages are selected; a sticky toolbar while reading, with reply and forward buttons beside the sender and under the message; plain-text mail in a normal font; a menu under your address (Settings, Control panel, Log out); a Compose button on phones; works in dark and light.
- DB Studio — the database browser as its own app — like Files: its own interface (React + TypeScript) at
db.<domain>(and/studio/on the panel's port), answered by hf-api (X-HF-App: studio, JSON API at/api/v1/…:databases,database,table/rows,table/structure,sql,row,row/save,row/add,row/delete,unlock,import,export). Customers sign in with their panel login and two-factor code, with the panel's lock-outs and checks; the Control Panel's menu entry, overview tile and Browse / Open DB Studio buttons open it in a new tab already signed in (a one-time link that works once, within 2 minutes, and never in another app), at the database. An administrator or reseller who uses "Log in as" opens the customer's databases with their own name in the customer's activity log. Every safety limit stays: a hidden login per database (never root), its rights checked before use, 30 s per statement, 1,000 rows, imports up tostudio_import_mb. The panel's DB Studio screen is removed;/?r=studioand/?r=pmaopen the app. Its address and set-up: "DB Studio is atdb.<domain>" above. - Files — the File Manager as its own app — like Mail for email: a new interface (React + TypeScript) and its own service written in Go, with a JSON API at
/api/v1/…(POST /api/v1/login,GET /api/v1/files?path=/web, upload, download, edit, rename, copy, move, permissions, zip, extract, delete; API tokens for scripts with"token": true). Onfiles.<domain>and at/files/on the panel's port; "File Manager" in the Control Panel opens it already signed in (a one-time link), and an administrator who used "Log in as" sees the customer's files with their own name in the activity log. New: drag files onto the list to upload them, with progress and Cancel; tick rows to zip, move or delete them; sort by name, size or date; filter the folder; picture preview; permissions as ticks; a text editor with line numbers, Ctrl/⌘ + S and a warning before leaving unsaved changes; the websites' folders in the side bar. Sign-in, two-factor, lock-outs and suspended accounts follow the panel's rules exactly, and every change still runs as the hosting account, inside its home only. The old File Manager screens in the panel are removed; their addresses (/?r=files,/?r=file-edit) open Files.hf-server files-portal on [NAME]|off; servicehostfog-files. - 14-day trial from the installer — at the licence question of
bash latest, press T, enter an email and the trial key arrives on screen (and by email). One trial per server IP and per email, kept even after a licence is deleted. - More operating systems — HostFog now installs on Ubuntu 22.04, 24.04 and 26.04 LTS and on Debian 12 and 13. Extra PHP versions come from packages.sury.org on Debian. The installer checks every required package before it starts, and leaves out (with a note) optional parts a release doesn't package.
- Browser terminal everywhere — Debian 12 has no ttyd package and Ubuntu 22.04's is too old; both now get ttyd's official build (1.7.7, checksum pinned), so the terminal works on every supported system.
- Installer without a keyboard — runs started by automation or cloud-init (no terminal attached) no longer stop silently at the logo.
- Software versions (Server Admin → System) — the installed version of Nginx, Apache, MariaDB, PostgreSQL, Redis and Node.js, and every version their official sources publish for this Ubuntu release, with Install, Switch and Update buttons and a live log. Settings and databases are saved first; Nginx goes back to the previous version by itself if the new one doesn't accept the configuration.
- PostgreSQL databases for customers — create a database with its own user, import and export, change the password, delete. Included in backups (full and single-database restore), locked while an account is suspended, and counted in the package's database limit.
- Every kind of JavaScript app (Node.js apps) — static React, Vue, Angular, Svelte, Gatsby and Astro sites are built and served with client-side routing; Next.js, Nuxt, Remix, SvelteKit, NestJS, Express, Fastify and other server apps run as services. "Detect from package.json" fills in the type and commands; "Build & deploy" installs, builds and puts it live while you watch. npm, yarn and pnpm.
- Installer checks the machine first — it stops with a plain message on an unsupported Ubuntu release, a container without systemd, an unsupported CPU type, less than 1 GB of memory or less than 5 GB of free disk.
- Spam guard (Mail health) — when an account suddenly sends far more email than usual (at least 300 recipients in an hour, and 5× its busiest hour of the last 7 days; both adjustable), all its sending is paused — from mailboxes and from its websites — while receiving keeps working. The administrator gets an email and the customer a notice; "Allow sending again" lifts it, and any account can be paused by hand.
- Mail queue (Mail health) — the email waiting to go out, with when, from, to and why it waits; retry or delete one message, retry everything, delete everything, or delete all mail from one sender (with the account it belongs to).
- Read-only staff (Staff logins) — add more administrators with full access or read-only. Read-only staff see every admin page and can answer support tickets and check support PINs, but can't change or delete anything, log in as a customer or download data. New staff choose their own password from an emailed link; giving full access asks for your password again.
- Download all your data (Backups) — customers make one .zip of everything in plain folders: website files, each database as an .sql file, every mailbox (Maildir) and their cron jobs, with a short README. Ready for 24 hours.
- Domain expiry reminders — each customer domain's registration end date is looked up in the registry's public RDAP service (weekly, daily when it's close). Customers get a reminder 30 and 7 days before, and the Domains page shows "Renew by" or "Registered until".
- Weekly summary for administrators (Server health) — every Monday one email with what needs a look: SSL certificates, websites that were down, backups and restore tests, paused senders and the mail queue, accounts nearly full, overdue invoices, domain registrations ending and server health problems. "Send it now" for a preview.
- Staff notes and tags (Accounts → Manage) — private notes and tags on a customer ("vip", "bank transfer"). Tags show on Accounts with a filter; notes and tags show on the customer's support tickets. Customers never see them.
- Follow-up emails for new customers — on day 3 and day 7, a customer who hasn't finished the Getting started steps gets one short email listing what's left, with a link to each (Settings).
- Saved replies for tickets — answers support gives often, inserted with one click into a reply, in the customer's language when one is written;
{name}and{brand}are filled in. Resellers keep their own. - Control panel data backups — every night the panel copies its own data (logins, accounts, packages, invoices, settings, tickets and history, with its settings file inside) to
/var/lib/hostfog/panel-backups, keeps 14 days (the newest 3 always), and copies it off the server too when off-server copies are on. Backups → Control panel data: make a copy now, change how long copies are kept, download one (asks for your password again). Put one back withsudo hf-server panel-restore <name>; the current data is kept next to it. Alsohf-server panel-backupsandhf-server panel-backup. - Unpaid invoice reminders — for invoices made by hand: a reminder 3 days after the due date and a last reminder after 7 days (with the invoice attached, your bank details and the payment reference), then the account is suspended after the number of days you choose (14 by default; 0 = never) and you get an email. Marking the invoice paid or void switches the account back on, but only when an unpaid invoice suspended it. Overdue invoices show as "Overdue · N days" for you and the customer. Billing → Unpaid invoices.
- File limit per package — Packages → Files sets the most files and folders (inodes) an account may have, email included (0 = no limit, enforced with disk quotas when they are on). Disk usage shows the count, and the customer and admin get a warning at 90%, at most once a week.
- Scheduled maintenance mode — Maintenance mode → Schedule: choose when the "back soon" page switches on and off by itself (for example overnight), in the customer's own time zone.
- Mailbox nearly full — at 90% of its size a mailbox gets an email (and its recovery email too) saying how full it is and how to make room, at most once a week, so mail doesn't start bouncing. The account owner gets a notice with a link to give it more space.
- Daily spam summary — one email a day listing what went to Junk in the last 24 hours, with a Not spam link for each message that moves it back to the Inbox (links work for 7 days). Switched on for all mailboxes under Email accounts → Spam filters, or per mailbox in webmail (Settings → Daily spam summary).
- Webmail two-factor login — Settings → Two-factor login: scan a QR code with an authenticator app, then webmail asks for a 6-digit code after the password; ten one-time backup codes; five wrong codes signs out. Mail apps keep using the password; opening webmail from the control panel counts as signed in.
- New sign-in alert — a mailbox gets an email when someone signs in to webmail from a new place (an IP not used in the last 90 days), with the time, IP address and browser.
- Maintenance mode per website (Websites → Maintenance) — one click shows a branded "back soon" page (your title, message and expected return) to visitors while the customer works on the site, with status 503 so search engines wait. A preview link lets the customer and their designer see the real site on their own device; SSL renewals keep working.
- Support PIN — every customer has a 6-digit PIN on their Profile (with a New PIN button) to read out on the phone. Admins and resellers check it under Accounts → Check a support PIN, and it shows on the customer's support tickets; checks are limited and recorded in the activity log.
- Business overview (admin and reseller) — charts of new accounts, cancellations, active accounts and monthly revenue over the last 12 months, with this month against the last. Resellers see their own customers.
- Webmail on the customer's own domain — Email accounts → Domains → Webmail address switches on
webmail.theirdomain.comwith its own free SSL certificate. The DNS record is added when DNS is managed here; if the name doesn't point here yet, it is tried again every 30 minutes and the customer gets an email when it is ready. - Always the newest styles and scripts — the panel and webmail now link their CSS and JavaScript by file version, so browsers load the new files straight after an update.
- Webmail in 21 languages — English, Spanish, Portuguese, French, German, Turkish, Arabic (right to left), Hindi, Italian, Dutch, Polish, Russian, Ukrainian, Chinese, Japanese, Korean, Indonesian, Vietnamese, Bengali, Urdu (right to left) and Persian (right to left): menus, pages, messages, confirmations and dates. The language follows the control panel's choice (the same cookie), else the server's default language, else the browser's; a picker is on the sign-in page and under Settings → Language. Translations live in
mail/lang/; the mail itself is never changed. - Weekly speed test — every website is tested once a week (a seventh each night) and the customer gets an email when its score drops by 10 points or more, with the biggest tip. Monthly reports always have a recent score. Settings → switch it off for everyone.
- Bulk actions (Accounts) — tick accounts (or all) and suspend, unsuspend, change plan, send a message (optionally even to customers who switched off news) or delete them; deleting asks for DELETE and goes through two-step approval. Works for resellers on their own customers. Every bulk action is in the activity log.
- Customer activity log (Account → Activity log) — sign-ins and every change in the hosting account over the last 12 months, by the customer or by support (through Log in as or from the admin/reseller panel), with when, who and from which IP; filters for sign-ins, security, changes and support, and a CSV download. Older entries are linked to their account when the panel updates.
- Approval for resellers — Settings → "Resellers need an administrator's approval…" makes a reseller's account deletes and whole-backup restores wait for an administrator (who gets an email); Resellers → Approval sets it per reseller (default, needs approval, no approval). Requests that wait for another administrator now stay open for 24 hours.
- Webmail: filters & forwarding — Filters is now Filters & forwarding. Forward all mail to up to 3 addresses, keeping a copy or not (spam is never forwarded). Filters get a name, several conditions (From, To/Cc, Subject, mailing list, size, attachments; contains, doesn't contain, is, starts with, ends with) matched all or any, and up to four actions (move, copy to folder, mark as read, flag, forward a copy, forward without keeping, delete); filters can be edited, copied and switched off. Older filters keep working. Forwarded mail now uses the mailbox as sender, so it passes SPF at the receiving end. The control panel's Forwarders tab shows which mailboxes forward or filter in webmail.
- Webmail dark mode switch — a moon/sun button in the webmail top bar and Auto · Light · Dark under Settings, remembered on the device (shared with the control panel on the same address).
- Speed test (Metrics → Speed test) — loads a website the way a browser does and gives a score from 0 to 100: server response time, page cache, compression, HTTPS and the http→https redirect, page size, large images, WebP, number of files, browser caching, HTTP/2, PHP version and Redis for WordPress. Each tip says what to do and links to the page that fixes it. Score history per website; tested from the server itself, so it works before the domain points here.
- Monthly report — on the 1st of each month every customer gets an email about the month before: visitors (with the change on the month before), pages viewed, traffic, uptime of monitored websites, space used, email and databases, the newest backup and restore test, SSL and the speed score of each website. In the customer's language. Customers can open it for any of the last 12 months in the panel (Metrics → Monthly report), email it to themselves, and switch the email off (Email notifications → Monthly report); admins can switch it off for everyone (Settings).
- Two-step approval for risky actions — deleting a hosting account or a reseller, restoring a customer's whole backup and switching off someone's two-factor login now need a second step. Settings → Risky actions: ask for the password (or two-factor code) again (the default), require another administrator to approve (they get an email; falls back to the password when there is no other administrator), or only "are you sure?". The request is held exactly as sent, works once and ends after an hour (24 hours when it waits for another administrator); every request, approval and rejection is in the activity log (admin → Approvals lists them).
- Passkey sign-in — sign in with Face ID, Touch ID, a fingerprint, Windows Hello, a phone or a security key (Password & security → Passkeys; "Sign in with a passkey" on every sign-in page). Nothing to type and it can't be phished; counts as two-factor login. With separate panels one passkey works on all of them. Standard WebAuthn (ES256/RS256), only public keys stored.
- Dark mode — follows the computer's light/dark setting, with a moon/sun switch and Auto · Light · Dark in the user menu. Webmail follows the computer's setting too.
- Public status page —
https://<panel>/status(or its own address withhf-server status-page on): websites, email, DNS, databases and the panel, checked every 5 minutes, with 90-day history bars, current incidents, planned maintenance and past incidents, plus/status.json. Admins post incidents and maintenance with updates (admin → Status page) and can email customers; maintenance starts and ends by itself. Linked from every panel footer and sign-in page. - Restore a single file from a backup — Backups → Restore one item now browses the backup folder by folder: download any file, Restore as a copy (next to the current one as
name.restored-<date>, nothing overwritten) or Put back, for files and whole folders; databases and mailboxes as before. - Errors explained (Logs) — a website's PHP and web server errors grouped, counted and explained in plain words with what to do (memory, timeouts, missing extensions, PHP version problems, typos, database passwords, redirect loops, blocked visits…), naming the plugin or theme involved; "Ask the AI helper" sends the exact message.
- Image optimiser — makes a website's JPEG and PNG images smaller (Smaller = quality 82, or Lossless), skips images already done, and can add WebP copies that Nginx serves automatically to browsers that accept WebP (originals stay; one click removes the copies). Tools are installed the first time.
- Copy a website — a full copy of a site (files and its own new database, same PHP version) to a new or existing domain or subdomain; WordPress addresses are rewritten.
- Resource usage — 30 days of server history: CPU, memory, disk and network traffic in/out, alongside the busiest accounts.
- Weekly backup restore test — every account's newest backup is read from start to end (archive complete, files and settings present, every database dump finished); a seventh of the accounts each night. Results per account on the Backups page, in Server health, and an email to the admin when one fails. "Test the newest backups now" and per-account Test.
- Discount codes and free trials — admin → Billing makes discount codes (percent or amount; first payment, a number of months or every payment; use limit and end date), created in Stripe; share
/order?coupon=CODE. Packages get a Free trial in days for online orders. - Knowledge base — Help for customers with categories, search, "Did this help?", a version per language, and ten starter articles that fill in the server's own settings; admins write and edit under Help articles. New support tickets suggest matching articles as the subject is typed.
- Referral programme — every customer gets a link under Refer a friend; when someone orders through it and pays for the first time, the customer who shared it gets a credit (Stripe customer balance) and an email; optionally the friend gets a discount code.
- Resetting a customer's password emails them a link to choose their own (valid 24 hours), alongside the new password shown to the admin; resetting a reseller's password does the same. The Getting started step "Change your password" now only ticks off for a password the customer chose themselves.
- Dates in emails follow the email's language ("17. Okt. 2026", "17 أكتوبر 2026"). Webmail password-reset emails use the language picked on the page, else the hosting account's language.
- The Settings hint for the brand colour now reads "The HostFog colour is #d87858".
- Emails in each customer's language — emails to customers and resellers (welcome, password reset, notifications, support summaries, SSL, space, malware, uptime, invoices, payment problems, moves, ticket replies) now go out in their own panel language, or the server's default language: English, Spanish, Portuguese, French, German, Turkish, Arabic (right to left), Hindi, Italian, Dutch, Polish, Russian, Ukrainian, Chinese, Japanese, Korean, Indonesian, Vietnamese, Bengali, Urdu (right to left) and Persian (right to left). The panel's bell notifications follow too. Translations live in
panel/lang/mail/and are easy to extend; anything not translated stays in English. Emails to the server admin stay in English. - Welcome emails carry a one-time "Choose your password" link instead of the password (valid 7 days; it also sets the SFTP password). Settings → "In the welcome email" switches back to including the password.
- Getting started for resellers too (password, two-factor login, first hosting account, Log in as). For customers, "Put your website online" now also ticks off for sites uploaded by SFTP, FTP or Git — the website folders are checked for anything besides the "coming soon" page.
- Welcome emails with everything in one place — a new hosting account's welcome email now holds all a customer needs, in sections: control panel address and sign-in, their website and exactly how to point the domain (nameservers or A records, free SSL), webmail plus the IMAP/SMTP settings for phones and mail apps, SFTP/FTP details, databases and DB Studio, what their plan includes, numbered first steps and where to get help. New resellers get their own welcome email (Reseller Panel sign-in, what they can do, the address and nameservers for their customers, first steps) — on by default when an admin creates a reseller. Section headings, details tables and numbered steps are designed in the HTML version, and readable in the plain text.
- Getting started checklist on a new customer's overview: change password, switch on two-factor login, point the domain (free SSL), put the website online and create the first email address — each step links to its page and ticks itself off when done. It disappears once everything is done, or with ✕. In all 21 languages.
- Branded HTML emails — every email the panel sends now has a designed HTML version in your brand colour (header with your name, a button for the main link, a large box for one-time codes, lists, small print), alongside the plain text for mail apps that prefer it. Works in Gmail, Outlook, Apple Mail and on phones, with no images to load.
- Webmail: forgotten password — "Forgotten your password?" on the webmail sign-in: a 6-digit code and a link go to the mailbox's recovery email (customers set it under Email accounts); without one, the account owner is told so they can reset it. Same protections as the panel's own reset.
- Error pages, .htaccess editor and per-site PHP settings no longer show a server error when the current file can't be read: the page opens with a clear note and a new file can still be saved.
- Customers hear about every change to their hosting — an email (plus the bell in their panel and the phone app) when their plan changes, the account is suspended or active again, support changes something through "Log in as" (one summary email after support has finished), a domain is added for them, free SSL is installed or keeps failing, space is nearly full, malware is found, someone signs in from a new place, or their password, contact email, two-factor login or API tokens change. Customers choose the kinds they get under Password & security → Email notifications; must-know notices always go out. Repeats are limited (e.g. the space warning at most weekly).
- Announcements — admins (all customers or one plan) and resellers (their own customers) write one message: emailed in the background and shown as a banner on each customer's overview for 30 days. "Service notices" always go out; "News" can be switched off by customers.
- Forgotten password with a one-time code — "Forgotten your password?" on every sign-in page: enter a username or email and get one email with a 6-digit code (15 minutes, 5 tries) and a reset link (60 minutes, works once). The page never reveals whether a login exists; requests are rate-limited per address and per login; only hashes of codes and links are stored. A new password (from a reset, the profile page, an admin or
hf-server) signs that login out on every other device, sends a confirmation email, and keeps two-factor login in place. Hosting accounts get the same new password for SFTP. - Three separate panels — Server Admin at
server.<domain>, Control Panel atpanel.<domain>and Reseller Panel atreseller.<domain>, each on the normal https port with its own sign-in page, look, app icon and access rules: only the people a panel is for can sign in there. "Log in as" opens the customer's own panel and "Return to admin" comes back; emails link each person to their own panel. On by default for new installs (--no-portalsto opt out); existing servers:hf-server portals on. The all-in-one address on the panel port (https://api.<domain>:8443) keeps working for everyone. New sign-in pages in all 21 languages. - Tunnels from the HF Server desktop app — Share in the HF Server desktop app makes the link on the server and connects by itself, no command to paste. New API:
GET /api/v1/tunnels,POST /api/v1/tunnels,GET /api/v1/tunnels/{name}/client(needs a "Read & change" token),DELETE /api/v1/tunnels/{name}. - HF Tunnel — your own tunnel service (admin → Tunnels → Switch on; customers → Tunnels). A site running on any computer gets a public
https://<name>.tunnel.<your domain>link through your server, with no router settings or port forwarding: the customer creates a tunnel, pastes one command (Windows PowerShell or macOS/Linux Terminal), and the site is live while it runs. The command downloads the frp client from your own panel; each tunnel's key opens only its own name (checked by a small key service — keys are never stored in plain text on the tunnel server); every tunnel gets its own Let's Encrypt certificate; a disconnected tunnel shows a friendly "this tunnel is offline" page. Admins see every tunnel and whether it is online, and set how many tunnels an account may have. Built on frp 0.61 (Apache-2.0), downloaded from its official releases and checked against the published checksums. Needs one DNS record (*.tunnel.<your domain>, now inhf-server dns-setup) and port 7000 open; Server health watches the tunnel services. - Page cache per website (PHP page → Page cache: off, or keep pages 5 min – 1 hour). Nginx serves ready-made pages to visitors who aren't logged in, so busy sites stay fast and PHP/MySQL barely work; on Nginx-only sites it caches PHP-FPM, on Nginx + Apache sites it caches Apache. Never cached: logged-in users (WordPress, WooCommerce, Joomla, PrestaShop, OpenCart cookies), carts, checkouts, admin and login pages, searches, previews, POSTs, and any page that sets a cookie or says no-cache. WordPress sites get a tiny must-use plugin, so the cache empties itself within a minute of any change (posts, comments, menus, themes, plugins, updates); the WordPress Toolkit's "Clear caches" and the PHP page's Clear button empty it at once. Pages show an
X-HostFog-Cache: HIT/MISS/BYPASSheader. - Server health (admin → Server health): every check that matters on one page, with a score, grouped results and a one-click fix where it's safe — repair Apache, restart a stopped service, add swap, free up disk space, retry the mail queue, sync the clock, get the panel's certificate — run live in the terminal dialog. It also checks the panel domain's DNS, reverse DNS, spam blocklists, website certificates about to expire, security updates, a newer HostFog version and the nightly backups. Checked every hour: the Overview shows a notice, the menu shows the number of problems, and the admin gets an email when something new breaks. On the server:
hf-server health. - Installs, updates and restarts run in a live terminal dialog. Installing a PHP version, a PHP extension, Node.js, Docker, a software package or WordPress/an app, restarting a service (Apache, Nginx, PHP-FPM, MariaDB, mail…), installing server updates, updating the panel itself and getting/renewing SSL certificates open a dialog that shows the server's output as it happens. When it finishes it shows ✔ and closes by itself after 3 seconds (the page refreshes with the result); if it fails, the error stays on screen until you close it.
- Copyright line on every panel and webmail page, the sign-in pages and the installer's last screen: "© <year> <company>. All rights reserved." with the server's own company name (the install's answer, White label) — the year comes from the server's clock, so it never needs updating; the installer's last screen names the publisher.
- More Nginx, less Apache. Nginx now sends every website's static files (images, CSS, JavaScript, fonts, video) straight from disk, with 7-day browser caching and compression; only PHP and missing files go to Apache. Folders that
.htaccessprotects (Directory privacy,Require all denied, hotlink protection…) keep going through Apache, found by a scan when the site's config is written, right after a Directory privacy change, and every 5 minutes. New Web server switch per website (PHP page): Nginx + Apache (default,.htaccessworks) or Nginx only (Nginx runs PHP itself through the account's PHP-FPM pool — fastest; WordPress-style links built in; password-protected folders keep their password, other protected folders are closed;.htaccessand the web firewall don't apply). Existing websites switch to static-from-Nginx on update. hf-server helplists every server command. New shortcuts:hf-server apache-fix,hf-server mail-name,hf-server mail-autoconfig,hf-server ssl-now,hf-server php-versions,hf-server php-repo(no more typing the hf-agent path). Unknown commands show the list.- DNS setup page for every customer domain (Domains → DNS setup, and linked right after a domain is added): shows which company runs the domain's DNS now, the two ways to set it up (point it at our nameservers, or keep DNS there and add the records), and every record the domain needs with a live ✔/✘ check against public DNS. The records can be downloaded as a zone file to import at the other provider.
- DNS setup for the panel's own domain during install: right after you enter the panel address, the installer lists the records to add or update in your domain's DNS (
panel.,webmail.,mail.,smtp.,imap.,ftp.,ns1./ns2., SPF for the mail server, DMARC and TLS-RPT), the glue records and the reverse DNS, and waits for Enter so you can make the changes first. It shows the list again at the end and saves an importable zone file (/etc/hostfog/dns-setup.zone). Show it again any time withhf-server dns-setup(--checkalso looks the records up in public DNS). - PHP versions: the panel only offers versions this server can actually install. On Ubuntu 26.04 the extra PHP versions repository (ondrej/php) publishes nothing yet, so installing e.g. PHP 8.4 failed with "Unable to locate package php8.4-…". Now it explains why and lists what is available; the repository is added by itself once it supports the release; optional extensions a release doesn't package are skipped instead of failing the install; and apt errors name every missing package, not just the last three.
- ALIAS records in the DNS editor (also at
@, e.g. for a CDN): published as the target's current addresses and kept up to date every hour. - Mail apps set themselves up: Thunderbird, Outlook, Apple Mail helpers and phone apps find the server, ports and username from just the email address and password (
autoconfig/autodiscoverrecords plus an_autodiscover._tcpSRV record, answered by the server). - Existing domains are brought up to date by themselves after an update: missing
ftp,mail,autoconfigandautodiscoverrecords are added, and MX records still pointing at an older HostFog default move to the mail server name. This also happens within 5 minutes whenever the mail server name changes. Records you made yourself are never changed. - DNS zone editor laid out in clear columns: Type · Name · Priority · Content · TTL · Actions, TXT values shown in quotes, long DKIM keys fold open. New domains start with
@andftpA records,wwwCNAME, and with email: MX (priority 10) to the mail server, amailCNAME for mail apps, SPF, DKIM and DMARC. - Apache no longer fails to start with "Job for apache2.service failed because the control process exited with error code". Before every Apache start or reload, HostFog now resets
ports.confif it went back to "Listen 80" (Nginx owns port 80), recreates missing~/logsfolders, disables vhosts left over from accounts removed outside the panel, and turns off Ubuntu's default sites again. If Apache still can't start, the installer, updates and the Services page show Apache's own error (or which program holds port 8080) instead of systemd's generic message.
Hosting panel
- Billing-software provisioning module, 21 languages (incl. Arabic, Urdu and Persian right-to-left), white label with your own logo
- Move accounts between servers in one click; copy from any host over FTP, MySQL and IMAP
- Wildcard SSL, DNSSEC and DNS clusters (secondary nameservers)
- Uptime monitor with email and phone alerts; Redis cache per account; CDN connection
- Email deliverability checks, blocklist checks and hourly sending limits; mailing lists
- Support tickets; Docker apps (n8n, Nextcloud, Uptime Kuma, Ghost, Vaultwarden, Gitea); Ruby apps
- Installable phone app with notifications; AI helper
- Licence plans Solo / Pro / Unlimited and bulk licences for resellers
- Uninstaller (
hf-server uninstall, keeps sites running or--purge), and Uninstall for apps from the app installer - Import accounts from full account backups: websites, databases, email with the old passwords, DNS and cron jobs
- Off-server backup copies to S3-compatible storage, optionally encrypted
- HF Panel updates from the panel: see what's new, update in one click or automatically every night
- Paid edition with licences: sell HostFog to other hosts per server per month, with a free trial
- Ubuntu 26.04 LTS support, alongside 24.04 LTS (interim releases such as 26.10 are not supported: 9-month life, no Pure-FTPd). On 26.04: PHP 8.5 (Ubuntu's own; OPcache built in), Dovecot 2.4 with its new configuration (same mail layout as 2.3, so accounts move between servers as is), MariaDB 11.8, OpenSSH 10 (fail2ban follows sshd-session), sudo-rs, Rust coreutils, Postfix without chroot (spam filter socket path), spamass-milter in the postfix group
- New accounts use the server's PHP version (8.3 on 24.04, 8.5 on 26.04) instead of a fixed 8.3
- Automatic SSL. The panel's own certificate (panel., webmail., mail., ftp.) is installed by itself within 5 minutes of their DNS pointing at the server — no re-running the installer — and nginx, Postfix, Dovecot and Pure-FTPd switch to it
- AutoSSL for customers: every domain and subdomain gets a free Let's Encrypt certificate (with https:// redirect) within minutes of its DNS pointing here; checks public DNS first and retries slowly (1h … 24h) so Let's Encrypt limits are never hit; the SSL page shows why a domain is still waiting; customers get a notification; removing SSL from a domain keeps AutoSSL off for it; switch for the whole server under Settings. On update, domains whose SSL a customer had removed on purpose stay without it
- Users online (Accounts → Users online, and an "Online now" tile on the overview): who is signed in to the panel right now — the page they are on, their IP and when they were last active — plus who was on earlier today. Refreshes itself every 30 seconds. Resellers see only their own customers; staff using "Log in as" show as themselves; signing out takes someone off the list at once. In all 21 languages
Security
-
Customers' ~/logs folder is now root-owned: before, a customer could replace a log with a link to a system file and have nginx/Apache/logrotate (running as root) write into it. Existing servers are fixed on update
-
MySQL: "_" in database names acted as a wildcard in grants, letting one account reach another's database; database imports (upload, copy from another host, account import, staging) no longer run with root rights, so a crafted dump can't run commands or read server files
-
Restores, website deletes, Git, SSH keys, app installs, Python virtualenvs and Docker app folders no longer follow links a customer planted (each could reach other accounts or system files as root)
-
Node/Python/Ruby app logs are written by the app itself, not by root; FTP passwords for "copy from another host" no longer show in the process list
-
Uptime monitor and "copy from another host" refuse private/internal addresses (could be used to probe the server's internal services)
-
Webmail: closed a way past the HTML mail cleaner; login form CSRF protection no longer accepts an empty token; sign-out needs the signed form (a link on another site can't sign people out)
-
File manager: deleting or renaming a link removes the link, not the folder it points to
-
Root agent: every argument validated; customer-controlled links and files are never followed or written as root
-
Package installs (PHP versions, extensions, Docker, system updates) wait for apt, repair an interrupted install, never prompt, and report the package that failed
-
Installer: opens the real SSH port before enabling the firewall; fail2ban protects SSH, FTP, Postfix and Dovecot logins
-
Complete removal —
hf-server deleteremoves HF Panel and all data: every account, database files, mail, certificates, logs, the server software, HF Panel's tools, package sources, the HostFog swap file and earlier uninstall copies; the firewall is reset to SSH only. Nothing is saved; it asks forDELETE EVERYTHING.