Repository navigation
Conversation
components/icons/index.tsx is now the only place icons come from. It wraps the Phosphor SSR build, so the same import works in server and client components, and makes every icon decorative by default (aria-hidden, not focusable) unless it gets an aria-label, as lucide did before. A test covers both cases. The 70 importing files only changed their import line; where the Phosphor name differs the old local name stays as an alias, so no component body changed. The image category icons keep their keys, the database values stay valid. Dependencies: lucide-react removed, @phosphor-icons/react 2.1.10, @fontsource-variable/archivo 5.3.0 and @fontsource-variable/source-sans-3 5.3.1 added (the fonts are wired up in the next commit), postcss 8.5.32. TypeScript stays on 6 (typescript-eslint allows <6.1.0), @types/node on 24 to match the server runtime.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.OpenSSF Scorecard
Scanned Files
|
Code coverage overviewLanguages: TypeScript TypeScript / code-coverage/vitest
View coverage of the 10 most impacted files.
ℹ️ Calculations represent line coverage. Results may change as calculations are updated. Updated |
Tokens: neutral anthracite ground and surfaces instead of the GitHub dark axis, MSK green kept as the brand colour, plus new tokens for the soft green surface, the primary hover, the anthracite band and the green band. Form controls get their own border token at 3:1 (WCAG 1.4.11). The log console moves off #0d1117. The contrast test checks the twelve new pairs in both themes; a counter-check with the old input border fails as it should. Fonts: Archivo (weight and width axis) for headings, Source Sans 3 for text, the system monospace for code. Inter and JetBrains Mono are removed. Fluid layout: a fluid type scale (text-display, text-h2, text-h3, text-product, text-lead, text-body, text-small, text-label) whose maximum is at most 2.5 times its minimum so zoom still reaches 200 %. The page shell, its gutter and the card grid minimum grow with clamp() instead of in steps. The theme follows the system setting now instead of defaulting to dark.
Button: minimum heights instead of fixed ones (44 px for md and lg, 36 px for sm), so long German labels wrap instead of being cut; hover uses the new primary hover colour; focus uses the global 3 px outline; the outline variant gets the 3:1 control border. Input and the new Select share one field look: 3:1 border, card surface, 16 px text so iOS does not zoom, red border via aria-invalid. Card: 10 px radius, a clickable card answers with a green border and a shadow instead of jumping. Badge: new soft green "tag" variant. Dialog: new modal on the native <dialog> element, which brings focus trapping, Escape and an inert page without injecting runtime styles the nonce CSP would block. A stylesheet rule stops the page behind it from scrolling. @radix-ui/react-dialog was unused and is removed. Radii follow the sample page: 6 px for controls, 10 px for cards.
The redesign moves every page under /en and /de and translates the German paths (/de/rechtliches/agb instead of /de/terms). Links, the language switcher, hreflang, the sitemap and the proxy all need the same answer to "what is this page called in that language", so it lives in one table in lib/routes.ts instead of being spread over string literals. The module is pure on purpose: resolveRequest() holds the whole routing policy (307 from / by cookie, Accept-Language, then English; 308 from any other known form, including the old addresses, to the one canonical address per language; exempt paths untouched), so vitest can check it without Next. The legacy redirects from next.config.js are part of the table now.
The redesign gives every page exactly one address per language, /en/... and /de/... with translated German paths, instead of English at the root and a /de prefix that only the proxy knew about. This commit moves all pages under app/[lang]/ with English folder names (the /en paths) and wires everything that builds an address to the route table from the previous commit. - proxy.ts asks resolveRequest() what to do: 307 from / (msk_lang cookie, then Accept-Language, then English), rewrite of the German form onto the folder, 308 from every other known form, old addresses included. The nonce CSP, the security headers and the body limits are unchanged; the page rate limits now match the internal path, so /en/tools/images and /de/web-tools/images share one bucket (before, /de/images was not limited at all). - The proxy remembers the language of the last page in msk_lang (HttpOnly, Secure, Lax, one year), so / can send a returning visitor back to it and OAuth callbacks, Stripe and Tebex return URLs land in the right language. - LocaleLink, LangProvider, the language switcher, canonical, hreflang (x-default now points at /, which picks the language), the sitemaps, robots.txt, llms.txt and JSON-LD all read the table. - API routes, OAuth callbacks, /auth/discord, botproxy, transcripts and the root files stay without prefix. /auth/discord gets its own root layout, because it is now the only page tree outside app/[lang]. - A catch-all under [lang] keeps the 404 inside the shop layout. - Mails link the new addresses in their language; old links in mails that were already sent keep working through the 308s. - The old next.config.js redirects moved into the legacy table, so they reach the new address in one hop.
… texts Under /en the legal texts are translations of the German originals, and the pages now say so at the top, each in the wording of the legal draft of 27.10.2026 (section 1.3): terms with the German version prevailing plus the consumer sentence, the DPA without it (it is only concluded with businesses), privacy policy and withdrawal instructions only "also available in German", because a precedence clause would not hold there (Art. 12 GDPR, § 356 (3) BGB), the imprint nothing. Each notice links the German page; the German pages get a small link to the translation. The wording lives in lib/legalNotice.ts with a test, so a precedence clause cannot slip onto the wrong page. The old "This is a translation for your convenience ... is authoritative" lines in terms, DPA, privacy and withdrawal are gone: they repeated the notice and, for privacy and withdrawal, contradicted it. The absolute addresses in the texts point at the new paths (/de/rechtliches/avv, /en/legal/dpa, ...). The privacy policy lists the new msk_lang cookie and no longer claims that no cookie is set for the language.
The pages moved folders, so the AST graph pointed at files that no longer exist. Regenerated with graphify update (no LLM).
The proxy wrote msk_lang on every page view under /en or /de. The language is in the address, so that cookie was not needed, and the privacy policy used to state that no cookie is set for the language. A cookie written without the visitor's action is also hard to justify under § 25 TDDDG. Now only the language switcher writes msk_lang, on click; the proxy still reads it to choose where / leads. The unused `remember` flag is gone. The privacy policy in both languages says when the cookie appears. A test guards that the proxy never writes the cookie; re-adding the write turns it red.
Decided on 27.10.2026: categories, compare, stats, images and upload get German forms under /de (kategorien, vergleich, statistik, bilder, hochladen). Dashboards, admin and the giveaway result link keep their English sub-paths, the value of a dynamic segment never changes. The English forms under /de still answer 308 to the German ones, the tests expect the new words.
An unknown address under /en or /de answered with an empty error shell, <html id="__next_error__"> without a lang attribute and without content. Only the browser rendered the 404 page afterwards. That breaks WCAG 3.1.1 and leaves a crawler nothing to read. The cause is Next itself: a thrown notFound() always ends in that shell, whether the root layout is static or not (checked with a minimal app on 16.4.3). Only a page that renders normally gets server-side HTML. So the proxy now decides. resolveRequest marks an address the route table does not know as unknown, and the proxy rewrites it with the status 404 and a request header the client cannot set itself. The catch-all renders the 404 page directly when that header is there and still throws notFound() when it is not, so the status stays 404 in every case. For a response that already carries the status 404 Next resolves the metadata of the layouts only, so the layout names the 404 page and sets noindex. Before, the page carried the home page title and "index, follow" next to Next's own "noindex". The 404 page itself is bilingual now. A notFound() thrown by a page (an unknown package id) still gets the error shell; Next offers no way around it short of the experimental global-not-found.
Phase 3 of the redesign, plan sections 5.3 and 5.10 and the sample page of Phase 0. Header: the 4 px MSK green stripe on top, the brand, then the three product lines (FiveM scripts, Discord bots, web tools) and the about page, visible instead of behind a "Bots" and a "Resources" menu. Each product line is a link plus a small button that opens a panel with its pages and one line of description, the disclosure pattern: Escape closes and returns the focus, a press outside closes, hovering opens it with a mouse. The active line gets a green underline. Pages that only arrive in Phase 5 (/discord, /tools, /about, /fivem/free, /fivem/all-access) are linked already but not prefetched, so they do not show up as 404s in the console. Right side: language as a short code with a small panel, the cart with a green count, and one account entry for both logins. The panel keeps the shop login (CFX.re, through Tebex) and the bot dashboards (their own Discord login) apart and holds the theme switch, as plan 5.3 asks: system, light, dark as three toggle buttons instead of a menu. Below 1280 px a menu button opens the product lines with their pages, account, language and appearance. The sign-in return (?success, ?discordLinked) moved into a component of its own, so the header no longer sits behind a Suspense boundary. No search in the header (E5): the catalogue is far below the 20 products where it pays off. SearchDialog, the old ThemeToggle, the Navbar re-export and the unused footer stylesheet are gone. Footer on the anthracite band: products, help (documentation, Discord support, statistics), legal and contracts in four columns, the imprint one click away, "Vertrag widerrufen" and "Verträge hier kündigen" unchanged. The trust line keeps its legal wording; payment marks only show on shop pages. Above the footer every public page gets the green closing band with the Discord invite and the documentation as the second way. A test holds the legal links to their routes in both languages. Green kickers replace the mono eyebrows with a dash (exclusion list, 4.2); .eyebrow stays as an alias for the pages of later phases. The browser bar colour follows the new background tokens instead of the GitHub dark.
Plan section 5.4 and the sample page of Phase 0, English as the lead version, checked with the longest German texts. Hero on the anthracite band: one sentence on what MSK is, FiveM and Discord in green, and the entry by need with a green line above it, one row per product line with its price: the cheapest paid script from Tebex, the cheapest ticket bot plan from lib/tiers.ts, free for the web tools. From 1280 px heading and entry list stand side by side. Then, each section with a kicker and from 1600 px with its head beside the content: at most three measured figures (the refund rate moves to the about page, E6), the paid scripts with the subscription, both Discord bots in a zigzag with price and the way to overview, dashboard and statistics, the three web tools as a compact row, the free scripts as a list instead of big cards, and three sentences on who is behind the shop, without a photo. The green band above the footer closes the page, so the second closing section is gone. Removed, as the exclusion list (4.2) asks: the release log in terminal look, "Why MSK" as a row of four icon cards, "How it works" and the old call to action. lib/releases.ts stays, the updates move to the product pages in Phase 4. Also gone: CustomPackageCard and CustomPackages, the unused Divider, InfoSection and DiscordButton, content/home-features.ts and their texts in lib/i18n.ts. The catalogue cards only get the new type and the green framework tags; the screenshot-first card is Phase 4. Prices on the home page are formatted per language through Intl (19,99 € and €19.99, plan 5.9). The free scripts carry a German description now. A test holds the "plans from" price to the cheapest paid tier, so a price change cannot leave a stale one behind.
Header, footer and home page got new components and lost old ones (SearchDialog, ThemeToggle, ReleaseFeed, WhyMSK and others), so the AST graph pointed at files that no longer exist. Regenerated with graphify update (no LLM).
CodeQL flagged the replace(/<[^>]+>/g, '') in the footer link test as an incomplete multi-character sanitisation. The test only compares the text and renders nothing, but splitting at the tags reads the same text without the pattern, so the alert goes away instead of being dismissed.
tailwind-merge only knows Tailwind's own font sizes and takes every other
text-* class for a colour. cn('text-[var(--color-primary-foreground)]',
'text-lead') therefore dropped the colour: the large primary button (size
lg, text-lead) lost its white label and showed the inherited dark text on
green. That is the cart button of the product page.
cn now extends the font-size group with the scale of globals.css (display
to label), so a scale size replaces another size and leaves the colour
alone. A test holds both halves.
…oice Tebex keeps every script twice, as an Encrypted and a Source package with two ids, two names and two pages. A buyer sees one script with a choice, so the shop now groups the packages into products (lib/products.ts) and gives each product one page at a speaking address: /en/fivem/garage, /de/fivem/garage, the subscription at /en/fivem/all-access and /de/fivem/alle-scripts. Nothing is maintained twice: the pairing comes from the two package ids content/resource-stats.ts already lists per paid resource, and from SUBSCRIPTION_PACKAGE_IDS. The slug is the resource name without msk_, as stable as a name gets. A package nothing configures still lands in exactly one product, paired by its Tebex slug stem or on its own, so a script added in Tebex keeps showing up without a deploy. Addresses are decided before a page renders. Old id addresses (/en/fivem/5732588, /packages/5732588, /package/...) answer 308 to the product in one hop, with ?variant=source when the id was the Source package; the canonical address never carries it. An unknown slug or id gets the 404 page of its language with status 404 from the proxy. A notFound() thrown by a page comes back from Next 16.4.3 as an empty error shell without lang, and global-not-found only covers unmatched routes, so the proxy marks the request and the product route renders the 404 content itself, as the catch-all does since Phase 3. For a slug the configured products do not know, the proxy asks Tebex, at most once a minute. The product page (plan 5.6): screenshots first in the gallery and the Tebex banner last, the buy box with Encrypted and Source as native radio buttons, the price of the choice with VAT, the cart button of the old page unchanged (same basket routes, Discord ID and gift dialogs, only the package id follows the choice), the seller line naming Tebex Limited as merchant of record, a comparison of the two releases, the requirements from the documentation with a link to it (E4, no FAQ), and the last three versions from the VERSIONS repository. On a phone the description moves below the cart button, so name, choice, price and button fit the first screen. Sitemap, hreflang, canonical, JSON-LD (an AggregateOffer with both variants) and /llms.txt name the product pages. The catalogue cards still link to the ids and reach the products through the redirect; they follow in the next commit.
The catalogue listed every Tebex package, so each script stood there twice with the same text and the same banner. It now shows one card per product (plan 5.5): the first screenshot after the Tebex banner, the name, one line of benefit, the frameworks as soft green tags and the price "from" with VAT. The banner is only used when Tebex has no other image. The card is one link through the name; there is no cart button, the choice between Encrypted and Source belongs on the product page. The filter moved from a column beside the grid into one row above it. A 232 px column left two cards per row at 1440 px (945 px for the grid, cards of 351 px); now the grid has the full width and shows three columns at 1280 and 1440, four tracks at 2560 and five at 3440, and no card gets narrower than --grid-card. The one facet left is the framework: with both variants in every product a variant facet filters nothing, a price facet over "from" prices next to nothing, and the "works with" names are the requirements on the product page now. Several ticked frameworks mean any of them. The subscription is a band below the grid, said positively: every script for a month, cancel monthly, a trial licence. The comparison of Encrypted and Source closes the page, linked from the sentence above the grid. The category pages and the home page use the same card. On a variant category the card names that variant's price and opens the product page with it chosen, so the Source category does not show the Encrypted price as "from". The per-package tables in lib/config.ts (badges, descriptions, tags) are gone; what the shop says about a product is in PRODUCT_COPY, keyed by its slug, in both languages.
The product model, the product page and the catalogue got new modules (lib/products.ts, lib/productView.ts, components/products/) and lost old ones (PackageCard, PackagesBrowser, the [id] route), so the AST graph pointed at files that no longer exist. Regenerated with graphify update, sequentially and without an LLM.
The garage runs on ESX, QBCore and Qbox, as its documentation says; config and copy only named ESX. The scripts need msk_core 4.1.0 or newer, as the 5.6.1 changelog states, so the requirements block names that version. Both confirmed by the owner on 28.10.2026.
The order confirmation is the contract confirmation under § 312f BGB, but it only knew monthly plans. A yearly subscriber was told "49,00 € pro Monat" and "verlängert sich monatlich", because formatSubscriptionPrice returns the amount and nothing passed on price.recurring.interval. The webhook now reads the interval off the subscription and both mails take it as a required field, so no caller can fall back to "per month". The confirmation prints price period and term in the words of § 6 (1) of the terms and of the order summary in the dashboard: "pro Jahr" and "zwölf Monate, verlängert sich jährlich" for a yearly plan. The trial reminder had the same flaw in its last sentence and gets the same fix. The English terms still said that subscriptions run monthly; § 6 (1) now says one month or twelve months, as the binding German text does. Tests cover both intervals in both languages, and fail when the builder ignores the interval or the helper misreads it.
…button Art. 246c Nr. 4 EGBGB requires the languages a contract can be concluded in to be stated before the order. The ticket bot's order summary did not name them. It now says "Vertragssprachen: Deutsch, Englisch." right above the order button, as in draft 1.4 of the legal texts. The price statement follows draft 2.3: the price line says only the amount and the period, and a sentence below names MSK Scripts (Moritz Kohm) as contract partner, Stripe as payment provider, the VAT exemption under § 19 UStG and the price as final price. The old "Payment is handled by Stripe" at the end of the terms line is part of that sentence now. The wording lives in one table, subscriptionTermsTranslations, so the bot landing page can use the same sentences under its price table. The summary moved into its own component so a test can render it and check that the contract languages and the price statement stand before the button; moving either below it turns the test red.
The ticket bot and giveaway bot pages follow plan 5.7 now. The head sits on the anthracite band with the price and the facts beside the heading, then the page goes function by function. The rows of equal icon cards are gone (exclusion list 4.2); items are grouped by spacing and a hairline in a shared FactList, the giveaway commands are a real table. The ticket bot plans are a table in the style of the sample page: plans as columns, features as rows, the recommended plan marked, the trial length at the price. Every value is read from lib/tiers.ts, the trial from lib/stripe.ts. Under the table stands the price statement of draft 2.3 (contract partner, Stripe, § 19 UStG, final prices), from the same strings as the order summary in the dashboard. A test changes a price and a limit in TIER_CONFIG and expects the table to follow, so a number typed into the component fails it. The FAQ is gone from the ticket bot page (decision E4). Its answers moved to where they belong: requirements and licence into a requirements section with the docs link, cancelling under the table, "is switching worth it" into a line that leads to the comparison page, which the page did not link before. The hub text no longer says hosting comes with Premium; it starts at Hosted. Comparison and both statistics pages keep their content and get the same head, lists and tables. Buttons and text links have band variants, because the page green measures about 2.3:1 on anthracite. No screenshots yet: the bot banners carry the old green mono eyebrow, so the pages are laid out to read without an image until real Discord and dashboard screenshots exist.
The navigation has linked to these four addresses since Phase 3, with prefetching switched off because they did not exist. They do now, and PENDING_PAGES is empty. /discord shows both bots side by side: what each does, where its price starts and the way to its page, dashboard and statistics. Description, price and facts are the ones the home page and the bot pages already use; the ticket bot's "from" price is the cheapest paid plan in lib/tiers.ts, which a test checks by changing it. /tools (German /de/web-tools) puts MSK Forms, Paste and Shortener one after the other with their real screenshots, plus the image gallery. Every address comes from content/custom-packages.ts, which now also carries the documentation link of each entry (checked against the docs sitemap) and a German description for the three tools. MSK Forms is the one tool with paid plans; the page links to its own pricing page rather than copying prices from another repository, and states contract partner and VAT exemption as the bot pages do. /about (German /de/ueber-msk) introduces Moritz Kohm, known as Musiker15, without a photo: the facts of the imprint, self-employed since May 2022, what he builds, measured figures with their sources and the places to find him. The figures include the share of refunded or charged-back payments (decision E6), read from the nightly Tebex snapshot together with its counts; any figure whose source fails is left out. A test covers that data path. /fivem/free (German /de/fivem/kostenlos) lists the free scripts with their frameworks, the live server count from fivestats.io, GitHub and the documentation. The free script and tool ids now live once in the content file instead of in each component. All four pages are in the sitemap and have their own title and description in both languages.
Since 19.09.2026 lib/tiers.ts gives bot hosting to Hosted and Business only, and the comparison page's own FAQ already says so. Three of its other passages still promised hosting "on any paid tier" in both languages: the hosting row of the table, the case where our bot is the wrong choice, and the answer on whether a server is needed. A Premium customer reading those would expect something the plan does not include. They now name the Hosted plan, as the FAQ and the landing page do.
Phase 5 added the overview pages, the shared page head, fact list and price table, the stats parts and the order summary of the dashboard, and dropped the icon arrays of the bot pages. Regenerated with graphify update, sequentially and without an LLM.
…tics Terms: § 1 (3) still named German as the only contract language, which contradicts the language clause in § 14 (3); it now points there. The same change went to main directly. Statistics pages: without data the hero showed "Not available" as the green kicker, and green stands for success on this site. The kicker is left out then; the warning line below says it in words. PageHero's kicker is optional for that.
main carries the same terms § 1 (3) fix as this branch, with the old addresses. The branch version is kept for both terms files because it has the fix and the new /de/rechtliches and /en/legal addresses.
The admin had 13 tabs in one row without groups (plan 6.1). It now runs in the shared app shell with the groups of plan 6.6: Overview, Sales, Ticket Bot, Image CDN, Website, Administration. The contents of the areas stay as they were; Phase 10 rebuilds them. Who sees which entry does not change: adminNavSections() in lib/adminTabs.ts is built on visibleTabs() and nothing else, so the sidebar offers exactly the tabs ?tab= accepts, and empty groups are left out. The addresses stay /admin?tab=<id>. A plain click switches in place as the tab bar did (history.replaceState, no server round trip), a middle or modifier click opens the address in a new browser tab. Session, permission checks and adminRoute() are untouched. The admin stays English only (E3), also under /de/admin, but every string of the shell, the overview, the sign-in and the error boundary now comes from adminTranslations in lib/i18n.ts, keyed by language, with adminCopy() falling back to English. Tab labels moved out of ALL_TABS into the table; the tab definitions gained their group. The error codes of the sign-in stay keys into the table, never text from the address. The theme switch gets English labels there instead of the German ones of the address. Sign-in, error boundary and dashboard all render inside the shell, so the admin has the one main landmark in every state.
The layout tree still showed every page directly under app/[lang] and the root layout as the shop frame. It now names the (site) route group, the addresses per server and area of the ticket bot, the (panel) group of the giveaway dashboard, the admin in the shell and components/dashboard.
The route group, the app shell components and the new dashboard pages and loaders, so graphify queries find them.
The ticket bot areas of Phase 8 need three things the shell of Phase 7 did not have yet, and the giveaway and admin areas will need them as well: - lib/format.ts formats dates, sizes, counts and retention periods through Intl, 27.10.2026 on German pages and Oct 27, 2026 on English ones. The dashboards showed three date formats side by side (plan 6.1). Dates are shown in German time, so the server renders the same day the browser hydrates; with the browser's zone a date near midnight would differ. - Notice is the message inside a page: tone in sign, bar and title, never colour alone, with role status or alert for messages after an action. - CopyField shows an address or redirect URI as selectable text with a copy button, and says so when the browser refuses the clipboard. ConfirmDialog now hands the typed confirmation to onConfirm, because the hosting removal route takes the typed server id as its own confirmation, and shows an error inside the dialog so a failed action can be retried. DataTable headers align in the middle, so sortable and plain headers line up.
Each card of the ticket bot dashboard built its own fetch. Phase 8 rebuilds all of them, and the API routes behind them do not change, so the requests have to stay exactly what the routes read: method, address, headers and body. A renamed key reaches a route as undefined and fails there, often in a branch nobody clicks in a test run, such as removing hosting or a yearly order. ticketBotApi.ts now holds every request: checkout and customer portal, both custom domain flows, bot control and logs, hosting set-up and removal, and the transcript list. tests/ticketBotApi.test.ts pins each one against the values the old components sent, copied from them rather than derived from the module. A body that is not JSON (a 429 from proxy.ts, Next's HTML 500) becomes an empty object instead of a parse error.
Phase 7 put the ticket bot dashboard into the app shell, one address per
server and area, but the areas still rendered the old tab contents. This
rebuilds them after plan 6.4, each as a component of its own:
- Overview: plan with trial or period end, transcripts stored and how long
each is kept (from lib/tiers.ts), domain and hosting state as badges in
word and sign. Below, only what needs a look: a running trial, a hosted
bot that is down, a domain waiting for DNS. Then the subscription with
the order summary of Phase 5, and server id, API key and documentation.
- Domains: both custom domains state their status once, put the DNS steps
before the field, save with the same words ("Save domain") and remove
only through a confirmation dialog. The upgrade line on a free plan now
leads to the order summary; it used to start the Stripe checkout
directly, without the summary § 312j (2) BGB puts before the order.
- Transcripts: a DataTable with sortable columns, sizes right-aligned,
dates per language, paging below, separate empty states for a first use,
a filter without results and an error, and the retention of the plan.
- Hosting: one main action that follows the bot's state (start a stopped
bot, restart a running one), stopping as the second action behind a
confirmation, the update with its warning, the address of the bot's own
dashboard, the live log on the console tokens with pause, the
credentials on FormField and FormSection with errors at their field, and
removing hosting in a danger zone where the server id is typed into an
empty field.
Dates go through lib/format.ts, prices through lib/tiers.ts, every request
through ticketBotApi.ts with the bodies the routes expect. Ownership is
untouched: the page still finds the server only among the session's own,
and every route checks it again.
DashboardClient, DashboardDomainCard, DnsInstructions, TranscriptsCard and
components/BotConfigEditor are gone; the last one was the only user of the
legacy msk-* classes. Translation keys they alone used are removed, the new
copy is in both languages.
tests/ticketBotAreas.test.ts renders the areas with fixture data: the tier
gates match lib/tiers.ts, each destructive action sends nothing until its
confirmation and then the expected body, and dates and prices follow the
page language.
The compatibility layer in globals.css kept the old msk-* utility classes and the Tailwind 3 token aliases (bg-surface, border-borderlt, text-dim and friends) alive for components that had not moved to the new tokens. Since the hosted bot's control panel was rebuilt, nothing in app, components, lib, content or tests uses a single one of them, so the layer goes now instead of waiting for Phase 11. docs/development.md said the layer existed for BotConfigEditor and named Inter and JetBrains Mono as the fonts; both were out of date.
The ticket bot areas, the request module and the shared notice, copy field and format helpers are new, five old components are gone, so graphify queries find the current structure.
The giveaway areas of Phase 9 need three things the shell did not have yet, and the admin of Phase 10 will need them as well: - RowMenu is the overflow menu of a table row (plan 6.3: one action inline, the rest in a menu). It is a native popover, so the scrolling container of a DataTable cannot clip it, the browser closes it on Escape and on a click outside, and no script injects styles under the nonce CSP. It closes on scroll, so it never floats away from its row. - SaveBar holds Save and Discard of a settings page. It appears only while there are unsaved changes and stays above the tab bar of a phone. - lib/format.ts gains a date with its time of day, a relative time counted from a given moment, and a percentage, each per language. Giveaways end at a minute, and the old dashboard printed US dates with seconds on German pages too. The DotsThree icon joins the icon list for the menu button. The scroll box of DataTable is now positioned: text placed absolutely for screen readers (an "Actions" header, "opens in a new tab") was laid out against the page and widened it on a phone instead of scrolling inside the table.
The giveaway dashboard was one client file of 1,668 lines in which every tab built its own fetch and its own request body. Phase 9 splits it into one module per area, and the two API routes behind them do not change: they pass the body on to the bot, so a renamed key arrives there as missing and is refused, or silently ignored. giveawayApi.ts now holds every request: reading each kind, create, edit, pause, resume, end, cancel, extend, reroll, saving a giveaway as a template, template save and delete, settings, and the four Tebex calls. giveawayModel.ts holds the shapes the bot answers with and the rules that turn a form into a body, moved out of the old file unchanged: prizes, coupons, conditions, bonus clamping, and null versus empty for inherited conditions. Two rules are new and only act before a request: a form is checked by the bot's own limits so a mistake shows at its field, and a duration is a number with a unit (plan 6.5). A stored duration such as 23h59m goes back unchanged while its fields are untouched. tests/giveawayApi.test.ts pins each body against the values the old components sent, written out rather than derived from the modules, and checks that the Tebex status is only asked for by the server owner.
Phase 9 of the redesign (plan 6.5). The giveaway dashboard kept the old tab contents in the new shell; the plan found six buttons per card with a red Cancel as the only filled one, winners as raw ids next to US dates with seconds, no overview, settings without structure, colour as a text field and the first channel preselected when creating a giveaway. - Overview: running, entries, next end and ended as tiles, below that what needs a look (paused giveaways, a recent one without a winner, a coupon without a Tebex key for the owner) and the running giveaways with time left and entries as bars. The bot keeps no history, so there is no chart over time. - Giveaways: a table with a status filter, state in word and sign, one action inline and the rest in the row menu. Ending, cancelling and drawing again ask first with buttons that name the consequence. Each state offers only what the bot accepts in it, as the cards did. - Each giveaway has a page with its facts, prizes and winners, and the edit form while it is open. Creating is a page of its own, in sections, with errors at the field and no preselected channel. A template can be passed in the address to start from it. - Templates: a table and an editor page per template. - Settings: sections with a save bar, colour as a colour field with hex, role lists as a searchable choice with chips, and a button preview. - Tebex stays the owner's only; removing the key sits in a danger zone and asks first. The old file is split into one module per area plus the provider, form parts and a shared action hook. The provider stays in the layout, so moving between areas and sub-pages does not load again. Requests, API routes, session and permission checks are unchanged; the store area is still rendered for the owner only and the page still sends anyone else to the overview. tests/giveawayAreas.test.ts renders every area with fixture data in both languages and checks the owner gates, that destructive actions send only from their confirmation with the old bodies, what each state offers, and dates and numbers per language.
The giveaway dashboard is now one module per area with its request and model modules, the old 1,668-line client file is gone, and the shell has a row menu and a save bar, so graphify queries find the current structure.
Phase 10 of the redesign, plan 7 ("Datenschutz im Admin"). The admin sent
every buyer's email and every banned player's IP to the browser in full,
and the plan asks for them to be masked by default, with a reveal per row
that is recorded in the audit log (Art. 5 (1) c and Art. 25 GDPR). Masking
in the browser would not be masking: the full value would still sit in the
JSON, the network panel and any cache in between. So it happens before the
response leaves the server.
lib/adminMask.ts masks an email as m***@g***.com #3f9a and an IP as
192.168.x.x #3f9a (IPv6 2001:db8:x:x #3f9a). The tag is the start of an
HMAC of the whole value, keyed from SESSION_SECRET, so the same buyer keeps
the same tag across rows and a list of known addresses cannot be checked
against it offline. Fields named like an address are masked whatever they
hold; addresses inside free text (a ban reason, a note, an audit detail)
are found by pattern and checked with isIP, so times and dates stay.
Masked: payments, bans, player lookup, gift cards, coupons, uploads, the
image inventory and the audit log. API keys leave shortened as the table
always showed them, because plan 6.3 wants every look at a secret in the
admin on record too.
POST /api/admin/reveal hands out one field of one record: the email of a
payment, IP or email of a ban, the key of a ticket bot server. It runs
through adminRoute (session, member, Origin check, general limit), then a
limit of its own per admin, then kind, field and id against the allow-list
in lib/adminReveal.ts, then the permission of the area the record is shown
in. An unknown record answers exactly like a missing permission. The audit
entry (who, record, field, never the value) is written before the value is
sent; writeAudit now reports whether it succeeded, and without the entry
nothing is revealed.
Also for the rebuilt areas: GET /api/admin/overview computes the figures of
the overview (sales of the last 30 days from the latest 100 payments, new
API keys, pending uploads and images without a label) per permission, so
only counts leave the server. The audit log names the admin through a LEFT
JOIN on the team, and the packages carry product and variant from the
catalog grouping, so Encrypted and Source show as a pair.
Phase 10 rebuilds the thirteen admin areas, as Phases 8 and 9 did with the bot dashboards. Each old tab built its own fetch and its own body, and the routes behind them do not change: a renamed key would arrive as missing and be refused, or be ignored without a word. adminRequests.ts holds every write the areas send (give a package, refund, coupons, gift cards, packages, bans, API keys, images, uploads, the news popup, the team) plus the player lookup, the reveal of a masked value and sign-out, and the addresses the areas read through useAdminResource. Each body is the one the old tab sent, key for key. A failure becomes an Error with the route's sentence, or the caller's when the body is no JSON (the proxy answers 429 as text). tests/adminRequests.test.ts pins every body against the old components, written out rather than derived from the module.
Phase 10 of the redesign (plan 6.6). The admin kept the old tab contents
inside the new shell: an overview that listed the person's own
permissions, an audit log of Discord ids and raw JSON, payments without
filter or sum and a Refund button in every row, window.confirm and
window.prompt for what cannot be undone, and inline English everywhere.
- Overview: revenue, payments and refunds of the last 30 days and new API
keys as tiles that lead to their area, a trend only when the latest 100
payments reach back far enough to compare, and below them what needs a
look (uploads waiting, images without a label, chargebacks), each only
when it is not zero.
- Payments: a table with filters for paid, free, refunded and charged
back, a period and the sum. A refund only in the detail of a paid
payment, behind a confirmation that names the amount and asks for the
transaction id. "Give package" is the page's one primary action.
- Audit log: sentences with the admin's name ("Musiker15 approved the
upload opengewerk-logo"), filters for person, action and period, the
recorded detail behind a disclosure.
- Team: permissions grouped like the sidebar, role templates that set the
boxes, a save bar; removing asks first.
- Packages: Encrypted and Source of one script in one row. Bans: date,
search, IP and email masked with a reveal. Lookup: a hint for a CFX.re
name instead of "Notch". Coupons, gift cards, API keys, images, uploads
and the news popup moved onto DataTable, RowMenu, FormField, SaveBar and
ConfirmDialog; the preview of the popup follows the theme.
- Every action that cannot be undone sends only from its confirmation,
one per row so the dialog knows its record from props. Errors stand at
the field. Tables scroll in their own container, pages are 25 rows.
- Locked areas (plan 6.6, the Ticket Tool pattern): the sidebar lists every
area, and the ones the person may not open stay as text with a lock and
the permission they need. Their page shows that note and mounts nothing
that loads; `?tab=` to such an area no longer falls back to Overview in
silence. The routes refuse exactly as before.
- Masked values come from the server and get a Reveal button per row; the
full value stays in the state of that row. API keys are copied after a
reveal, which is recorded like every other.
- Every string is in adminTranslations, English only (E3) and ready for a
second language; the permission labels moved there from lib/adminPerms.
Dates and amounts go through lib/format, which gains formatMoney.
The area files replace the tab files one to one, plus adminUi.tsx for
what they share. tests/adminAreas.test.ts renders every area with the
JSON the real routes send for fixtures full of addresses and checks that
none reaches the markup, that reveals ask for the one field, that each
destructive action sends only from its confirmation with the old body, and
the content plan 6.6 asks for. tests/adminNavLocked.test.ts covers the
locked entries, their pages and the routes that still refuse.
Plan 6.6 moves the developer notes that stood above the admin tabs into the documentation and leaves a help link in the admin. docs/admin-dashboard.md has one section per area, reached from each area's "Admin guide" link, and describes the masking: the format, where it applies, the reveal route with its checks and its audit entry. features.md links to it.
- The payments table lost its Details column: the transaction id opens the detail now, which leaves the email and its reveal room at 1440 px. - A reveal button is no longer disabled while it loads. A disabled button drops the focus, and the same button turns into Hide; outside a dense table it is 44 px, as in the payment detail. - Role templates in the team editor no longer stretch to the height of the section heading, link buttons in tables sit flush with the text, and the filesystem check says it has not run instead of showing an empty box. - The audit fixture is in the order the route sorts by.
The admin is now one module per area with a request module and shared parts, the old tab files are gone, and masking, the reveal route and the overview route are new, so graphify queries find the current structure.
After the redesign a number of design leftovers had no reader: the second green (--color-accent and its label), the labels of status fills that no component draws (success, warning, info), the Discord text colour, the Premium+ tier colour, the eight ecosystem badge colours, the 3xl and 4xl breakpoints, two slide animations, the hero gradient, container-prose and the .eyebrow alias. Keeping them meant DESIGN.md and the contrast test described colours the site no longer shows. Badge is now what every call site already used it as, the soft green tag, so its unused ecosystem variants go and the call sites lose the variant prop. Card loses its five sub components, nothing imported them. The sale price keeps its colour under the name --color-sale, since it is no badge. The four remaining .eyebrow spans use .kicker, which had the same rule. The contrast test is rewritten in English around token names: it now also checks text on the tile, the hovered green links, the status colours on ground, card, tile and their own tint, the Discord mark, the Business chart label, the sale price and the focus ring on a card. A new coverage test fails when a colour token has no pair, so a new token cannot ship untested. The comments in globals.css are English now and describe the current rules instead of their history.
The icon module is meant to be the bundle's allowlist, but 25 of its 116 icons had no import left after the rebuilt pages and dashboards, among them Bell, ChartBar, Rocket, Shield, TerminalWindow and Wrench. They go, and the module comment now says that an icon without an import is removed. The comments in the icon module, the GitHub mark and the gallery category tile still explained themselves against lucide-react; they now describe the Phosphor setup as it is.
The rebuilt pages left copy behind that nothing renders: 23 translation lines in lib/i18n.ts (the giveaway overview counters, the verify step's "signed in as", the resource rank, the image detail arrows, the order cancelled kicker, an admin team field and two more), the featured package ids of the old home page, the custom packages section title and the CommandRow type of the old command table. content/custom-packages.ts loses the fields only the old package cards read: price, isFree, the button labels (the translations carry those now), the tags and the colour variant of each badge, since every tag is the same soft green. Its header explains the fields that are left and how an entry reaches the page. The two landing copy headers no longer talk about icons that are kept in step with the lists.
…ette The readable view of sitemap.xml was the last place that still painted the GitHub dark palette (#161a20, #1f242c, #e6edf3, #8b949e) and the old dark green #60b02f. That palette is on the exclusion list of the redesign. The stylesheet now uses the values of the dark theme tokens: ground, card, text, muted text, hairline and green. Search engines read the XML and never see this, so only people checking the sitemap by hand notice the change.
DESIGN.md still described the workbench design: Inter and JetBrains Mono, a GitHub-dark ground, two greens with a monospace label rule and ecosystem coloured badges. None of that is in the code since the redesign, so the file pointed anyone adding a page the wrong way. It now covers the principles and the binding exclusion list, the colour tokens per theme with their roles, the fixed places of MSK green and the logo ramp, the contrast rule and what tests/contrast.test.ts checks, the fluid type scale and fonts, the layout tokens and the breakpoints that are actually used, the components of components/ui, components/page and components/dashboard with when to use which, icons, motion, accessibility, the bilingual rules, and how to add a colour or an icon. It links the docs pages instead of repeating them and names the two things plan 4.7 asks for that are not built yet (prefers-contrast and a forced-colors pass).
docs/development.md still named Inter, JetBrains Mono and lucide-react, gave #60b02f as the dark green, listed a Container component and a BotConfigEditor.tsx that no longer exist, described the admin as one *Tab.tsx per area, showed a NEWS_POPUP constant that moved into the database, called the custom packages "Discord bots and GitHub repos" and listed js-cookie, which nothing imports. The tree now has the page, stats and account folders, the full dashboard kit and the docs that exist, and the styling section names the anthracite ground, the contrast test and the utility classes of globals.css. The test count is current. docs/features.md gave the legal forms as unprefixed German addresses and /report, and promised badges, a custom packages section and a release feed that "moves in Phase 4"; docs/admin-dashboard.md pointed uploads at /images/upload. Both now give the /en and /de addresses. ADMIN_DASHBOARD_PLAN.md and PRICING_MIGRATION.md stay as written and get one line that marks them as historical. The README links the admin guide and says what DESIGN.md covers.
Card lost its sub components, Badge its variants, the icon module 25 icons and lib/config.ts the featured package ids, so graphify queries should no longer find them.
Plan 4.7 asks the shop to follow the visitor's system settings; only reduced motion was in the code. "More contrast" now raises muted text and lines to full strength, and in Windows contrast themes controls keep a system border and focus uses the system highlight. A test guards the three rules; a weakened contrast value turns it red. SECURITY.md pointed at the upload client's old path. js-cookie and its types were in package.json although nothing imports them any more.
A browser that reaches the bot dashboard host without a valid proxy session went to the fixed BOT_DASHBOARD_RETURN_URL. On the server that still names /ticketbot/dashboard, which after the redesign only works through a 308 and always lands in English. The address is now built from the route table in the visitor's language, taken from Accept-Language because the shop's msk_lang cookie does not reach the subdomain. The header can only choose between /en and /de, never the origin or the path; a test covers both and fails when the language is ignored. The env variable is no longer read.
Musiker15
marked this pull request as ready for review
October 28, 2026 14:43
|
Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here. So far, Strix has reviewed 50 pull requests and surfaced 1 security issue across this workspace. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft while the redesign is in progress. Every phase lands here as one or more commits, CI runs on each push, and the whole thing goes live together when it is merged.
Phases
/enand/dewith a route table, redirects, hreflang, sitemap per language. Speaking product slugs follow in Phase 4.Ground rules
clamp(), components use container queries, running text stays at 65 characters.