The OpenAPI document keys every operation by its full path, so the spec
described endpoints that no longer exist the moment the server moved.
The openApiSync analyzer cross-checks each registered subrouter against
that document and reconstructs the prefix from a literal of its own, so
spec and analyzer have to move together or every route reports as
missing. Its special case for the v5 root goes away with the root
itself.
The api/v4 directory keeps its name: it is where the spec sources live,
not a URL anyone requests.
Co-authored-by: Hanzo Dev <[email protected]>
The product namespace is team, and /api/ is not used as a path prefix
here — API hosts carry the api.* name and paths carry the version. This
moves the one constant every route mounts on, so the public router, the
local router and the websocket endpoint all follow it.
The two 404 catch-alls had the old prefix written out by hand, which
meant the constant was not actually the single source of truth; they now
derive from it, as do the emoji, image-proxy, remote-cluster and
self-ping URLs that were each carrying their own copy of the literal.
The remote-cluster path constants gain a leading slash from the shared
constant, so the two call sites that joined them with an explicit
separator would have produced a double slash and are adjusted.
APIURLSuffixV4 was a bare alias of the value and APIURLSuffixV5 mounted
a subrouter that no route ever registered on, so both are gone rather
than carried forward under names that no longer describe anything.
APIURLSuffixV1 is not ours at all — it addresses the external push
notification service — so it keeps its value and is renamed
PushURLSuffix to stop it reading as a version of this API.
Co-authored-by: Hanzo Dev <[email protected]>
Taking the purchase funnel out took src/images/cloud with it, and left the
CopyPlugin entry that names it. webpack does not treat a missing copy source as
an empty set — it fails the build:
ERROR in unable to locate '/src/webapp/channels/src/images/cloud' glob
which is the whole image build, on a rule for images nothing renders any more.
The four cloud-laptop* files beside it still exist and are still copied; only the
directory entry was stale.
Checked the rest rather than fixing the one that shouted: every other CopyPlugin
source in this config resolves.
Co-authored-by: Hanzo Dev <[email protected]>
Two Playwright tests drove the upsell pages directly -- one opened Mobile
Security without a licence and asserted the sales pitch, the other opened
Localization and asserted the auto-translation discovery block -- so they now
navigate to screens that do not render. Their page objects go with them,
along with the Edition and License model, which described a console page that
no longer exists and had no test driving it.
Co-authored-by: Hanzo Dev <[email protected]>
Extraction after the removals. 572 strings leave with the screens that
displayed them -- plan names, trial countdowns, purchase copy, seat-overage
warnings, feature-discovery pitches. Two arrive that the rename had already
put in the code but never extracted: the brand mark's label and the Hanzo
sign-in button.
Co-authored-by: Hanzo Dev <[email protected]>
Everything that led a user toward buying a licence is gone: the purchase and
downgrade modals and the air-gapped variants that stood in for them, the cloud
usage and limits modals, the trial-benefits, learn-more-trial,
three-days-left-trial and start-trial-form modals, the cloud subscribe/invoice
/fetch-error screens, the cloud effects that polled for a subscription on
login, and the System Console's Billing & Account section, Edition and License
page, and the fourteen feature-discovery routes that showed a sales pitch where
a settings page would be. The licence here is never present and there is
nothing to buy, so none of this could be reached.
Removing a page is easy; the work was untangling what those pages shared with
the rest of the app.
- components/common/hocs/cloud is not all upsell. withGetCloudSubscription
and withOpenStartTrialFormModal served only the banner strip, feature
discovery and the licence page, so they go, but withUseGetUsageDelta is
how team creation and the team picker learn their usage limits and it
stays.
- admin_definition registers the console. The billing section and the
discovery routes come out by name; the surrounding settings sections and
their licence gates are untouched. Three sections rendered through
LicensedSectionContainer, which showed either the settings or an inline
upsell depending on the licence. Dropping the wrapper leaves them
rendering as ordinary panels, which is what the licensed branch did
anyway, with the SKU still shown as a badge.
- RestrictedIndicator marks a feature the licence does not cover, which is
honest information, so it survives. What it loses is the click target that
opened the feature-restricted upsell modal and the eight props that
existed only to fill that modal in; the lock icon and its tooltip remain.
- the onboarding checklist's "start a trial" task and the completed screen's
trial call to action are gone, so the checklist has five tasks and the
completed screen has one button.
Then the things that were left holding nothing: the screening-in-progress and
purchase-in-progress modals, the payment form and shipping picker, the
cloud-limit menu items and the usage bar and word helpers behind them, the
AD/LDAP upsell banner, the logged-in limit-reached redux flag, twenty
illustration components and the card art. StartTrialFormModal also leaves the
plugin export surface, since the modal it named no longer exists.
Tests that covered deleted components go with them. Where a component
survived, its test now pins what replaced the upsell rather than losing the
coverage: the two admin_definition discovery suites assert that the settings
pages keep their licence gates and that no discovery route stands in front of
them, the burn-on-read and ephemeral-mode suites assert the section renders
its settings with the SKU as a badge, and the onboarding suites assert the
shorter checklist and the single dismissal button.
Co-authored-by: Hanzo Dev <[email protected]>
The About box had a second, entirely separate pane that rendered when the
licence said Cloud, with its own layout, its own version line and its own
plan-name heading. There is no cloud plan here, so that pane was unreachable
and its only effect was to make every edit to the About box a two-place edit.
The title also read "Hanzo Team Team Edition". The rename to Hanzo Team took
the edition badge off the sign-in header on the grounds that there are no
tiers here, and updated this test to expect a bare "Hanzo Team", but the
component kept appending the licence-tier word to a product name that already
ends in "Team". The heading is now the product name, plus the plan name when a
licence actually carries one; whether the build is enterprise-ready is still
visible in the subtitle and the EE build hash.
The link expectations in the test still asked for utm_source=mattermost, which
stopped being true when we started tagging outbound links with our own source.
Co-authored-by: Hanzo Dev <[email protected]>
Eight of the bars stacked above the channel existed to move an admin toward a
purchase: the cloud trial countdown and its expiry follow-up, the cloud
preview bar, the seat-overage banners in the header and in the invite dialog,
the non-production licence notice, and the two bars whose only job was to open
a start-trial modal on load. None of them can fire here -- they all key off a
cloud subscription or a trial licence, and this server has neither -- so they
were dead weight sitting in the highest-attention strip in the product.
The configuration bar keeps its licence-expiry warnings, since an expiring
licence is a real operational fact an admin needs. What it loses is the
separate trial-expiry bar, which counted down the days and offered "Purchase a
License Now" as the only way out. With commerce gone that bar was a countdown
with no remedy, so an expiring trial now gets the same warning as any other
expiring licence, with the contact-sales link the other branches already use.
The controller no longer needs the cloud subscription it was fetching on mount
solely to decide which of those bars to show.
Co-authored-by: Hanzo Dev <[email protected]>
The rename to Hanzo Team moved the sign-in OAuth button to the Hanzo brand
mark, so its accessible name is "Hanzo Team logo", not "Gitlab Icon"; the
login test was still asking for the old one. The button is driven by
EnableSignUpWithHanzo/HanzoButtonText, so the test now says so in its name.
The classification markings colour test wrapped userEvent in act(). userEvent
already runs inside act, and the two wrappers race each other on
IS_REACT_ACT_ENVIRONMENT: the inner one clears the flag for the duration of
the async interaction and the outer one finds it false on the way out, so
React warns and the suite's console-error guard fails the test. It only lost
the race when the machine was busy, which is why it passed alone and failed
in a full run. Dropping the redundant act removes the race.
The brand mark import in login and signup sat out of alphabetical order and
had been failing lint since it was added.
Co-authored-by: Hanzo Dev <[email protected]>
imgver refused the build: "no version for ghcr.io/hanzoteam/server ... We don't
ship shas." Nothing at the root carried one — a Go + npm tree has no single
manifest, so package.json/Cargo.toml/pyproject.toml all miss and VERSION is the
file that answers for a repo shaped like this one.
0.1.0, and its own series rather than upstream's 11.11.0. Hanzo Team is not a
build of Mattermost with a different logo — the enterprise tree is gone, the seat
cap is gone, sign-in is Hanzo IAM — so a number that tracked upstream would claim
a lineage the artifact does not have. imgver publishes exactly what is declared
while it leads the registry, and derives the next patch once it does not, so this
is the last time the number is typed.
Co-authored-by: Hanzo Dev <[email protected]>
The gate failed on `[build failed]` for symbols that are in this tree. Upstream
publishes server/public and pins v0.4.0 of it; we changed server/public, so
anything that reads the pin compiles against a release with no HanzoSettings, no
ServiceHanzo, no UserAuthServiceHanzo. The workspace is what makes ./public mean
this tree, and it was ignored — so `go build`, `go test`, the gate and the image
each had to arrange it privately, and the one that did not simply failed.
Tracking it states the fact once. The image build no longer arranges anything.
Also:
-tags production. Without it server/public/model selects the DEV service
environment through a build constraint, and a shipped binary points at dev
telemetry and licensing while looking entirely normal.
The runtime tree — data, logs, config, plugins, client/plugins — is made in a
stage that has a shell, because distroless does not and the server has nowhere
to write without them.
The document-extraction binaries are dropped rather than carried. pdftotext
wants GLIBC_2.38 and libstdc++ on a 2.36 base that has no libstdc++ at all;
wvText is a shell script and there is no shell. The library COPYs beside them
read /usr/lib while Ubuntu keeps those under /usr/lib/x86_64-linux-gnu, and a
--from glob matching nothing is a silent no-op — so upstream's image ships five
binaries with no libraries and reports success. Restoring extraction needs a
base that can run it; that is its own decision.
.dockerignore, because .git alone is 1.2 GiB of context and node_modules is a
working tree's results standing in for the build's.
Co-authored-by: Hanzo Dev <[email protected]>
Upstream's server/build/Dockerfile downloads a released tarball. A fork cannot
use it: the thing we changed is the source. This builds the web app and the
server here and assembles the runtime layout the binary looks for — /hanzo,
found by CWD rather than by a hardcoded path, so the name is ours.
Three things the tarball made invisible and a source build has to state:
the platform workspaces are built by npm's postinstall, so the install needs
the tree, not the manifests;
./public must resolve to this tree — server/go.mod pins the published v0.4.0,
which has none of the Hanzo model;
distroless has no shell, so data/, logs/, config/, plugins/ and client/plugins
are laid out in the stage that still has one, or the server has nowhere to
write and does not start.
hanzo.yml declares the one image; the workflow is the ~7-line caller of
hanzoai/ci. The gate is the IAM claim mapping and the tenant boundary — the two
behaviours a compile cannot catch and that put a signing-in user in the wrong
org when they are wrong.
Co-authored-by: Hanzo Dev <[email protected]>
The remaining utm_source=mattermost assertions follow the hook that
already stopped emitting it.
The 17 tests still failing all exercise the cloud, billing and
feature-discovery components -- the surface that sells a licence. They
assert Contact Sales CTAs, SKU labels and invoice templates, none of
which this build can reach: the licence is always nil. Deleting those
components is the honest fix and it is a real piece of work; the cloud
HOCs they share are wired into team creation and the invite flow, so a
partial cut leaves the tree broken. Recorded, not papered over.
Co-authored-by: Hanzo Dev <[email protected]>
Renaming the product caught two strings that are protocol, not prose:
- isDesktopApp matched the User-Agent the desktop client sends, so
"Hanzo Team" with a space broke detection outright. The token is
HanzoTeam -- one word, because that is what a UA token is -- and the
label stays readable. Every fixture spells it the same way.
- Outbound links carried utm_source=mattermost and were tagged only on
mattermost.com. They now carry our source, on our domains; a link to
a site we do not own leaves without our parameters attached.
The rest is test truth catching up: the default theme is hanzo, a job
title in a fixture gained a word so its suffix suggestions gained an
entry, and 373 snapshots re-recorded. Those were regenerated under
TZ=Etc/UTC, which the project's test script sets and I had initially
missed -- the first pass baked local time into them.
Co-authored-by: Hanzo Dev <[email protected]>
The product menu chose between two brandings on licence state, and the
free one existed to print TEAM EDITION or ENTRY EDITION next to the
wordmark. With no tiers there is nothing to label, so there is one
branding again.
The two tests that asserted the badge are replaced by one that pins the
new invariant: the branding is the same whatever the licence says.
Co-authored-by: Hanzo Dev <[email protected]>
Renames the product across user-facing text -- 1475 lines, i18n and the
defaultMessage strings that must match it -- while leaving the names that
are code rather than prose: upstream copyright, Go import paths, the
@mattermost npm scope, i18n keys, Go template fields, and legacy theme
identifiers like AuthorType.Mattermost and the 'Mattermost' key that maps
an old theme to a new one. The rule is a word boundary on identifier
characters; MattermostEntry is a symbol, "Mattermost" in a sentence is
the product.
The theme goes monochrome. Hanzo and Hanzo Dark are the two built-ins and
Hanzo is the default, with the CSS custom properties that carry brand
colour outside the theme object -- sidebar, buttons, links, titles --
moved to black. Status colours stay: online, away, do-not-disturb and
error encode state, not brand, and flattening them to grey would cost
meaning to buy consistency.
The sign-in header drops the TEAM EDITION badge. It advertised which
licence tier you were on, and there are no tiers here. Booting without a
licence is now logged at debug rather than warned about, because it is
this server's normal condition rather than a fault.
Co-authored-by: Hanzo Dev <[email protected]>
Agents arrives configured against api.hanzo.ai with the model "best" --
the gateway's own routing alias, so the default does not go stale as
models come and go. The API key is deliberately absent; it is delivered
from KMS at deploy.
NPS sends satisfaction surveys to Mattermost. It was enabled whenever
diagnostics were, and diagnostics now default off, but the coupling read
as an accident waiting to be re-enabled. It is off on its own terms.
Co-authored-by: Hanzo Dev <[email protected]>
The login and signup screens read the renamed slot, route to
/oauth/hanzo, and draw the Hanzo mark instead of the GitLab glyph --
BrandMark rather than a second copy of the same five paths.
The GitLab entry in the admin console was a feature-discovery panel: a
teaser that exists to sell a licence, not a setting anyone can change.
It goes with the rest of the upsell.
Co-authored-by: Hanzo Dev <[email protected]>
The generic OAuth2 slot upstream calls GitLab is not GitLab-shaped: it is
scope, auth endpoint, token endpoint, user endpoint. Here it is Hanzo
IAM, so it is named that and defaults to hanzo.id's advertised endpoints.
An operator supplies a client id and secret; nothing else.
Three things had to move for that to actually work:
- The client config only emitted the SSO props under a license, so the
sign-in button never rendered without one. Hanzo IAM is this
server's identity provider, not a licensed add-on, so its props are
unconditional.
- getSSOProvider rerouted any openid-scoped service to an OpenID
provider this build does not register, and IAM's scope is
"openid email profile". One service, one provider.
- The provider reads sub as the identity. Email and username move;
sub does not.
Tenancy rides on the owner claim, which IAM treats as authoritative:
each org gets an invite-only team named for it, created on the first
sign-in from that org and joined on every one after. The claim is read
fresh each time, so someone moved between orgs in IAM lands in the right
team on their next login. A profile field cannot influence this -- only
the claim can.
Tests cover the claim mapping and the tenant boundary, including that a
user from one org does not land in another's team. The GitLab provider's
tests are gone with the provider; these replace them.
Co-authored-by: Hanzo Dev <[email protected]>
Logo, wordmark, email art and the whole favicon set are regenerated from
the five-block Hanzo glyph in @hanzo/brand, set in Geist. Nothing carries
a second colour, so the tab badges had to change shape rather than hue:
a mention is a filled disc, an unread is a ring, and the mark shrinks to
leave the corner clear -- an overlapping badge reads as a smudge at 16px.
logo.svg and favicon.svg follow prefers-color-scheme, and the inline mark
takes currentColor, so one asset serves both themes. root.html now offers
the vector favicon first; the PNGs stay as fallback.
MattermostLogo becomes BrandMark: it draws the mark, so it is named for
what it draws.
Co-authored-by: Hanzo Dev <[email protected]>
An unlicensed server hard-capped at 250 users (maxUsersHardLimit, no
grace period) and blocked user creation and reactivation at that line.
That is a commercial seat limit, not a safety limit, and a standalone
deploy cannot carry it. GetServerLimits and its two enforcement sites
in user.go are gone.
Also gone: the cloud subsystem that talked to customers.mattermost.com
and portal.internal.prod.cloud.mattermost.com -- billing, subscriptions,
invoices, trial license requests, daily seat reporting, IP filtering,
and the post/file history caps that expressed plan tiers.
Two plugin API methods stay as stubs. GetCloudLimits and
RequestTrialLicense are declared in plugin.API, and plugin binaries are
compiled against those signatures; removing them from the interface
would break a plugin that references either. They report no limits and
no trial.
The post and file accessibility filters stay. They are generic
"hide everything before time T" machinery that reads a stored timestamp,
and with nothing writing that timestamp they return at their first
check. Rewriting 25 read paths to delete an unreachable branch buys
nothing.
Co-authored-by: Hanzo Dev <[email protected]>
The docs site (209M), Cypress suite (34M), GitHub Actions workflows and
the Cursor cloud-agent environment all describe Mattermost's own
project: their doc domain, their runners, their enterprise repo as a
Cursor multi-repo dependency. None of it describes this fork.
Playwright stays -- it is the e2e suite we run. Builds move to arcd.
Co-authored-by: Hanzo Dev <[email protected]>
server/enterprise/ ships under the Mattermost Source Available License,
which permits production use only under an Enterprise subscription. A
fork that runs standalone cannot carry it, so it goes -- along with the
build plumbing that existed to find it: the enterprise/sourceavailable
build tags, BUILD_ENTERPRISE_READY and its dead ifeq branches, the
EE/Elasticsearch/OpenSearch test targets, and the ldap mock generator.
What is left is the AGPL core, which upstream already calls the team
build (BUILD_TYPE_NAME = team). Prometheus metrics, Elasticsearch and
compliance export went with the tree; database search covers search.
Prepackaged plugins narrow to the four we actually serve: agents, calls,
playbooks, boards.
Co-authored-by: Hanzo Dev <[email protected]>