• Knowledge base collections and articles now have nested URLs that follow the collection tree, e.g. /support/product/guides and /support/product/guides/article-slug
    • A collection slug only needs to be unique among its sibling collections, so collections under different parents can share a name and a slug
    • An article linked to several collections keeps the collection it is viewed from in its breadcrumbs and sidebar, while its canonical URL and hreflang links point to its primary collection
    • Old URLs keep working: /support/article/{slug} and the former flat collection URLs redirect (301) to the new nested ones
    • Renaming or moving a collection, or moving an article to another collection, records 301 redirects for every URL that changes when the slug-change redirects setting is on
    • The language switcher and menu items translate the whole nested path into the target language
    • The knowledge base pages now emit a canonical URL and hreflang alternates with each language's own slugs
    • The admin forms reject a collection slug already used at the same level, a root slug reserved for other support pages, a duplicate article slug, and a parent that would nest a collection under itself
    • Collection pickers in the admin show each collection's full path (e.g. "Product › Guides")
    • Any duplicate slugs already in the database are made unique during the update
    • The HelpDesk dashboard replaces its ticket table with four charts for a selectable period (7, 30, 90 days or 12 months): tickets opened vs. closed over time, tickets by status, tickets by department, and the customers who opened the most tickets
    • Every bar and slice of the dashboard charts opens the matching "All Tickets" list, already filtered by that status, department or customer
    • In the admin ticket lists the subject, the submitter name and the department are now real links: the subject opens the ticket, the name opens the "All Tickets" list filtered by that customer, the department opens it filtered by that department; each can be opened in a new tab from the right-click menu
    • When customer badges are enabled, the admin ticket lists gain a "Badge" filter (a tier, or "No badge"); the filter is hidden while the feature is off
    • Tickets from badge holders are now lifted to the top of the "Open" list only, where agents pick up new messages; every other ticket list keeps its plain chronological order
    • Closing a ticket now records who did it: the agent, the customer, or the auto-close scheduler
    • The admin ticket sidebar and the PDF export show a "Closed by" line naming the agent, or "Customer" / "System"
    • On the support portal the customer sees "Closed by: You" only for a ticket they closed themselves; a closure by an agent shows no name
    • Changing the status from the Replies header now refreshes the "Closed at" and "Closed by" sidebar lines without a reload
    • The reply form on the admin ticket page now keeps a draft per ticket and per agent: the message, the internal-note switch and the attached files survive a reload or a switch to another ticket
    • Drafts are auto-saved on a timer while the agent types, and a "Save as draft" button saves on demand; a "Discard draft" button drops it
    • New "Reply Drafts" settings under HelpDesk > Settings > Display & Behavior: enable/disable auto-save, the auto-save interval in seconds, and enable/disable manual save (which shows or hides the "Save as draft" button)
    • Files attached to a draft are kept with it and move to the reply when it is sent; the per-reply attachment limit counts both
    • Deleting a ticket also removes the drafts written on it, with their files
    • New "Reply Form" settings under HelpDesk > Settings > Display & Behavior: the reply form can sit before or after the ticket messages (default: after)
    • The admin reply editor now opens smaller (150 px by default) and grows with the reply being written up to a maximum height (500 px by default, 0 for none), after which the text scrolls; both heights are settings and only affect this editor
    • The headset button on /admin/licenses/products opens a "Support department" modal: pick the department the product is served by, or unlink it
    • "Create support department" moved inside that modal, and the department it creates is selected straight away
    • Re-pointing a product at another department moves its access-gating link too, so the old department stops being granted by a product no longer sold with it
    • The linked department keeps showing as a badge under the product name
    • New row button on the same page: pick the knowledge base collections buyers of that product get to read
    • The knowledge base collections a product unlocks are named under the product name, next to its support department
    • Fixed: the knowledge base admin asked for helpdesk.kb-collections.* / helpdesk.kb-articles.* while the add-on seeds the underscored names, so its screens were closed to every role but a super-admin. The code now uses the seeded names, and any dashed permission an install created by hand is renamed onto them with its role assignments
    • New "Customer Badges" screen: three tiers ship ready to use (Supporter, Pro, VIP) and can be renamed, re-coloured, re-priced or replaced
    • A tier is granted automatically once the customer's lifetime spend (shop orders, Envato purchases and Gumroad sales combined) reaches its amount, set per badge in HelpDesk settings
    • Tickets from badge holders are listed first in every agent queue, by the tier's priority weight
    • Agents can assign or clear a badge by hand from the ticket sidebar and the Customers list; a manual assignment is never undone by the automatic pass
    • Assigning a badge can email the customer to tell them their tickets are now answered first: switchable per assignment, with a per-badge default
    • A badge can shield its holder's license keys from the Licenses add-on's automatic rate-limit bans
    • Hovering a badge shows what it grants, and the lifetime spend behind it
    • New command helpdesk:badges:sync re-evaluates every customer; it also runs nightly while automatic assignment is on
    • The ticket status is now changed from a dropdown in the Replies card header instead of a sidebar card
    • Custom field values that are a URL or a domain name get copy and open-in-new-tab buttons
    • Hovering a message in a ticket reveals a reply button that jumps to the reply editor
    • New helpdesk_kb_collection_article pivot: an article can be listed under additional (secondary) collections on top of its primary collection_id
    • A collection page now lists articles whose primary collection is that collection OR that are linked to it through the pivot
    • Admin article form gains an 'Additional collections' multi-select to manage the secondary collections
    • Idempotent: the pivot table is only created when missing
    • New helpdesk attachments are now stored under a date-based layout: reply attachments in helpdesk/replies/{Y}/{m}/{d}/ and custom-field uploads in helpdesk/custom-fields/{Y}/{m}/{d}/, mirroring the media library
    • Migration relocates existing helpdesk attachment files (and their generated thumbnails) into the date-based layout, keeping the replies / custom-fields sub-category and deriving the date from each file's upload time
    • Rewrote media.file_path and helpdesk_custom_field_values.value to point at the new paths
    • Idempotent: files already sitting in a date sub-folder are left untouched
    • Added helpdesk_tickets.last_activity_at (nullable, indexed) timestamp
    • Backfilled last_activity_at from the latest reply (any author) per ticket
    • TicketService now stamps last_activity_at when a reply is created, edited, posted by the system, or ingested from inbound mail, never on reads or status/priority/department changes
    • The 'Updated At' column on the active/resolved/closed/all/no-response lists now reflects last_activity_at; the open list and dashboard keep the customer-reply-based 'Last Updated' column
    • getAllPaginated() sorts these lists by COALESCE(last_activity_at, created_at) DESC so the displayed column matches the row order
    • Added helpdesk_tickets.customer_last_replied_at (nullable, indexed) timestamp
    • Backfilled customer_last_replied_at from the latest non-admin, non-internal-note reply per ticket
    • TicketService::addReply() and TicketService::create() now stamp customer_last_replied_at when the reply author is a customer (non-admin, non-internal-note)
    • Replaced the 'Created' column with 'Updated' on the admin ticket dashboard and list views (uses customer_last_replied_at with created_at as fallback)
    • Updated getAllPaginated() ordering to sort by COALESCE(customer_last_replied_at, created_at) DESC so the displayed column matches the row order