Bicrypto 6.6.1
2 August 2026
This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.
Core v6.6.1
Release Date: August 2, 2026 Tags: SMS, NOTIFICATIONS, TWO-FACTOR, SECURITY, PROVIDERS, MSG91, TWILIO, ADMIN, SETTINGS, WITHDRAWALS, OPERATIONS, BUG-FIXES, DELIVERABILITY, PAYMENTS, TRANSFI, DLOCAL, DEPOSITS, PAYOUTS, CRYPTO, ONRAMP, OFFRAMP, IBAN, FEES, GATEWAYS, CREDENTIALS, WEBHOOKS, SETUP, CHARTS, DATES, TIMEZONES, DASHBOARDS, ANALYTICS, TRANSLATIONS, LOCALISATION, PLURALS, LOADING-STATES, ANIMATION, DATA-TABLE, ADDONS, SUPPORT, CRM, TICKETS, LIVE-CHAT, FILTERS, MENUS, NAVIGATION, HEADER, WALLETS, CURRENCIES, ICONS, BALANCES, EXCHANGE-RATES, MULTI-CURRENCY, REPORTING, BLOG, CATEGORIES, UPLOADS, IMAGES, MEDIA, KYC, AFFILIATE, NFT
Overview
Seven bodies of work: SMS, a new payments provider, a rebuilt payment gateway admin, a product-wide sweep of loading states, a repair of the support desk, a correction to what a wallet balance means, and a platform-wide sweep of money figures that were adding currencies together.
Two more land alongside them: a blog category that could never be used and images that would not upload, and the shared chart kit, translation layer and table chrome that the rebuilt addon dashboards are built on.
Several of these move figures on your screen, and some need something from you afterwards — wallet balances, analytics cards, support ticket statuses, chart date labels, and thirty-two admin screens that now require their permission granted per role. Every one of them is a correction, and every one is described in Upgrade Notes. Read that section before you update.
Update Instructions
Update your addons in the same pass as the core. Every addon's admin dashboard was rebuilt in this cycle, and the parts those dashboards are built from — the shared chart kit, the table chrome, the translation layer — ship here, in the core. The two halves are one release: take the core on its own and your addons keep the dashboards they already had, and take an addon's new version without this core and its dashboard is missing the pieces it draws itself with.
So download everything first — the core release and every extension, blockchain and provider you have — and finalise once:
pnpm update-all --dry-run # lists every pending version, downloads nothing
pnpm update-all # downloads them all, then runs the chain itselfpnpm update-all ends by running the update chain, so there is no pnpm updator to run after it.
If you would rather download from the admin panel, take all of them before you run anything — the core under Admin → System → System Updates, the rest under Admin → System → Extension Manager — and only then finalise, once:
pnpm updatorEither way it is one maintenance window rather than one per product. An addon extracted after the chain has already run is sitting on disk doing nothing: its schema changes have not been applied, its seeders have not run, and its admin pages are not in the frontend that was just built. What you see is an addon screen that 404s or still shows its old dashboard, and the only cure is another full pnpm updator — another stop, rebuild and restart.
Nothing else needs to change. Installations using Twilio keep sending exactly as before, and TransFi ships switched off — nothing about payments changes until an operator enables it.
Three things change on their own, all corrections: customers finishing a payment on six gateways now land on a page that exists, a gateway's alias becomes read-only, and dates on charts are now labelled in the timezone the underlying bucket was cut in rather than in the timezone of whoever happens to be looking. All three are in Upgrade Notes.
Two things appear on their own and are not corrections. Your customers' wallets page now shows each currency's coin logo beside its code — a currency with no icon is drawn exactly as it was before, and if you would rather the page stayed text-only, turn Currency Icons off under Admin → System → Settings → Wallet. The same list also now leads with the currency rather than the wallet type; no column is added or removed.
Your support tickets are repaired as part of the update. pnpm updator runs the seeders, and one of them re-derives every open ticket's status from who spoke last in it — because tickets already stored under the old, ambiguous status would otherwise stay invisible in a queue that is now correct. It changes no ticket that is closed, and running it twice does nothing the second time.
Balances and money figures move on their own, and every one of those moves is a correction. A customer's wallet total no longer counts deposits nobody has approved, so an account with a pending bank transfer will show a lower balance than it did yesterday — the pending amount is still on the page, stated as awaiting approval. Figures that were adding several currencies together now convert first, so a merchant dashboard, an NFT volume, a bot's profit or an analytics card may move by orders of magnitude. Nothing is recalculated in your database; these are display figures being computed correctly. All of it is in Upgrade Notes.
One thing does need doing. Thirty-two admin screens now enforce the permission their own source has always declared, so they must be granted per role in Admin → Roles — the same step a new addon needs. Until then they are reachable by Super Admin only, and because Super Admin bypasses permission checks entirely, an upgrade tested from the owner account will look fine while every configured role has lost them. The list is in Upgrade Notes.
Highlights
Payments — TransFi
TransFi is now supported for taking local currency in, paying it out, buying and selling crypto, and issuing customers permanent bank details. It covers 24 deposit and 27 payout currencies across African mobile money, Nigerian bank transfer, SEPA and USD wire.
It is not a sixteenth gateway. Every existing gateway is a charge API: post an amount, get a redirect. TransFi will not create a payment until the customer exists on its side as a verified identity, so a first deposit asks for a date of birth and an address, and the platform now mirrors each depositing customer into TransFi and remembers who they are there.
Paying out is entirely new ground. Fiat withdrawal has always been manual — an operator paid by bank and recorded it. This is the platform's first outbound money movement, and it brought with it the money-safety rules the spot and ecosystem paths learned the hard way: claim before sending, refund before changing status, and never refund on the absence of evidence.
Building against these code paths surfaced six pre-existing faults. Two of them lost customer money, and one meant a gateway already in production could never credit a deposit at all.
Three behaviour changes affect money and one needs you to check your history — read Upgrade Notes before updating.
Payment gateways — setup you can finish from the admin
Sixteen gateways ship with the platform. None of what makes one work was anywhere in the admin. The gateway list showed a title, a logo, fees and an on/off switch — and none of those decide whether a payment can happen. Credentials, a webhook URL and whether you are pointed at test or live keys do, and all three live outside the database, so a gateway could sit switched on, looking healthy, and fail every deposit.
That is not a hypothetical. On the installation this was built against, twelve of sixteen gateways were switched on and could not authenticate. The list page now says so before anything else.
Both screens are rebuilt. Each gateway carries a readiness verdict rather than a switch position, and opening one gives you the setup guide for that specific vendor: which values it needs and what each is for, the webhook and return URLs to paste into their dashboard, the steps in the order they have to be done, and the traps that make a wrong setup look like a right one. Test connection checks a key against the vendor before it is saved anywhere.
Building it surfaced three pre-existing faults in the gateway code. One sent every paying customer to a page that has never existed, on six gateways.
Two behaviour changes apply without any action from you — read Upgrade Notes before updating.
SMS
An SMS release. One-time codes can now be delivered by MSG91 instead of Twilio, which is substantially cheaper for Indian traffic, and there is a new admin screen to compare the two, validate credentials before saving them, and see which is carrying what.
Getting there meant unpicking Twilio from the platform. SMS was written directly into fourteen places — every login, every code, the health check — with no way to change it. It now runs through one provider layer, and four separate places that each decided "is SMS available" differently now ask the same question.
Two faults surfaced on the way. Phone verification sent the code before saving it, so a failure at the wrong moment left the customer holding a code that could never be accepted. And SMS two-factor was gated on a Twilio setting the platform has never used, switching the method off on installations where it would have worked.
On the SMS side: one behaviour change on a security path, and one message-wording change.
Loading states — the product stops blanking itself while it waits
Most screens replaced themselves with a spinner while their data loaded, then rebuilt into something a different size. Every page did this at the moment the customer was looking at it. Across a twelve-page sample the total layout movement measured 1.1194; it is now 0.0310. The P2P landing page alone went from 0.7549 to 0.0020.
The fix is not better spinners. A pending page is now the same page with the unknown values replaced — headings, labels, icons, borders and grids render immediately, and only the figures wait. Placeholders are sized by the text they stand in for rather than by a hand-typed height, so they cannot drift when the typography changes.
Chasing it surfaced something larger. Four components — the site footer, the licence gate, a settings provider and the homepage — withheld themselves until the browser had mounted, which meant the server sent a different page than the one that appeared. The footer was absent from the server HTML on every page in the product. The homepage shipped none of its sixteen product sections.
That class of fault also broke things outright. Every custom CMS page returned a 404, on every installation, unconditionally. Payment gateway administration was reachable by Super Admin only. And a licence check that could not reach the server sent operators to a dead end they could not leave.
Thirty-odd further faults were found the same way, most of them the interface stating something false while it waited: a funded wallet reported as $0.00, a live article labelled DRAFT, an order row that read Sell before it knew the side, four notification channels reported Inactive during every poll.
Support — a queue that could not show the tickets it was counting
Customers who wrote in through live chat did not appear in the support queue. The counters above the table moved — the ticket was there, it was counted as pending work — while the table itself said "No data available". There was nothing on the screen to explain the contradiction and nothing an operator could click to resolve it.
The cause was a single word in the queue's default view: it opened filtered to Pending. But Pending means no agent has replied yet, and a ticket only stays there while the customer has said nothing at all — a live chat leaves Pending on the customer's first message. So the default view excluded, by construction, every ticket that had a customer's words in it: the entire set an agent exists to answer. On the desk this was reported from, the queue held four tickets and not one of them was Pending.
Underneath it, one status meant two opposite things. A ticket was marked Replied both when an agent answered and when the customer answered, so nothing downstream could tell "this person is waiting" from "this person has been answered" — including the age column, whose overdue clock stopped on tickets nobody had read.
Two more faults kept it hidden. The filter deciding which rows existed did not draw itself: it kept its selection privately, so the control showed an empty placeholder and offered nothing to clear. And the ability to filter on more than one value at a time — which "waiting on an agent" requires, because it is two statuses — had never worked anywhere in the product.
The desk's own figures were separately wrong. Simply opening the chat bubble minted a ticket before a word was typed, and those empty sessions counted as work. Avg Response read 55263m under the caption "Lightning fast". And a first-response time was never recorded at all if the ticket was assigned before it was answered — which is the normal way to work, and the exact thing the queue's own Assign to me button encourages.
Ticket statuses are repaired on update and your response-time figures will move — read Upgrade Notes before updating.
Wallets — a balance is money you have
Two faults on the wallets page, both reported from a live installation, and both of them told the customer something untrue about their own money.
A deposit that nobody had approved was already spendable. A customer choosing a manual method — Bank Transfer — filled in the form, submitted, and the figure at the top of the page went up immediately. Nothing had arrived. No operator had seen the transfer, no bank had confirmed it, and the wallet itself was untouched — but the page counted the request as though it were funds. Rejecting the deposit took it away again, which read as the platform confiscating money rather than as a number that should never have been there. A 40,000 NGN bank transfer was spendable the moment the customer clicked submit.
And naira was being counted as dollars. The allocation panel beside the balance added every wallet's raw figure together and put a $ in front of the total, so 40,000 NGN read as $39.96K rather than about $29, and Fiat took 100% of a share breakdown of a portfolio that was mostly crypto. The conversion existed and was correct — the page was reading the wrong figure next to it.
Testing that path end to end found five more faults in the same flow, and one of them meant an operator could not approve a bank transfer at all: the approve action demanded a Reference ID from a form that never marked it required, on a deposit type that has no reference to give. Two of the five moved money the wrong way.
Balances on screen will change when you update, and two of these need you to check your records — read Upgrade Notes before updating.
Money figures — the dollar sign that was not earned
The naira fault above was not one page's mistake. It is a pattern, and it was in about thirty places across the product: take rows that each carry their own currency, add the amounts together, print the total as dollars. 40,000 NGN plus 0.5 BTC plus 25 USDT is a number denominated in nothing.
It is hard to see because the dollar sign is usually not in the source at all. The shared statistic card defaults to USD when nothing tells it otherwise, so a figure acquires a $ by omission. Searching the product for "$" finds a minority of them.
Every one is now fixed. Gateway merchant dashboards and payouts, the NFT marketplace, trading bot profit, copy-trading leader and follower statistics, forex, ICO, affiliate rewards, staking, investments and the public markets page. Three were worse than a mislabel: the NFT marketplace priced a mixed-currency total at the Ethereum rate — roughly three thousand times over for a collection trading in stablecoins — a second NFT figure used a hardcoded 2022 exchange rate table, and a bot's simulated paper profit was being added to real profit and shown as one number.
The analytics tabs needed more than a relabel. Their figures come from a shared engine that could only ask the database for a plain total, so there was no way to express "convert each currency first" — which is why seven money cards on the transaction history alone were wrong, and the two totals derived from them with it. The engine now understands it, and twenty-eight analytics configurations were converted to use it. Where a figure genuinely cannot be converted, the card now says which currency it had to leave out rather than quietly counting it as zero.
Numbers on admin dashboards and customer pages will move, and they are moving because they were wrong — read Upgrade Notes before updating.
Blog — a category nobody could use, and images that would not upload
Two things were reported from a live site, both about the blog: a category that had been created and saved never appeared in the list when writing an article, and "all I try to upload doesn't show after". Both were real, and behind them were five separate faults.
The category one is a deadlock rather than a delay. The writer's editor filled its Category list from the public category list — the one the blog's own category page uses, which by design shows only categories that already have a published article in them. A category you have just made has none. And it cannot get one, because an article cannot be filed into a category that is not offered. A new category was locked out permanently, and nothing on the screen said so. On the installation this was traced on, sixteen categories existed and four could be picked.
The images were four unrelated faults that all look identical from a chair. Anything over roughly 3.75 MB was refused by the server while the picker happily accepted up to 5 MB — an ordinary phone photo. The picker offered SVG, which the upload endpoint has never accepted and never will. A category could not be saved at all with the image left blank, which pushed operators into attaching one. And on a live server a newly uploaded image did not appear until the site was restarted, unless the web server had been given a specific rule by hand — so old images worked and new ones did not.
What kept all four invisible is the same thing: a rejected upload cancelled the save and left nothing on the screen. Not on the image field, not anywhere. Per-field errors from the API could not reach a form in the admin at all — the code that would have displayed them had never once run.
The field also never said what it accepts. It does now, on every image picker in the admin, not just this one.
Pulling on the "reports success but changed nothing" thread found more of it. Every place the platform flips a status was checked against the table it writes to — ninety-one in total — and six were writing to a column that does not exist, which a database quietly discards. Each returned "updated successfully" and recorded the change in the admin audit trail. One more named a table that does not exist and failed every time it was called. That whole class is now rejected outright rather than reported as done.
One upload limit is stated differently and newly uploaded images are served a new way — read Upgrade Notes before updating.
The platform under the new addon dashboards
Every addon's admin dashboard was rebuilt in this cycle. Those ship in their own addons' notes; what lands here is the shared chart kit, translation layer and table chrome they are built on — and it lands on screens you already have.
Building them surfaced one fault that makes a figure on your screen wrong. Every dated chart drawn with the shared chart kit could name the wrong day, and at the turn of a year the wrong year — the totals were right, the labels above them were not, and only for some of the people reading them.
The translation layer took the rest of the work. Count-aware messages — "1 owner", "12 owners", "No owners" — had never actually worked; the first message to use one would have shown a reader its own formatting instead of a sentence. The list of translation areas the build checks against is kept by hand and had quietly fallen behind the message files, so the first page to ask for one of two missing areas failed to build. And 886 new English strings went in across seventeen addon admin areas and into all 90 language files.
The rest is waiting. Shared chart cards, legends, tiles and money figures now hold their own shape while data is in flight, and the data table and its analytics tab stopped animating themselves into existence on every page load.
Two things need attention after you update — thirty-two admin screens now require their own permission granted per role, and chart date labels move because the old ones were wrong. Read Upgrade Notes before updating.
Upgrade Notes
A deposit that has not been approved is no longer part of the balance
Submitting a manual deposit raised the customer's displayed balance immediately. No money had arrived, no operator had approved anything, and the wallet itself was never credited — the figure on the page was adding the request to it. Rejecting the deposit removed it again, which looked to the customer like funds being taken away.
- Changed: the balance is settled money only. A deposit awaiting approval is shown separately, on the same page, as "awaiting approval — not included in your balance", with its own line on each wallet type. A wallet whose only funds are an unapproved deposit no longer counts as active.
Approving a bank transfer works again — check your pending deposits
Approving a manual fiat deposit from its detail page was refused with "Reference Id is required". The form does not mark that field required, and a bank transfer has no provider reference to put in it, so the only way through was to invent a value. Approving from the deposit queue worked, which is why this could sit unnoticed.
- Changed: a deposit approves without a reference. A reference you do supply is still validated.
A fee larger than the deposit no longer takes the whole deposit
If a deposit method's fees came to at least the deposit amount, approving it credited the customer nothing, booked the entire fee as platform profit, and marked the deposit COMPLETED. On the installation this was found on, a method with a 1.01 flat fee had taken 1.00 deposits.
- Changed: approval is refused with the two figures named, and the deposit stays pending so the fee can be corrected first.
Deposits can no longer be settled from the generic transaction editor
The transaction editor under Admin → Finance could set a pending deposit to COMPLETED. That screen only writes the record — it moves no money — so the customer was told their deposit had completed and received nothing. It was worse than a no-op: the real approval route refuses any deposit that is no longer pending, so the one action that could have paid them was permanently blocked.
- Changed: settling a deposit or a withdrawal from that screen is refused, naming the screen that can. Editing the record's other fields is unaffected.
dLocal deposits could never credit anyone
Three separate faults, each of them fatal on its own, meant no dLocal deposit was ever credited by the notification path. The gateway looked healthy and customers were charged.
- Changed: dLocal deposits credit. The transaction reference the deposit is created with is the one both the notification handler and the verification step look it up by; a refund's ledger entry no longer collides with the deposit's own; and three of dLocal's statuses that the platform's records reject outright — chargeback, partial refund, authorised — are translated to statuses it accepts, so the notification no longer fails before reaching the step it was carrying.
Money figures that added several currencies together now convert first
About thirty figures across the product summed amounts denominated in different currencies and printed the total as dollars. The dollar sign was rarely written anywhere — the shared statistic card supplies one when nothing tells it the unit.
- Changed: each currency is converted at its own rate before anything is added. Where that is not possible the figure is grouped by currency, or the symbol is dropped and the real unit named. A currency with no configured rate is excluded and declared — the card says "Excludes NGN — no exchange rate" — rather than being counted as zero.
Analytics cards on your admin pages will move
The analytics tabs get their figures from a shared engine that could only ask the database for a plain total, with no way to say "convert each currency first". Every money card on a multi-currency table was therefore a mixed-unit sum wearing a dollar sign — seven of them on the transaction history alone, plus the two totals derived from those seven.
- Changed: twenty-eight analytics configurations now convert per currency before totalling. Cards that measure an average or a maximum across denominations — which cannot be converted meaningfully — have had the dollar sign removed instead of being given a false one.
A retired forex plan no longer zeroes a customer's portfolio
- Changed: the forex overview reads an investment's currency from its plan even when that plan has been retired. It previously skipped those investments entirely, so a customer whose only plan had been retired was shown $0 invested and $0 profit with the investments still listed in their history.
Rejected withdrawals refund correctly now — check your history
A rejected withdrawal refunded the customer using a key scoped to their wallet, not to the withdrawal. The duplicate check is global, so the first rejection on a wallet refunded and every later one was silently skipped — after the withdrawal had already been marked rejected.
- Changed: refunds are keyed per transaction, so repeated rejections on the same wallet each refund correctly.
Approving a withdrawal now works on fiat, and refuses on ecosystem
The approve action only ever handled spot wallets — it read blockchain fields from the withdrawal and looked for a spot wallet — so Approve on a fiat withdrawal could not work, from either the withdrawal page or the bulk queue.
- Changed: fiat withdrawals approve as bookkeeping and book the platform fee at settlement. Ecosystem withdrawals are refused with a reason, because they are settled on-chain by the ecosystem queue. Spot is unchanged.
Provider-executed payouts cannot be completed by hand
- Changed: marking a TransFi-dispatched withdrawal COMPLETED is refused. Its status comes from the provider webhook or the payout reconciler.
Paying customers no longer land on a 404 after a successful payment
Six gateways sent the customer to /user/wallet/deposit/<gateway>/verify when they finished paying. There has never been such a page. Unless you had set the return URL yourself, the last thing a customer saw after handing over money was a not-found page.
- Changed: the defaults now point at the deposit page, which exists. Affects Mollie, PayFast, Paysafe, Paystack, Paytm and PayU — thirteen return, callback, success, failure and cancel URLs in total.
A gateway's alias can no longer be changed
- Changed: the gateway update API no longer accepts
alias, and it is shown read-only with the reason.
Chart dates were labelled a day early for everyone behind UTC — check anything you read off a dated chart
The dashboards behind these charts cut their daily and monthly buckets at UTC midnight and send each boundary as an exact instant. The label was then drawn in the timezone of the browser looking at it. So a bucket holding 2 August was labelled Aug 01 for every viewer behind UTC — which is all of the Americas — while the bar's own value was the whole of 2 August.
- Changed: a bucket boundary that states its timezone is labelled in that timezone. The axis tick, the tooltip and the total in the bar now agree with each other, and every reader sees the same label.
- What was wrong, precisely: the offset moves the label, never the value. No total, average or count was ever wrong — the name over it was. On a chart of months the same error moves the year: the bucket holding 1 January was labelled December of the year before.
Support tickets your customers are waiting on will appear — go and read them
The support queue opened filtered to Pending, and a ticket leaves Pending the moment its customer says anything. Every live chat and every followed-up ticket was therefore excluded from the default view, while the counters above it kept counting them.
- Changed: the queue opens on everything waiting on an agent — tickets nobody has answered and tickets whose last message came from the customer — oldest first. The filter is now shown, ticked, in the filter panel, so the scope is visible and one click wide.
Ticket statuses are repaired on update
A ticket was marked Replied both when an agent answered and when the customer answered, so the stored status could not say who was waiting.
- Changed: status now states whose turn it is — Pending (nobody has answered yet), Open (the customer spoke last), Replied (an agent spoke last), Closed. The update re-derives the status of every ticket that is not closed from the last message in it.
Average first response will change, and it may look worse
- Changed: a first-response time is now recorded when an agent first replies, rather than only when a reply also happened to be the moment the ticket got assigned. It is also recorded when the answer took under a minute, which previously counted as no measurement at all.
- What was wrong, precisely: the platform decided "this is the first agent reply" by checking whether the ticket was still unassigned. So on any desk where operators assign a ticket and then answer it — what the queue's own Assign to me action exists for — no response time was ever stored, and the headline average was computed from whichever few tickets happened to be answered by someone who skipped assignment. A reply inside the same minute scored zero and was then discarded as "not measured", so the fastest work on the desk was also excluded.
The support desk's counters and its table now describe the same tickets
- Changed: the figures in the header are computed over exactly the rows the table can show. Three things had made them disagree: the table hid tickets raised by the admin looking at it while the counters included them, empty live-chat sessions counted as work, and Unassigned counted closed tickets nobody had ever been assigned to.
SMS two-factor may become available where it previously was not
Whether SMS could satisfy two-factor was decided by a setting called APP_TWILIO_VERIFY_SERVICE_SID. The platform has never used Twilio Verify — it generates its own codes and checks them itself — so that setting was standing in for a service nothing called.
- Changed: availability is now decided by whether the installation can actually send an SMS. On an installation with Twilio credentials and a phone number but that setting left blank, SMS two-factor becomes available where it was previously switched off, including as a way to approve a withdrawal.
One code message is worded differently
Enabling SMS two-factor from your profile sent "Your OTP is: 123456". It now reads "Your OTP code is: 123456", matching the eight other places that already sent that wording.
The SMS health check no longer sends a text message
Admin → System → Health tested SMS by sending a real message to your support number.
- Changed: it now validates credentials directly with the provider. It no longer spends money or texts anybody, and it reports which provider it checked.
Your custom pages start working — check them
Every page built in the page builder returned a 404, on every installation. The catch-all route decided whether the builder was active inside an effect whose closure had captured the setting before it loaded. The expression was false by construction, so the timer always concluded "not a builder page" and called notFound(). The configured landing type was never actually consulted.
- Changed: the decision is made on the server, before any HTML is written. Your custom pages render, and their stored SEO title, description and keywords now reach the browser.
- What to do: look at your custom pages after updating. If you built pages that appeared to be broken and gave up on them, they will now be live. A page that no longer exists 404s on the server as it should, rather than returning a 200 and swapping itself out afterwards.
A licence check that cannot reach the server no longer blocks the admin
The licence gate treated "the API said this is unlicensed" and "the API did not answer" as the same outcome. On any failure — a 500, a dropped connection, a malformed reply — nineteen admin areas rendered "License Required — Redirecting…" with nothing to redirect to. The author's intended fail-open could never run, because the error handling was written for exceptions and the fetch layer here returns errors rather than throwing.
- Changed: an unreachable or unreadable licence service now lets the admin through and logs a warning. Only a definitive "not licensed" denies, and it can only do so once it has a real activation page to send you to.
- Why this is safe: the backend enforces licensing on its own routes and returns a 403 regardless. The gate is a signpost, not the lock — so failing open grants no capability, while failing closed locked paying customers out of their own back office during a blip.
Thirty-two admin screens now need their permission granted — do this after updating
The route permission manifest had drifted from the files that define it: 185 entries against 217 defined. Those thirty-two screens were reachable by anyone with generic admin access, regardless of what their own permission file said.
- Changed: the manifest is regenerated from source, so each screen now requires its own permission — as it was always meant to.
- What to do: grant the new keys per role in Admin → Roles, the same step a new addon needs. Until you do, these screens are reachable by Super Admin only. Affected areas: Trading Bots (7 screens), Binary AI Engine (13), Hummingbot (5), plus the audit log, SMS notification settings, binary settings, the NFT admin and logo settings.
- Test as a normal admin role, not as Super Admin. Super Admin bypasses permission checks by name, so an upgrade tested from the owner account will look completely fine while every configured role has lost the screens.
- Why it drifted:
pnpm bundlerebuilt the list of permissions that exist on every release and never rebuilt the list of what each route requires. Both now run, so this cannot silently accumulate again.
Two screens now render for roles that previously saw an empty page
- The payment gateway administration area blanked itself on the server, so it was effectively Super Admin only.
- What to do: nothing. This is the fix for a fault, not a change of policy.
Images you upload from now on are served by the application
An uploaded file is written into the site's public folder, and the site read that folder once, at startup. A file that appeared afterwards was not in the list, so it was not served — every image uploaded since the last restart showed as broken while older ones were fine. The documented installation includes a web-server rule that avoids this, and installations set up without it hit the fault silently.
- Changed: requests for uploaded files now fall through to the application when the file is not already known, so a fresh upload appears immediately without a restart. Files that were already there are still served directly, exactly as before.
KYC documents now state a 10 MB limit
The document upload advertised 50 MB and the platform refused anything over roughly 3.75 MB, because the true limit was never the one on the screen — a file is encoded for transport and grows by a third on the way, and the transport ceiling was five.
- Changed: the stated limit is 10 MB, and it is now the real one. This is a rise for every document anybody has actually been able to upload, not a reduction — 50 MB was never achievable.
Four blog endpoints were removed, and status writes now fail loudly
Six places that flip a status were writing to a column that does not exist on the table they targeted. A database discards an unknown column, so nothing was written — and each one still answered "updated successfully" and wrote the change into the admin audit trail.
- Changed: four blog endpoints that could only ever have done this — turning a blog category or tag on or off, when neither has ever had such a setting — are removed. Two more, for marking an affiliate referral reward as claimed, now write the right column. Any status write naming a column that does not exist is now refused with a clear error instead of reported as done.
Added
Wallets — pending money is visible without being spendable
- Added a line under the wallet balance reading "$X awaiting approval — not included in your balance", and a per-type figure beside each allocation row. The information the old behaviour was trying to convey is still there; it is no longer mixed into the balance.
- Added a notice naming any currency the customer holds that has no configured exchange rate. Those balances contribute nothing to the total, so a portfolio total that quietly omits part of itself now says so.
- Added a refresh of the wallet totals after a deposit, withdrawal or transfer. The totals were cached for the whole session, so a customer who moved money and came back to the wallets page was shown the figures from before they did.
Analytics can convert currencies
- Added currency conversion to the shared analytics engine. A money card can now name the column that says what currency each row is in — either its own, or the wallet or plan it belongs to — and the engine totals each currency at its own rate before adding them. This is what makes the money cards on the transaction, deposit, withdrawal, profit, investment, forex, e-commerce, copy-trading, affiliate and ecosystem pages real figures rather than mixed-unit sums.
- Added a declaration of what a converted card could not price. A currency with no rate is left out of the total and named on the card, instead of being folded in as zero — a total that silently omits part of itself is worse than no total.
TransFi deposits
- Added TransFi as a fiat deposit gateway across 24 currencies — M-Pesa, MTN, Airtel, Vodafone, Wave, Orange, Moov, Tigopesa, Halopesa, Zamtel, Nigerian bank transfer, SEPA Instant, SEPA Bank Transfer, Open Banking and USD Fed Wire.
- Added a deposit step that collects the date of birth and address TransFi requires — once, then the customer is remembered.
- Added automatic handling of TransFi's identity screening. A new customer is checked in about half a minute; the deposit waits briefly and then retries on its own rather than making them start again.
- Added a verification step for when TransFi asks for identity documents mid-deposit. Their thresholds are not published and vary by country, so this can happen on any deposit, including for a long-standing customer.
- Added crediting from the settled amount rather than the requested one. TransFi deducts its fee before the money reaches you, so a 5,000 deposit arrives as 4,900 and the customer is credited from what actually landed.
- Added a reconciler, because webhooks alone are not enough here: one TransFi status is never sent by webhook at all, retries stop after about two hours, and a compliance hold has no defined end. It also expires abandoned checkouts, which nothing previously did for gateway deposits.
TransFi payouts
- Added automated fiat withdrawal through TransFi to bank accounts, IBANs, e-wallets and mobile wallets across 27 currencies.
- Added a payout provider registry, which the platform did not have. A withdrawal method can now be bound to a provider; a method with no binding stays manual, which is every existing method.
- Added saved payees. A customer's bank details were previously re-typed into free-text fields on every withdrawal; they are now stored, validated and reused.
- Added two separate switches, both off by default: whether the provider may be used at all, and whether withdrawals are dispatched on request or wait for admin approval.
- Added a reconciler that settles or fails dispatched payouts and resolves withdrawals orphaned by a crash between debiting the customer and reaching the provider. It refunds only on positive evidence that no payout exists; anything ambiguous is escalated for a human rather than guessed.
Buying and selling crypto
- Added TransFi onramp and offramp: customers can buy crypto with local currency, or sell it and be paid to their bank or mobile wallet. Both off by default.
- Added an operator choice of how the crypto is held, because it is a real decision rather than a detail:
- Customer's own wallet (the default) — the platform never touches the crypto and carries no custody risk.
- Platform wallet — requires per-chain deposit addresses and a funded hot wallet, and currently refuses with an explanation rather than half-working, because a mode that cannot match an incoming transfer to the right customer loses people's crypto.
- Added address checking before an order is created, so a mistyped address is caught immediately instead of after the customer has paid.
Permanent bank details for customers
- Added virtual IBANs. A customer can be issued a permanent EUR account and pay into it whenever they like, instead of starting a checkout each time.
- Added protection against duplicates. TransFi will happily issue a second account to the same person on a repeat request; the platform returns the existing one.
A measured rate card
- Added Docs → Payment Gateways → TransFi Rate Card, listing what TransFi charges on every one of the 150 corridor and method combinations, with the minimum and maximum for each.
- Added
npx tsx scripts/transfi-rate-card.ts --write, which regenerates it by asking TransFi for a real quote per corridor.
The gateway page tells you when to lock the webhook signature format
- Added a panel on the TransFi gateway page that reports which signature format TransFi's callbacks actually use, and the exact setting to add once it knows.
Operational visibility
- Added a TransFi entry to Admin → System → Health, which validates credentials against TransFi and reports their own message when they are wrong. Only Stripe had one before, so a gateway enabled with a blank key gave no signal until a customer's deposit failed.
- Added the twelve gateways whose credentials were declared nowhere to the configuration file — 2Checkout, Adyen, Authorize.Net, dLocal, eWAY, iPay88, Klarna, Mollie, PayFast, Paysafe, Paytm and PayU. A fresh installation had no way to discover what to set, and the first symptom was a failed deposit.
- Added a gateway logo inventory recording which logos belong to a live gateway and which are held for one not yet built.
Payment gateway management
At Admin → Finance → Deposit → Payment Gateways.
- Added a readiness verdict on every gateway, in place of a switch position: Accepting deposits, Configured but switched off, On but cannot authenticate, Credentials needed, or No integration. Three separate facts decide it — is there a handler bundled, are its credentials present, is the switch on — and a gateway can be on and broken, which is the state the page now leads with.
- Added a headline alert counting gateways that are switched on and cannot authenticate, with a filter to show only those. Customers can select these on the deposit form and every attempt fails at the vendor.
- Added a test / live indicator per gateway, read from key prefixes, sandbox settings and base-URL overrides. Shipping test keys to production is the most common way a working gateway stops working, and nothing previously reported it.
- Added search across name, alias, region and currency, and filters by readiness.
- Added a summary of gateways, how many are accepting deposits, how many still need credentials, and how many currencies are covered between them.
A setup guide per gateway
Opening a gateway now gives you its own guide rather than a form.
- Added, for all 16 bundled gateways, the environment variables each one reads, what each value is for, whether it is currently set, and which are optional so they do not read as outstanding work.
- Added the webhook and return URLs for that gateway, built from your public site URL and ready to copy into the vendor's dashboard. These were previously discoverable only by reading the integration source.
- Added the setup steps in the order they have to be done, with direct links to sign up, to the vendor dashboard, to the page holding the keys and to their webhook settings.
- Added the vendor-specific traps, stated as warnings rather than left to be discovered — that PayPal's client id is compiled into the frontend and needs a rebuild rather than a restart, that Adyen's client key is locked to the origins you list, that PayU issues two salts and only one works with this integration, that a PayFast passphrase set on one side only breaks every signature, that Paystack keeps a separate webhook per mode so going live with the test one credits nothing.
- Added the reference facts for choosing between them: where each works, how it settles, and the vendor's published pricing.
Test connection
- Added a credential check against the vendor, before anything is saved. Paste a value, confirm it works, then write it to your configuration — or leave the fields blank to test what is already configured.
- Added live checks for ten gateways: Stripe, PayPal, Paystack, Mollie, Adyen, Klarna, Authorize.Net, dLocal, eWAY and TransFi. Every one is read-only — nothing is charged, and no payment, session or order is created.
- Added an honest format check for the six that expose no endpoint that authenticates without creating something: 2Checkout, iPay88, PayFast, Paysafe, Paytm and PayU. It says so rather than implying a check it did not perform, and it still catches the two mistakes that actually happen — a value left empty, and a value pasted with its surrounding quotes.
- Added a third answer: could not tell. A rejection is reported only when the vendor explicitly refused the credential. A timeout, an outage or an unexpected response says so instead — because "your key is wrong" sends an operator to regenerate a key, and on Authorize.Net and dLocal regenerating one breaks production immediately.
- Added a ready-to-paste configuration snippet once a check passes.
Gateway details and fees
- Added a currency picker bounded by the currencies your platform actually has enabled, replacing a grid of 160 buttons covering every currency in the world next to a warning that picking a wrong one "may cause issues".
- Added a flag on any currency already stored on a gateway that is not enabled on your platform — a deposit in one fails when the wallet is looked up. On the installation this was built against, one gateway carried twenty-two of them.
- Added a live worked example under the fee editor: for an amount and a currency you choose, what the customer pays, what each fee takes and what reaches the wallet. The two fees compose in an order that is not obvious, and it was stated nowhere.
- Added a warning when a fee is larger than the deposit it is charged on — expressible in the form, and it would credit a negative amount.
- Changed the status switch to save on its own, immediately, separate from the rest of the form. Turning a misbehaving gateway off should not require committing whatever else was half-edited.
- Changed saving to stay on the page. It previously returned to the list on every save, which is wrong for a screen whose work — paste a key, test it, paste a webhook URL — is iterative.
MSG91 for one-time codes
- Added MSG91 as an alternative deliverer of login and two-factor codes, phone verification, withdrawal codes and password-change codes.
- Added a single setting,
SMS_OTP_PROVIDER, which takestwilio(the default) ormsg91. - Added everything needed in two values — an authkey and one OTP template id. A sender id is optional; MSG91 falls back to its shared one, and your own is only needed past roughly 2,000 messages a month in a country, or for branding.
- Applies to one-time codes only. Notification SMS continues to be sent by Twilio — see the note under Changed.
SMS Providers screen
At Admin → System → SMS Providers.
- Added a routing panel showing which provider carries codes, which carries everything else, and whether each can currently send.
- Added a side-by-side comparison, led by the column that matters most: whether a provider reports a failure or hides it.
- Added a per-provider list of the environment variables it needs, ticked when present, with optional ones shown separately so they do not read as outstanding work.
- Added a setup guide per provider, with direct links to sign up and to the page holding the key.
- Added Test credentials — checks a key against the provider before it is saved anywhere, and sends no message. Paste a key, confirm it works, then write it to your configuration.
- Added a ready-to-paste configuration snippet once a key passes.
Provider health that reflects whether messages will actually arrive
- Added a credential check that asks the provider directly instead of sending a message.
- Added a balance check for MSG91. With no credit, MSG91 accepts every message, returns a success response and a reference number, and queues nothing — codes simply never arrive and nothing anywhere reports a problem. Health now says so in plain terms.
- Added a specific message for the most common MSG91 mistake: the OTP Widget snippet on their site contains a
tokenAuthvalue that looks almost identical to an authkey but is a public browser token. Pasted into the server setting it is rejected — invisibly, because sends still report success.
Placeholders that measure themselves
- Added
LoadableandSkeletonText. A pending value renders its placeholder string in place, invisibly, at the real font size, and the shimmer is painted over that box — so the height is produced by the same text layout that will run on the real value, rather than by a number somebody typed. Change a figure fromtext-2xltotext-3xland the placeholder follows on its own. - Added the same treatment to
StatsCard, which 104 screens use. Its previous placeholder wash-8against a 30px figure, and it hid the delta row entirely while loading — in a grid where every card matches the tallest, that resized the whole row.
A layout-shift measurement harness
- Added
npm run skeleton:cls, which drives a real browser: it freezes the API, photographs the pending layout, releases, photographs again, and reports every element whose text is identical in both passes but whose box moved. Static chrome has no business moving, so each hit names the offending element in its own words with a pixel delta — which a single Core Web Vitals score cannot do. - Added CLS attribution per frame, naming the DOM node responsible. The browser scores a frame on the union of everything that moved in it, so a number can rise while the layout genuinely improves; without attribution the two are indistinguishable.
- Added a low-overlap guard. A page that replaces itself entirely has nothing left to compare, so it reports "0 elements moved" — the same output as a page that never moved. That false pass is now flagged and counted as failing.
- Added console and hydration error capture alongside the layout numbers, because a server/client mismatch does not throw — React repairs it silently and leaves a page that shifts for no visible reason.
- Added
--api-target, which forwards API traffic so the harness can measure a production build rather than the dev server. The rewrite that proxies the API is dev-only, so without it every request resolved to an HTML page and the first production run produced a full set of numbers taken from pages whose data never arrived. - Added
--concurrencyand route discovery from the app directory. A full sweep of 353 routes runs in minutes instead of hours. Dynamic routes are skipped rather than guessed at — a made-up id 404s and measures a flawless zero, which is the most dangerous possible result. - Added
npm run skeleton:auth, which solves the login proof-of-work once so authenticated screens can be measured. It refuses to save a session it cannot confirm is an admin one, because an under-privileged session renders a plausible empty page and measures as perfect.
A source scanner for the same fault
- Added
npm run skeleton:debtandskeleton:check, which parse the code and report loading states that cannot be right: whole-page swaps, shapeless spinners, content withheld until loaded, and branches whose two halves are differently shaped. It is a ratchet against a committed baseline, in the same shape as the existing design-system check. - Added a
mount-gaterule, weighted highest of all. It finds components that withhold themselves until the browser has mounted — the fault above, where the server sends a different page and the whole layout arrives at hydration. This class was invisible to every other rule; all four instances were found by measuring in a browser first. - Added
plans/SKELETONS.md, which records the contract, the two failure modes and their diagnostic signatures — including that a shift landing before the data does is never a skeleton problem.
Permission tooling
- Added
pnpm check:permission, which reports drift between the route manifest and the files that define it without writing anything. - Added a BLOCKED classification to it. Ordinary drift is safe to regenerate; a route naming a permission no migration grants to any role is not, because adding it makes the screen Super-Admin-only for good. The check previously reported both identically and told you to regenerate — pointing directly at the cliff.
Translations for the new admin dashboards
- Added 886 English strings across seventeen addon admin areas — the largest being forex trading (100), the Hummingbot admin (88), the ecosystem admin (74), the binary AI engine (69) and staking (66), with the rest spread over affiliate, copy trading, e-commerce, FAQ, forex, futures, the merchant gateway, ICO, NFT, P2P, trading bots and the market maker.
- Added every one of them to all 90 language files, so no dashboard falls back to printing a raw key or logging a missing-translation warning on each page load.
Filtering a table by more than one value
- Added multi-value filtering to admin tables. Ticking several options in a filter now asks for any of them, instead of failing the request — see Fixed.
- Added the ability for a column to offer a multi-value filter while still showing a single badge in its cell. The support queue is the first to use it: its status filter opens with Pending and Open ticked, which is what "waiting on an agent" means.
Messages that can count
- Added exact-count and per-language plural forms to the translation layer. A message can now say "No owners" at zero, "1 owner" and "12 owners", from a single entry.
- Added form selection driven by the rules of the active language, rather than by comparing the number against one and two. Languages with more than two plural forms — Russian, Polish, Arabic — can now reach them.
Descriptions on menu items
At Admin → Menus.
- Added a description on any menu item — the second line the site header's dropdown prints under the label. It can be set when you add an item, and on an item the platform shipped; emptying the box puts the shipped text back rather than leaving a blank line.
- Added the description to the row itself, under the label, so the menu list shows what each item says rather than only that it says something.
Coin logos on the wallets page
The switch is at Admin → System → Settings → Wallet → Display.
- Added a Currency Icons switch. With it on, the wallets list draws each currency's coin logo beside its code, and so do the panel that opens from a row and the wallet's own page. It ships on; turning it off returns the page to codes alone.
- Added a fallback that ends in something readable rather than in a broken image. An ecosystem token uses the icon you uploaded for it; if that file has gone missing — and for every other wallet — the platform falls back to the bundled icon set. A symbol in neither is drawn exactly as it was before this release: the wallets list shows its code alone, and the wallet's own page keeps the lettered disc that page has always used. There is no state in which a row loses its identity, and none in which the same code is printed twice.
Every image picker now says what it accepts
- Added a line under every image field in the admin stating the size an upload is reduced to — 1024 × 728 by default, keeping the picture's proportions — and that it is stored as WebP. Any dimensions are accepted; that box is what is kept. The field previously said nothing at all, so "what size should this be?" had no answer anywhere in the product. For a blog category, whose image is drawn as a wide banner behind the name, a landscape picture of about 1024 × 512 fills it exactly.
Table filters can offer a list that comes from your data
- Added the ability for a filter to fill its own dropdown from live data. A column showing something that lives in another table — a category, a plan, an author — never had a fixed list of choices to offer, so its filter opened empty. It reads as "there are none", which on the blog article list is exactly the complaint that started this work.
Changed
A wallet balance is settled money
- Changed the wallet total, the allocation panel and the active-wallet count to exclude deposits awaiting approval. See Upgrade Notes.
- Changed the allocation panel to read the converted figure the server already provides rather than the raw per-currency sum sitting beside it. The panel prints its figures with a dollar sign and divides them to get share percentages, so the wrong field made 40,000 NGN read as $39.96K and gave Fiat 100% of a portfolio that was mostly crypto.
Deposits and withdrawals can only be settled where the money moves
- Changed the generic transaction editor to refuse settling a deposit or a withdrawal, naming the screen that can. See Upgrade Notes.
Payment webhooks are exempt from geographic restrictions
- Changed the geo gate to let provider webhooks through. These are server-to-server callbacks that settle money already taken from a customer, and the caller's location is the payment processor's data centre, not the customer's.
Rejected webhooks return a real error in development
- Changed the CORS layer to leave provider webhooks alone. Writing those headers fixed the response status at 200, so a webhook the platform rejected looked accepted and the provider stopped retrying.
A dead setting was removed rather than wired up
- Changed the Super Admin protected list to drop
depositApproval. It appeared nowhere else: no settings screen, no default, and nothing read it. Protecting a setting that does not exist implies a control is in force when none is.
Editing a payment gateway requires the gateway permission
- Changed the gateway edit screen to require
edit.deposit.gateway, which the API behind it already enforced. The page itself fell through to the general admin permission, so it was the looser of the two.
SMS delivery runs through one provider layer
- Changed every SMS in the platform to go through a single provider layer. It was previously written directly into fourteen places, so adding any alternative was impossible.
- Changed four separate checks of "can this installation send an SMS" — the login screens, the withdrawal policy, the notification service and the health check — onto one answer. Each read a different subset of the settings, so they could disagree, and did.
Only codes are switchable
- Changed the provider choice to cover one-time codes only. Notification SMS is always sent by Twilio.
The support queue opens on the work that is waiting
- Changed the queue's default view from tickets marked Pending to every ticket waiting on an agent, oldest first. See Upgrade Notes.
- Changed ticket status to mean one thing — whose turn it is. A customer's message now always puts the ticket back in front of an agent, whether it arrives through live chat or through a raised ticket; an agent's reply always hands it back to the customer.
- Changed the Pending tile to Awaiting Response, counting every ticket waiting on an agent rather than only those nobody had replied to yet. It read 0 on a desk with unanswered conversations in it, which is the good news no matter how bad things are; the exact Pending count is now the caption underneath.
- Changed Unassigned to count only tickets an agent can still pick up. A resolved ticket that was closed without ever being assigned is not work waiting for anybody.
Response times are stated in units a person reads
- Changed the Avg Response figure from raw minutes to a duration. The desk this was reported from displayed 55263m; the same figure now reads 38d.
- Changed the caption under it from a fixed "Lightning fast" to one derived from the value, so it can say "Slower than a day" when that is what the number says.
Corner radii brought back into line
- Changed nine card surfaces that had drifted to a corner radius the rest of the platform does not use — in the P2P trade loading screen, the copy-trading dashboard, the import preview dialog and the notification list. The P2P loading screen was the visible one: its placeholder cards had rounder corners than the cards they stand in for, so the corners changed shape as the page finished loading.
Clearer configuration errors
- Changed "Service SID is not set" — a message naming a setting that no longer did anything — to name the settings actually missing for the provider in use.
- Changed the SMS section of the configuration file to the four values that matter, with the rest removed.
Loading is now a state of the page, not a replacement for it
- Changed roughly 390 files so that a screen keeps its own layout while it waits. Static content — headings, labels, icons, borders, tab bars, column headers, grid containers — renders immediately and never moves; only values carry placeholders.
- Changed the
DataTable, which 96 screens use, so its pending rows match its real ones. Its placeholder chose a shape from the column name while the real cell chose from the column renderer, so a column calledstatuswith plain text drew a pill and one carrying a badge drew a text bar. Text cells also reserved 16px inside a 20px row — four pixels short on every row, a hundred on a 25-row page. - Changed the sixteen settings screens to share one pending state driven by the same configuration object the real page uses, replacing sixteen hand-written copies that had each drifted. 1,652 lines became 326.
- Changed the site footer, the licence gate, the gateway settings provider and the homepage so they render on the server. The footer had been absent from the server HTML of every page in the product.
- Changed
pnpm bundleto rebuild the route permission manifest as well as the permission registry. Only the second ran, which is why the two drifted. - Changed the two permission build tools to share one parser. They each carried their own regular expression, and neither understood one of the three shapes a permission file is written in — so two screens declared a gate that was invisible to the whole toolchain.
Chart cards, legends and tiles wait without emptying themselves
- Changed a chart card's pending state from a spinner in the middle of an empty rectangle to a placeholder of the plot itself, and its footer rule now draws while it waits. A loading chart card was 43px shorter than the card it was about to become, and in a grid row measured by its tallest member that moved every card beside it, the figures above and everything below.
- Changed the chart legend, which previously drew nothing at all until the series arrived and then appeared from nowhere. A six-segment donut card grew by 81px at the moment its data landed; it now reserves its rows and only the names, values and shares wait.
- Changed the small metric tiles and money figures to carry a pending value inside the real layout. On a money figure the currency code still renders — the page knows which wallet it is showing before it knows the balance — so the amount beside it no longer slides sideways when it arrives.
- Changed the activity timeline to keep its card, its connector rail and its footer count while the trail loads, instead of being swapped out for three flat bars two-thirds the height of a real row. It also no longer says "No activity recorded yet" on a record whose history is still on the wire.
The data table stops assembling itself in front of you
- Changed the analytics tab, the toolbar, the pagination bar, the overview hero, the empty state, the no-access notice and the analytics error notice to render at rest. Each carried a staggered entrance: the analytics tab alone ran a lead delay, then a delay per section, then one per tile and another per chart, so the last chart on a multi-section page started a 0.7-second slide about a second after the grid holding it had already been laid out. The toolbar buttons arrived one at a time after the table below them had drawn; the pagination bar, being last, settled last.
The wallets list leads with the currency, not the wallet type
- Changed the wallets list to put Currency in the leftmost column, where Type used to sit. A wallets list is a list of currencies — the type qualifies a row rather than being what the row is — and a left edge reading FIAT, SPOT, SPOT, ECO sorts the eye by the least distinguishing thing on the line. Nothing is added or removed; the same columns are in the same order otherwise.
A coin logo no longer sits on a coloured wash
- Changed a currency mark showing an icon to draw it on a neutral plate rather than on the wallet-type gradient. Coin artwork is transparent around the disc, so the gradient showed through as a coloured halo and the mark read as a status pill rather than as the coin. The gradient is still what a currency with no icon gets, where it is the background the letters sit on.
The menu editor's rename control is now an edit control
- Changed the pencil on each row of Admin → Menus from a single label box to a small panel holding the label and the description, and its name from Rename to Edit so it says what it opens.
- Changed the Add inside list to indent each option by how deep it sits, now that it mixes menus the platform ships with sections you have built inside them. Two entries can carry the same name, and a flat list of quoted names cannot say which one you are pointing at.
Image pickers no longer offer a format the platform rejects
- Changed every image picker in the admin to offer JPG, PNG, GIF and WebP only. SVG was listed, and the upload endpoint has never accepted it — deliberately, because an SVG served from a public folder can carry script. So the picker invited a file that every save then refused.
A status write that names a column that does not exist is now refused
- Changed the shared status helper to check the field against the table before writing. It previously handed the column name straight to the database, which discards anything it does not recognise — so the write vanished and the caller was told it succeeded. All ninety-one places that use it were audited against their tables; the six that were wrong are listed under Fixed, and every other one names a real column, so nothing that worked changes.
Categories are listed in a predictable order
- Changed the blog category lists — in the article editor and on the public category page — to come back in alphabetical order. They were returned in whatever order the database found them, which for these records is effectively random and reshuffles as categories are added, so the same dropdown was in a different order each time it opened.
Fixed
A blog category you created could never be used
- Fixed the article editor filling its Category list from the public category list, which by design contains only categories that already hold a published article. A category you have just created holds none — and it cannot come to hold one, because an article cannot be filed into a category the editor does not offer. A new category was unusable forever, and since a category is required, the article could not be written at all. On the installation this was reported from, sixteen categories existed and four were selectable.
- Fixed that list being silently empty when it could not be loaded at all. A category list with nothing in it and a request that was refused looked identical — a menu that opens onto nothing. The field now says which of the two it is.
A blog category could not be saved without an image
- Fixed creating or editing a category failing with "image: Image must be a valid URL" when the image was left empty. An untouched image field submits an empty value, and the check that a stored image looks like a real file rejects an empty one — so the one field an operator had deliberately left blank rejected the whole record. Leaving it blank now stores no image, which is what it always meant.
Ordinary photographs were too large to upload
- Fixed every upload being capped at roughly 3.75 MB while the picker accepted 5 MB and the platform's own message said 10 MB. A file is encoded for transport and grows by about a third on the way, and the ceiling was applied to the encoded size — so both stated limits were unreachable and a normal phone photo was refused. All three upload paths now carry a ceiling sized for the encoded form, and the limit an operator is shown is the one that applies.
A failed image upload cancelled the save and said nothing
- Fixed an upload the server refused aborting the whole save with nothing on the screen: no message on the image field, no change of state, the form simply sitting there. Every image fault above arrived this way, which is why they read as "it doesn't work" rather than as anything an operator could act on. The reason is now shown, on the field that caused it.
- Fixed per-field errors from the API never reaching a form in the admin at all. The code that displays them was guarded by a test nothing in the platform can satisfy, so it had never run once. Every admin form that submits to a validating endpoint gains this, not only this one.
Referral rewards marked as claimed were not marked as claimed
- Fixed the endpoint that marks an affiliate referral reward claimed writing to a column named status, which that table does not have. Nothing was written, and it answered "updated successfully" — so a reward reported as paid stayed in awaiting payout, and the admin audit trail recorded a change that had not happened. See Upgrade Notes.
Notification channel switches could not be changed through the API
- Fixed the endpoint that turns a notification template's email, SMS and push delivery on or off naming a table that does not exist, so it failed every time it was called. The template screen itself was unaffected — it saves the whole template rather than using this endpoint.
Blog category and tag on/off endpoints reported success and changed nothing
- Fixed by removing them. Blog categories and tags have never had an on/off setting, and these four endpoints wrote one anyway — discarded by the database, reported as successful, recorded in the audit trail. Nothing in the product called them. See Upgrade Notes.
Newly uploaded images did not appear until the site was restarted
- Fixed uploaded files being unreachable on a live server until the next restart, on any installation whose web server had not been given a rule for them by hand. The site lists its public files once at startup, so a file written afterwards is not in that list — leaving older images working and every new one broken, which is the most literal reading of "all I try to upload doesn't show after". See Upgrade Notes.
The article list could not be filtered by category
- Fixed the Category filter on Admin → Blog → Posts being permanently empty. The column knew where to fetch its choices and the filter panel never asked — a filter could only show a list written into the page, which a category list is not. An empty dropdown reads as "no categories exist", which is the sentence the original report opens with.
NFT creators could only choose from ten categories
- Fixed the Category list on the NFT collection and mint forms coming from a paginated list and keeping only its first page. The eleventh category onward could not be selected, with nothing to indicate more existed.
- Fixed the collection edit form showing no category at all for a collection filed under one that has since been retired. It now shows it, marked (retired), so the record reads as what it is rather than as unfiled.
An unapproved deposit was counted as balance
- Fixed the wallet totals adding pending deposits to the balance. A customer submitting a manual bank transfer saw the money immediately, before any operator had confirmed it and while the wallet itself held nothing — and rejecting the deposit removed it again, which reads as the platform taking funds. See Upgrade Notes.
- Fixed the daily snapshot the 24-hour change is measured against being written by three places that each meant something different by it, so the difference between two definitions was charged to the customer as a gain or a loss they never made. An untouched account could show +294%.
Naira was counted as dollars on the wallets page
- Fixed the allocation panel printing the raw sum of every wallet's native amount with a dollar sign in front of it. 40,000 NGN read as $39.96K instead of about $29, and the share percentages were computed from the same figure, so a mostly-crypto portfolio was reported as 100% fiat. The converted figure was already being sent; the panel was reading the one next to it.
An operator could not approve a bank transfer
- Fixed the approve action demanding a Reference ID on a deposit type that has none, from a form that never marked the field required. The refusal read "Reference Id is required" and the only way past it was to invent a value. Approving the same deposit from the queue worked, so the two screens disagreed about whether the same decision was allowed. See Upgrade Notes.
A fee larger than the deposit credited the customer nothing
- Fixed approval treating a fee at or above the deposit amount as a deposit of zero: the customer was credited nothing, the full fee was booked as platform profit, and the deposit was marked COMPLETED. The guard that produced this looked like it was preventing a negative credit. See Upgrade Notes.
One bank transfer appeared twice in the customer's history
- Fixed an approved deposit showing as two rows. Crediting the wallet writes its own ledger entry beside the original request, both carrying the same amount and wallet, so a single transfer was listed twice and counted twice by the page's own totals. The history already hid the equivalent duplicates on the ecosystem side; approved deposits are now hidden the same way.
The deposit decision email said PENDING and quoted the wrong balance
- Fixed the approval and rejection notice telling the customer their transaction was PENDING — the one status the email is never sent for — because the message was composed before the new status was applied.
- Fixed the same email quoting the balance from before the credit as the customer's new balance, and quoting it as a raw eighteen-decimal figure.
A deposit settled from the wrong screen was paid to nobody
- Fixed the generic transaction editor being able to mark a pending deposit COMPLETED without moving any money — and, by doing so, permanently blocking the only route that could have paid the customer, which refuses any deposit that is no longer pending. See Upgrade Notes.
- Fixed that same editor blanking a record's amount, fee, description and reference when edited without resupplying all four.
dLocal deposits could never be credited
- Fixed the deposit being created under one reference and then immediately overwritten with another, while every reader looked it up by the first. Every dLocal notification answered "transaction not found", so no dLocal deposit was ever credited by the notification path, and the verification step searched for the reference in the wrong field entirely.
- Fixed a refund's ledger entry reusing the reference the original credit had already taken. References are unique, so the reversal failed on a database constraint and was dropped — the customer kept money the card issuer had already pulled back.
- Fixed three dLocal statuses — chargeback, partial refund, authorised — being recorded verbatim when the platform's records reject those words. The write failed validation, which surfaced as a server error, and the step that write was carrying never ran: a charged-back deposit stayed in the customer's wallet while dLocal retried into the same wall.
- Fixed refunds and chargebacks taking back the full deposit when only the deposit minus the fee was ever credited, so every reversal left the customer paying the fee twice while the platform kept a fee on money it was returning. A full refund now returns the platform fee as well.
- Fixed the COMPLETED status being recorded before the credit was attempted, so a credit that failed left the record final, the retry rejected and no pending deposit for anyone to chase. See Upgrade Notes.
Money figures added currencies together and called the total dollars
- Fixed about thirty figures across the product that summed amounts in different currencies and printed the result as USD. Gateway merchant dashboards and payouts, the admin merchant page, the NFT marketplace and its two analytics pages, trading bot profit, copy-trading leader and follower statistics, forex accounts and overview, ICO portfolios, affiliate rewards, staking, investments and the public markets page. See Upgrade Notes.
- Fixed the NFT marketplace pricing a mixed-currency total at the Ethereum rate — about three thousand times over for a collection trading in stablecoins — and asserting the result was dollars. The same page also decided whether a currency was fiat from a hardcoded list of five codes, so every other national currency was priced off a cryptocurrency ticker that does not exist and contributed nothing.
- Fixed NFT contract balances being valued from a hardcoded 2022 exchange rate table, with anything missing from it treated as 1:1 with the dollar.
- Fixed an exchange rate of exactly zero being treated as a real price in four places. A currency that exists but has never been synced has no rate, so genuine holdings were valued at $0.00 and — because the code considered them priced — were never reported as excluded.
- Fixed a trading bot dashboard adding a simulated paper bot's profit to a live bot's real profit whenever they shared a quote asset. A live position up 20 USDT beside a paper position "up" 480 read as 500.
- Fixed copy-trading leader statistics — the figures on the public leaderboard — summing profit and volume across every quote asset a leader trades, and dividing one mixed total by another to get their return. A leader up 300 USDT and 0.4 ETH was listed at "$300.40", roughly $1,300 short and moving with the price of Ethereum. A leader quoted in Bitcoin had a real 0.004 BTC profit rounded to 0.00.
- Fixed copy-trading return-on-investment being a ratio of two different populations: the profit excluded currencies that could not be priced while the amount invested still counted them, so the figure read low with nothing on screen explaining why. It is now withheld rather than guessed when a currency cannot be priced.
- Fixed a fee on the gateway dashboard's recent payments list carrying a dollar sign that lived inside a translation string — so it was wrong in all 90 languages, and beside an amount on the same row that correctly named the payment's own currency.
- Fixed five merchant policy limits being displayed as dollar amounts. They are compared against a payment in whatever currency it arrives in, so a merchant taking naira was held roughly fifteen hundred times tighter than the "$5,000" the screen claimed.
Totals computed from one page of results
- Fixed the affiliate rewards page computing lifetime earnings, and the count of unclaimed rewards, from the rewards currently loaded. An affiliate with forty rewards was shown the total of ten, and the figure changed as they turned the page. The server now returns the real aggregate.
- Fixed the affiliate summary shimmering forever when its request failed. A skeleton says "still loading"; the page now says the figures are unavailable and offers to retry.
A retired forex plan zeroed a customer's portfolio
- Fixed the forex overview skipping any investment whose plan had been retired, because it read the currency from the plan and a retired one was not returned. A customer whose only plan had since been retired saw $0 invested and $0 profit with the investments still listed below.
- Fixed forex account balances being formatted to two decimal places regardless of currency, so a real 0.00412 BTC balance rendered as 0.00 — a funded account reading as empty.
A rejected withdrawal refunded only once per wallet
- Fixed refunds being keyed to the wallet instead of the withdrawal. The duplicate check is global, so the second and every later rejection on a wallet was treated as a repeat and skipped — after the withdrawal had been marked rejected. The customer saw "rejected" and never got their money, and nothing errored. See Upgrade Notes.
Paystack could never credit a deposit
- Fixed Paystack reusing the provider's reference on both the pending deposit and the ledger entry that credits it. References are unique, so crediting failed on a database constraint, surfaced as a server error, and Paystack retried indefinitely while the customer was never credited.
Approve did not work on fiat withdrawals
- Fixed the approve action assuming every withdrawal was a spot withdrawal. It looked for blockchain fields and a spot wallet, neither of which a fiat withdrawal has, so Approve failed on fiat rows from both the withdrawal page and the bulk queue. See Upgrade Notes.
Refusing a withdrawal reported a server error
- Fixed the approve action replacing every refusal with a generic server error. "Not found", "not pending", "already being processed" and the ecosystem refusal all reached the client as a 500, which reads as "try again" — so an operator or a bulk run would retry a withdrawal refused for a permanent reason.
Non-English characters were corrupted in every request body
- Fixed request bodies being decoded in pieces and joined. A character split across two pieces — an accent, a currency symbol, any non-Latin script — was replaced with a placeholder. Invisible in English, and fatal for webhook signature checks, which compare against the exact bytes the provider sent.
Two gateways verified webhook signatures against the wrong data
- Fixed Paystack and dLocal checking signatures against a re-serialised copy of the request body rather than what arrived. By the time a webhook is handled the body has been through validation, so the copy is not what the provider signed — those checks passed by luck.
- Fixed dLocal comparing signatures character by character, which leaks how much of a forged signature was correct. It now uses a constant-time comparison, as Paystack already did.
A failed payment could show as completed
- Fixed the deposit screen treating a verification response as success whenever it carried a success flag — including
{ success: true, status: "FAILED" }, which is exactly what a verification returns when it has successfully determined the payment failed. That produced a "payment completed" message and credited nothing. Affected all six redirect-based gateways. - Fixed an unrecognised payment status counting as success. It no longer does.
Six gateways sent paying customers to a page that does not exist
- Fixed the default return, callback, success, failure and cancel URLs on Mollie, PayFast, Paysafe, Paystack, Paytm and PayU — thirteen in total — pointing at
/user/wallet/deposit/<gateway>/verify. No such route has ever existed. Any installation that did not set the override itself showed a customer a not-found page as the final step of handing over money. See Upgrade Notes.
A gateway could be renamed out of its own integration
- Fixed the gateway update API accepting
alias, which is the key mapping a gateway to its payment handler and carries a uniqueness constraint. A rename saved without error and then failed every payment through that gateway — no error at save time, no error when the deposit form rendered, and a failure only the customer ever saw. See Upgrade Notes.
Saving a gateway could hide it from the deposit form
- Fixed the gateway editor checking the title and description before saving but not the currency list. A gateway saved with no currencies disappears from the deposit form entirely, which was reachable and reported nothing.
Gateway logos were invisible in dark mode
- Fixed gateway logos being drawn on a dark tile. The bundled artwork is dark ink made for a light background — Authorize.Net is navy type, TransFi is black type — so several gateways rendered as an empty box in the dark theme. They now sit on a fixed light plate, which is what brand artwork of a polarity you do not control requires.
The checkout design preview would not build
- Fixed every sample state in the checkout design preview missing the flag that separates "signed out" from "not asked yet". Beyond breaking the build, it meant the signed-out design could not be previewed at all — without that flag each layout renders its loading treatment instead.
Markdown tables ignored their column alignment
- Fixed table cell alignment being assembled at runtime, which the stylesheet compiler cannot see. Every markdown table rendered left-aligned regardless of its
:---:markers — in patch notes, documentation and anywhere else markdown is displayed.
A coin logo that failed to load left an empty coloured circle
- Fixed a currency icon that cannot be loaded being hidden, which left the circle it was drawn in painted and completely blank — no logo, no letters, nothing saying which currency it stood for. It now falls back to the three-letter mark. This is reachable wherever an icon is stored against a currency whose file is no longer there, which is the deposit and withdrawal currency pickers: both have always drawn their icons from stored data.
The SMS Providers menu entry had no translation
- Fixed the navigation entry for Admin → System → SMS Providers having no translated title or description, so it fell back and logged a missing-translation warning on every admin page load.
A crypto gateway could appear as a fiat deposit option
- Fixed the deposit list filtering gateways on status and currency but not on type, so a gateway marked as crypto that happened to list a fiat currency was offered as a fiat deposit.
Payment gateway labels were missing from translations
- Fixed the translation extractor stopping at a re-export file, which made everything behind it invisible.
Pay with TransFiis now translated in all 90 languages, each phrased from that language's own wording rather than left in English.
Debug output shipped in the gateway editor
- Fixed the payment gateway editor printing the full payload to the browser console on every save.
A verification code was sent before it was saved
- Fixed phone verification texting the code and only then storing it. If storing failed, the customer received a code the platform had no record of and could not verify no matter what they typed, with nothing on screen to explain it. The code is now stored first; a delivery failure leaves an unused code and a working retry.
SMS two-factor was switched off by a setting for a service the platform does not use
- Fixed SMS availability being decided by
APP_TWILIO_VERIFY_SERVICE_SID. Twilio Verify is not used anywhere — the platform generates and checks its own codes — so installations with working SMS credentials could find the method unavailable, including for approving withdrawals, with no indication why. See Upgrade Notes.
The health check spent money and texted a real phone
- Fixed the SMS health check sending an actual message to the support number on every run. It now validates credentials with the provider directly.
- Fixed its failure messages naming Twilio regardless of which provider was in use.
The interface stated things that were not true while it waited
Removing a loading spinner exposes everything it was covering. Each of these ran against data that had not arrived and asserted something false — and every one of them was reachable before this release, hidden behind the spinner.
- Fixed both KYC wizards telling every visitor to complete verification. The check read "not allowed" from a gate that had merely not decided yet.
- Fixed a funded customer being shown "No Wallet Found — you need to deposit $0.00" at checkout. The wallet request is gated on knowing who the customer is, and that was false for a signed-in customer arriving from an email link or merchant redirect — so their balances were never fetched at all.
- Fixed the enrolment page for copy trading announcing "Copy trading is currently unavailable on this platform".
- Fixed a verified merchant being told verification was required, on every load.
- Fixed four notification channels reporting Inactive and Redis reporting Disconnected in red, for the length of every thirty-second poll.
- Fixed confident zeroes on money: wallet balances, staking totals, seller revenue ("Total Revenue $0.00" to a seller who had earned), and P&L painted green with an up-arrow at
$0.00. - Fixed a live offering labelled Completed, a published article labelled DRAFT, a live page labelled Active, an order row reading Sell before the side was known, and a running market maker announced as halted.
- Fixed empty states shown to accounts that had data — "No strategies yet" to creators with a dozen, "No evidence submitted" about a party to a dispute, "you have no collections" to creators with several.
- Fixed a
$undefinedprice where "Free" belonged, an article reported as "0 words, 1 min", dates rendering as "Invalid Date" and "1 Jan 1970 · 20881d ago". - Fixed a 404'd token offering sitting on a loading placeholder forever, because the loading check was evaluated before the error check.
Faults that were not about layout at all
- Fixed the blog post editor being able to overwrite a live article with an empty one. The submit handler had only ever been unreachable because a spinner was in the way; nothing else prevented it.
- Fixed a creator profile being able to save blank values over a real profile, and report it as a creation.
- Fixed the binary order panel crashing to its error boundary — "Cannot read properties of undefined" — when a stored order type no longer exists. The lookup is typed as total, so the compiler considers every read safe, but the key comes from the browser's own storage.
- Fixed an unsubscribe link from a valid email telling every recipient their link was invalid, plus a race where preference toggles were overwritten by the data arriving behind them.
- Fixed the first request to the content API after a restart returning a 500. Two modules import each other and a cache warm-up ran before the value it needed was initialised, so whichever loaded first decided the outcome.
- Fixed a decorative placeholder that picked its width with
Math.random()during render, producing different markup on the server and the browser.
Live chats never reached the support queue
- Fixed the support queue opening filtered to Pending, a status a ticket leaves as soon as its customer says anything. A customer writing in through live chat was counted by the header and absent from the table, which showed "No data available" — so there was no row to open, and no agent could be put on it. See Upgrade Notes.
- Fixed a ticket being marked Replied both when an agent answered and when the customer answered. One status meant two opposite things, so the desk could not tell who was waiting, and a customer's follow-up left the ticket presenting as answered.
- Fixed the ticket age column's overdue clock stopping when a customer replied. Because a follow-up counted as "answered", a ticket could go unread indefinitely without ever being flagged as late.
Multi-value table filters failed the request
- Fixed ticking more than one option in a table filter producing a malformed database query that threw. The multi-value filter has always sent an "any of these" instruction and the query layer had no entry for it, so the value it built was addressed by nothing at all. Affects the e-commerce order tables and the wishlist table, which filter by product.
- Fixed any filter instruction the query layer does not recognise being passed to the database malformed. It now falls back to an exact match, so a filter the platform has not been taught yet narrows a list instead of taking the page down.
- Fixed unticking a multi-value filter down to nothing being sent as "match none of the possible values", which blanks the table. No selection now means no filter.
A table's own filter could hide every row without showing itself
- Fixed a table's built-in filter keeping its selection privately instead of reading the table's actual state. A screen that opens with a filter already applied therefore showed rows excluded, an empty placeholder where the filter should be, and nothing to clear — the operator had an empty table, no explanation and no way out. This is half of why the support queue's blank table was unresolvable from the screen.
A customer could post in their own transcript as the support agent
- Fixed the live chat message route trusting the sender the browser claimed to be. A customer could post a message into their own conversation rendered as an agent reply — with the agent's plate, in their own widget and in the operator's thread. The route is the ticket owner's own and can only ever write a customer message; agent replies have their own staff-permissioned route, which was always correct.
First response time was never recorded if the ticket was assigned first
- Fixed "is this the first agent reply" being decided by whether the ticket was still unassigned rather than by whether an agent had already spoken. On a desk that assigns work before answering it — the workflow the queue's own Assign to me action is for — no ticket ever recorded a first-response time, and the headline average was taken over the exceptions. See Upgrade Notes.
- Fixed a reply inside the same minute scoring zero and then being discarded as "not measured", in the stored value and in every average and per-agent chart built on it. The fastest answers on the desk were the ones that never counted.
- Fixed a desk with no recorded responses at all reporting an average of 0, which reads as instantaneous. It now says plainly that nothing has been measured.
The support desk counted tickets that were not tickets
- Fixed the header counting a population the table below it could not show. Tickets raised by the admin who was looking were counted but never listed, so the total could exceed the rows that existed — and the operator best placed to notice a stuck ticket was the one person hidden from it.
- Fixed opening the live chat bubble creating a ticket that counted as Total, as Pending and as Unassigned before the customer had typed anything. An empty session is not work; the session is still created, it simply does not appear as a ticket until there is a message in it.
Chart dates could name the wrong day, and at a year boundary the wrong year
- Fixed dated axis and tooltip labels being rendered in the reader's own timezone when the underlying bucket had been cut in UTC and sent as an exact instant. Anyone behind UTC saw every daily bucket labelled a day early, on every chart in the admin analytics tabs and on the new dashboards, with the tooltip repeating the same wrong day so there was nothing to contradict it. See Upgrade Notes.
Count-aware messages showed the reader their own formatting
- Fixed the plural parser stopping at the first closing brace, which is the one closing the first form. A message written with a singular and a plural form matched only up to the singular, and the whole thing rendered as an empty string followed by the leftovers — the second half of the message, braces and all, printed on screen.
- Fixed the form being chosen by comparing the count against one and two. Those are English rules wearing international names, so the categories used by Russian, Polish and Arabic could never be selected and those languages could only ever fall through to the general plural.
A missing translation area failed the build rather than the page
- Fixed two translation areas — the Hummingbot admin and the binary AI engine admin — that existed in the message files but were absent from the list the build checks names against. Both had sat dormant for as long as no page asked for them; the first page that did failed to build, and the message pointed at the page rather than at the list.
A page with no translations above it took the whole route down
- Fixed the translation layer throwing when no provider was found above a component. The throw happened in the first component to ask for a string, so what the visitor lost was the entire page, not its labels — a recoverable condition turned into a server error on the whole route.
- Changed: keys render as themselves and a warning is logged, so a genuine authoring mistake is still visible while the page still renders.
A menu item you created could not be given anything to hold
- Fixed the Add inside list on Admin → Menus offering only sections that already had something in them. A top-level item you had just created had no children yet, so it was not a section, so it never appeared in its own parent list — the one thing an operator building a new navigation section wants to do second was the only thing the screen would not offer. Sub-items could go under the menus the platform ships and nowhere else.
- Changed: every item you have added is offered as somewhere to add to, empty or not. Create a top-level entry, then fill it.
Deleting a menu section you built left its contents behind
- Fixed deleting an item you added not deleting the items you had added inside it. Those were left in the stored menu, drawn nowhere and listed nowhere, while still counting as a customisation — so the menu stayed marked as customised with no row left that could clear it, and the whole-menu Reset was the only way out.
Removed
- Removed four endpoints that switched a blog category or tag on or off. Neither has ever had such a setting, so all four wrote a column that does not exist, were discarded by the database, and answered "updated successfully" anyway. Nothing in the product called them and no screen offers the switch. See Upgrade Notes.