fix(generator): show leftover download pages so they can be deleted

After the Support Generator migration, older pages stayed reachable at /d/:slug but vanished from the list, so the name stayed reserved. List leftover bundles (including when the ALPHA module is not installed) with revoke/delete only.

Refs #412

Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow)
This commit is contained in:
UNITRONIX
2026-09-11 12:24:24 +02:00
parent e765cdeef3
commit 28aed5efbf
33 changed files with 361 additions and 51 deletions
+3
View File
@@ -1,5 +1,8 @@
## [Unreleased]
### Fixed
- **Leftover generator download pages hidden (#412):** After the Support Generator migration, older download pages stayed in the database and remained reachable at `/d/:slug`, but they disappeared from the Generator list so they could not be deleted and the name stayed reserved. The panel now lists leftover pages (including when the ALPHA module is not installed) with revoke/delete only. Ships via panel update. Verify: Generator shows previous pages with a Legacy badge; Delete frees the slug; a new bundle can reuse the name.
### Changed
- _(none yet)_
+4
View File
@@ -808,6 +808,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "تحميل…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "قديم",
"legacy_hint": "صفحة التنزيل هذه من المولّد السابق. يمكنك إلغاءها أو حذفها لتحرير الاسم. لا يمكن تعديلها أو إعادة بنائها هنا.",
"legacy_leftovers_title": "صفحات التنزيل السابقة",
"legacy_leftovers_hint": "بقيت هذه الصفحات بعد تحديث المولّد. ألغِها أو احذفها لتحرير الاسم — لا تحتاج إلى تثبيت وحدة ALPHA أولاً.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -801,6 +801,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Načítání…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Legacy",
"legacy_hint": "Tato stránka ke stažení pochází z předchozího generátoru. Můžete ji zrušit nebo smazat a uvolnit tak název. Zde ji nelze upravit ani znovu sestavit.",
"legacy_leftovers_title": "Předchozí stránky ke stažení",
"legacy_leftovers_hint": "Tyto stránky zůstaly po aktualizaci generátoru. Zrušte je nebo smažte, abyste uvolnili název — modul ALPHA nemusíte nejdřív instalovat.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Indlæser...",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Ældre",
"legacy_hint": "Denne downloadside stammer fra den forrige generator. Du kan tilbagekalde eller slette den for at frigøre navnet. Den kan ikke redigeres eller genopbygges her.",
"legacy_leftovers_title": "Tidligere downloadsider",
"legacy_leftovers_hint": "Disse sider blev tilbage efter generatoropdateringen. Tilbagekald eller slet dem for at frigøre navnet — du behøver ikke installere ALPHA-modulet først.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -801,6 +801,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Wird geladen…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Legacy",
"legacy_hint": "Diese Download-Seite stammt vom vorherigen Generator. Sie können sie widerrufen oder löschen, um den Namen freizugeben. Bearbeiten oder neu erstellen ist hier nicht möglich.",
"legacy_leftovers_title": "Vorherige Download-Seiten",
"legacy_leftovers_hint": "Diese Seiten sind nach dem Generator-Update erhalten geblieben. Widerrufen oder löschen Sie sie, um den Namen freizugeben — das ALPHA-Modul muss dafür nicht installiert sein.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -794,6 +794,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Loading…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Legacy",
"legacy_hint": "This download page is from the previous generator. You can revoke or delete it to free the name. It cannot be edited or rebuilt here.",
"legacy_leftovers_title": "Previous download pages",
"legacy_leftovers_hint": "These pages stayed after the generator update. Revoke or delete them to free the name — you do not need to install the ALPHA module first.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -801,6 +801,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Cargando…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Heredado",
"legacy_hint": "Esta página de descarga proviene del generador anterior. Puede revocarla o eliminarla para liberar el nombre. No se puede editar ni reconstruir aquí.",
"legacy_leftovers_title": "Páginas de descarga anteriores",
"legacy_leftovers_hint": "Estas páginas permanecieron tras la actualización del generador. Revóquelas o elimínelas para liberar el nombre; no es necesario instalar antes el módulo ALPHA.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Ladataan…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Vanha",
"legacy_hint": "Tämä lataussivu on peräisin edellisestä generaattorista. Voit mitätöidä tai poistaa sen vapauttaaksesi nimen. Sitä ei voi muokata eikä rakentaa uudelleen täällä.",
"legacy_leftovers_title": "Aiemmat lataussivut",
"legacy_leftovers_hint": "Nämä sivut jäivät generaattorin päivityksen jälkeen. Mitätöi tai poista ne vapauttaaksesi nimen — ALPHA-moduulia ei tarvitse asentaa ensin.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -790,6 +790,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Wird geladen…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Ancien",
"legacy_hint": "Cette page de téléchargement provient de lancien générateur. Vous pouvez la révoquer ou la supprimer pour libérer le nom. Elle ne peut pas être modifiée ni reconstruite ici.",
"legacy_leftovers_title": "Pages de téléchargement précédentes",
"legacy_leftovers_hint": "Ces pages sont restées après la mise à jour du générateur. Révoquez-les ou supprimez-les pour libérer le nom — vous navez pas besoin dinstaller le module ALPHA au préalable.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "लोड हो रहा है...",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "पुराना",
"legacy_hint": "यह डाउनलोड पृष्ठ पिछले जनरेटर से है। नाम खाली करने के लिए आप इसे रद्द या हटा सकते हैं। यहाँ इसे संपादित या फिर से नहीं बनाया जा सकता।",
"legacy_leftovers_title": "पिछले डाउनलोड पृष्ठ",
"legacy_leftovers_hint": "जनरेटर अपडेट के बाद ये पृष्ठ रह गए। नाम खाली करने के लिए उन्हें रद्द या हटाएँ — पहले ALPHA मॉड्यूल इंस्टॉल करना ज़रूरी नहीं है।",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Betöltés…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Régi",
"legacy_hint": "Ez a letöltési oldal az előző generátorból származik. Visszavonhatja vagy törölheti, hogy felszabadítsa a nevet. Itt nem szerkeszthető és nem építhető újra.",
"legacy_leftovers_title": "Korábbi letöltési oldalak",
"legacy_leftovers_hint": "Ezek az oldalak megmaradtak a generátor frissítése után. Vonja vissza vagy törölje őket a név felszabadításához — az ALPHA modult nem kell előtte telepíteni.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Memuat…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Lama",
"legacy_hint": "Halaman unduhan ini berasal dari generator sebelumnya. Anda dapat mencabut atau menghapusnya untuk membebaskan nama. Tidak dapat diedit atau dibangun ulang di sini.",
"legacy_leftovers_title": "Halaman unduhan sebelumnya",
"legacy_leftovers_hint": "Halaman ini tetap ada setelah pembaruan generator. Cabut atau hapus untuk membebaskan nama — Anda tidak perlu memasang modul ALPHA terlebih dahulu.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -801,6 +801,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Caricamento…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Legacy",
"legacy_hint": "Questa pagina di download proviene dal generatore precedente. Puoi revocarla o eliminarla per liberare il nome. Qui non è possibile modificarla o ricostruirla.",
"legacy_leftovers_title": "Pagine di download precedenti",
"legacy_leftovers_hint": "Queste pagine sono rimaste dopo laggiornamento del generatore. Revocale o eliminalle per liberare il nome — non serve installare prima il modulo ALPHA.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -801,6 +801,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "読み込み中…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "旧版",
"legacy_hint": "このダウンロードページは以前のジェネレーターのものです。名前を解放するには無効化または削除できます。ここでは編集や再ビルドはできません。",
"legacy_leftovers_title": "以前のダウンロードページ",
"legacy_leftovers_hint": "ジェネレーター更新後もこれらのページが残っています。名前を解放するには無効化または削除してください。ALPHAモジュールのインストールは不要です。",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -801,6 +801,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "로드 중…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "이전",
"legacy_hint": "이 다운로드 페이지는 이전 생성기에서 남은 것입니다. 이름을 해제하려면 취소하거나 삭제할 수 있습니다. 여기서는 편집하거나 다시 빌드할 수 없습니다.",
"legacy_leftovers_title": "이전 다운로드 페이지",
"legacy_leftovers_hint": "생성기 업데이트 후에도 이 페이지가 남아 있습니다. 이름을 해제하려면 취소하거나 삭제하세요. ALPHA 모듈을 먼저 설치할 필요는 없습니다.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Laster inn …",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Eldre",
"legacy_hint": "Denne nedlastingssiden kommer fra den forrige generatoren. Du kan tilbakekalle eller slette den for å frigjøre navnet. Den kan ikke redigeres eller bygges på nytt her.",
"legacy_leftovers_title": "Tidligere nedlastingssider",
"legacy_leftovers_hint": "Disse sidene ble værende etter generatoroppdateringen. Tilbakekall eller slett dem for å frigjøre navnet — du trenger ikke installere ALPHA-modulen først.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -801,6 +801,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Laden…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Verouderd",
"legacy_hint": "Deze downloadpagina komt van de vorige generator. U kunt deze intrekken of verwijderen om de naam vrij te maken. Bewerken of opnieuw bouwen kan hier niet.",
"legacy_leftovers_title": "Vorige downloadpaginas",
"legacy_leftovers_hint": "Deze paginas bleven staan na de generator-update. Trek ze in of verwijder ze om de naam vrij te maken — u hoeft de ALPHA-module niet eerst te installeren.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -786,6 +786,10 @@
"empty_state": "Wybierz pakiet Support po lewej albo utwórz nowy.",
"loading": "Ładowanie…",
"no_bundles": "Brak pakietów. Utwórz pierwszego klienta BetterDesk Support.",
"legacy_badge": "Starsza wersja",
"legacy_hint": "Ta strona pobierania pochodzi z poprzedniego generatora. Możesz ją odwołać lub usunąć, żeby zwolnić nazwę. Nie da się jej tutaj edytować ani przebudować.",
"legacy_leftovers_title": "Poprzednie strony pobierania",
"legacy_leftovers_hint": "Te strony zostały po aktualizacji generatora. Odwołaj je lub usuń, żeby zwolnić nazwę — nie musisz najpierw instalować modułu ALPHA.",
"module_title": "Moduł BetterDesk Support",
"module_intro": "BetterDesk Support to klient desktop tylko-przychodzący (AGPL, na bazie BetterDesk-Client). Generator pobiera szablony portable z GitHub Releases i wstrzykuje zablokowany custom.txt dla każdej platformy. Wygląd ładuje się w runtime z API Client Branding — nie jest wbudowany w instalatory.",
"module_bullet_incoming": "Tylko połączenia przychodzące (bez łączenia wychodzącego do innych urządzeń)",
+4
View File
@@ -801,6 +801,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Carregando…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Legado",
"legacy_hint": "Esta página de download vem do gerador anterior. Você pode revogá-la ou excluí-la para liberar o nome. Não é possível editá-la nem reconstruí-la aqui.",
"legacy_leftovers_title": "Páginas de download anteriores",
"legacy_leftovers_hint": "Estas páginas permaneceram após a atualização do gerador. Revogue-as ou exclua-as para liberar o nome — não é necessário instalar o módulo ALPHA antes.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Se încarcă…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Legacy",
"legacy_hint": "Această pagină de descărcare provine din generatorul anterior. O puteți revoca sau șterge pentru a elibera numele. Nu poate fi editată sau reconstruită aici.",
"legacy_leftovers_title": "Pagini de descărcare anterioare",
"legacy_leftovers_hint": "Aceste pagini au rămas după actualizarea generatorului. Revocați-le sau ștergeți-le pentru a elibera numele — nu trebuie să instalați mai întâi modulul ALPHA.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Laddar...",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Äldre",
"legacy_hint": "Den här nedsidan kommer från den tidigare generatorn. Du kan återkalla eller ta bort den för att frigöra namnet. Den kan inte redigeras eller byggas om här.",
"legacy_leftovers_title": "Tidigare nedsidor",
"legacy_leftovers_hint": "Dessa sidor fanns kvar efter generatoruppdateringen. Återkalla eller ta bort dem för att frigöra namnet — du behöver inte installera ALPHA-modulen först.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "กำลังโหลด...",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "รุ่นเก่า",
"legacy_hint": "หน้าดาวน์โหลดนี้มาจากตัวสร้างรุ่นก่อน เพิกถอนหรือลบเพื่อปล่อยชื่อได้ ที่นี่แก้ไขหรือสร้างใหม่ไม่ได้",
"legacy_leftovers_title": "หน้าดาวน์โหลดก่อนหน้า",
"legacy_leftovers_hint": "หน้าเหล่านี้ยังอยู่หลังอัปเดตตัวสร้าง เพิกถอนหรือลบเพื่อปล่อยชื่อ ไม่ต้องติดตั้งโมดูล ALPHA ก่อน",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Yükleniyor…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Eski",
"legacy_hint": "Bu indirme sayfası önceki oluşturucudan kalmıştır. Adı serbest bırakmak için iptal edebilir veya silebilirsiniz. Burada düzenlenemez veya yeniden oluşturulamaz.",
"legacy_leftovers_title": "Önceki indirme sayfaları",
"legacy_leftovers_hint": "Bu sayfalar oluşturucu güncellemesinden sonra kaldı. Adı serbest bırakmak için iptal edin veya silin — önce ALPHA modülünü yüklemeniz gerekmez.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Завантаження…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Застаріла",
"legacy_hint": "Ця сторінка завантаження з попереднього генератора. Її можна відкликати або видалити, щоб звільнити назву. Тут її не можна редагувати чи перебудувати.",
"legacy_leftovers_title": "Попередні сторінки завантаження",
"legacy_leftovers_hint": "Ці сторінки лишилися після оновлення генератора. Відкликайте або видаліть їх, щоб звільнити назву — модуль ALPHA встановлювати не обов’язково.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -807,6 +807,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "Đang tải…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "Cũ",
"legacy_hint": "Trang tải xuống này đến từ trình tạo trước. Bạn có thể thu hồi hoặc xóa để giải phóng tên. Không thể chỉnh sửa hoặc dựng lại tại đây.",
"legacy_leftovers_title": "Các trang tải xuống trước",
"legacy_leftovers_hint": "Các trang này vẫn còn sau bản cập nhật trình tạo. Thu hồi hoặc xóa để giải phóng tên — bạn không cần cài mô-đun ALPHA trước.",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -790,6 +790,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "正在加載…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "舊版",
"legacy_hint": "此下載頁來自先前的產生器。您可以撤銷或刪除以釋放名稱。此處無法編輯或重新建置。",
"legacy_leftovers_title": "先前的下載頁",
"legacy_leftovers_hint": "產生器更新後這些頁面仍然保留。撤銷或刪除即可釋放名稱,不必先安裝 ALPHA 模組。",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+4
View File
@@ -801,6 +801,10 @@
"empty_state": "Select a Support bundle on the left or create a new one to begin.",
"loading": "正在加载…",
"no_bundles": "No bundles yet. Create your first BetterDesk Support client.",
"legacy_badge": "旧版",
"legacy_hint": "此下载页来自先前的生成器。可撤销或删除以释放名称。此处无法编辑或重新构建。",
"legacy_leftovers_title": "先前的下载页",
"legacy_leftovers_hint": "生成器更新后这些页面仍然保留。撤销或删除即可释放名称,无需先安装 ALPHA 模块。",
"module_title": "BetterDesk Support module",
"module_intro": "BetterDesk Support is an incoming-only desktop client (AGPL, based on BetterDesk-Client). The Generator downloads portable templates from GitHub Releases, then injects a server-locked custom.txt for each platform. Appearance is loaded at runtime from the Client Branding API — not baked into installers.",
"module_bullet_incoming": "Incoming connections only (no outbound remote to other devices)",
+20
View File
@@ -39,6 +39,25 @@ function isProductType(raw, expected) {
return normalizeProductType(raw) === expected;
}
function parseBundleBranding(raw) {
if (!raw) return {};
if (typeof raw === 'object') return raw;
try {
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch (_) {
return {};
}
}
/** True for template-generator (v2) rows; false for leftover pre-migration pages. */
function isBetterDeskSupportBundle(row) {
const branding = parseBundleBranding(row?.branding);
return branding.sku === 'betterdesk-support'
|| branding.generator_kind === 'betterdesk-support'
|| Number(branding.generator_version) >= 2;
}
function isQueuedBuildStatus(status) {
return QUEUED_BUILD_STATUSES.has(String(status ?? '').trim().toLowerCase());
}
@@ -54,6 +73,7 @@ module.exports = {
QUEUED_BUILD_STATUSES,
normalizeProductType,
isProductType,
isBetterDeskSupportBundle,
isQueuedBuildStatus,
normalizeBuildStatus,
};
+57 -2
View File
@@ -48,8 +48,63 @@
}
}
.bundle-item .badge-product--legacy {
opacity: 0.7;
.bundle-item-title {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
width: 100%;
}
.badge-product--legacy {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 1px 6px;
border-radius: 999px;
border: 1px solid var(--color-border, rgba(255, 255, 255, 0.16));
opacity: 0.75;
}
.gen-legacy-hint {
margin-bottom: 16px;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid var(--color-border, rgba(255, 255, 255, 0.12));
background: var(--color-bg-elevated, rgba(255, 255, 255, 0.03));
font-size: 13px;
line-height: 1.45;
}
.gen-legacy-leftovers {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--color-border, rgba(255, 255, 255, 0.08));
}
.gen-legacy-leftovers-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 10px;
}
.gen-legacy-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 10px 12px;
border: 1px solid var(--color-border, rgba(255, 255, 255, 0.08));
border-radius: 8px;
}
.gen-legacy-row-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.platforms-checklist {
+155 -41
View File
@@ -58,6 +58,7 @@
[
'gen-module-gate', 'gen-module-status', 'gen-accept-terms', 'gen-install-module',
'gen-finish-install', 'gen-main',
'gen-legacy-leftovers', 'gen-legacy-leftovers-list', 'gen-legacy-hint',
'gen-new-support', 'gen-bundle-list', 'gen-editor-title', 'gen-revoke-btn', 'gen-delete-btn',
'gen-save-btn', 'gen-rebuild-btn', 'gen-builds-list', 'gen-builds-summary', 'gen-platforms',
'gen-empty-state', 'gen-editor-form',
@@ -181,7 +182,42 @@
renderPlatforms();
}
function isLegacyBundle(bundle) {
return !!(bundle && bundle.legacy);
}
function leftoverBundles() {
return (state.bundles || []).filter(isLegacyBundle);
}
function isCurrentLegacy() {
return isLegacyBundle(state.currentBundle);
}
function setFormLocked(locked) {
['gen-name', 'gen-slug', 'gen-app-name', 'gen-server-host', 'gen-relay-host', 'gen-api-port', 'gen-use-https']
.forEach((id) => {
if (els[id]) els[id].disabled = !!locked;
});
if (els['gen-platforms']) {
els['gen-platforms'].querySelectorAll('input').forEach((input) => {
input.disabled = !!locked;
});
}
}
function applyLegacyEditorMode(isLegacy) {
if (els['gen-legacy-hint']) els['gen-legacy-hint'].classList.toggle('hidden', !isLegacy);
setFormLocked(isLegacy);
if (els['gen-save-btn']) {
els['gen-save-btn'].classList.toggle('hidden', !!isLegacy);
if (isLegacy) els['gen-save-btn'].disabled = true;
}
if (els['gen-rebuild-btn']) els['gen-rebuild-btn'].classList.toggle('hidden', !!isLegacy);
}
function markDirty() {
if (isCurrentLegacy() || !state.moduleReady) return;
state.dirty = true;
if (els['gen-save-btn']) els['gen-save-btn'].disabled = false;
}
@@ -217,8 +253,14 @@
els['gen-bundle-list'].innerHTML = state.bundles.map((b) => {
const active = state.currentId === b.bundle_id ? 'active' : '';
const revoked = b.revoked ? 'revoked' : '';
const badge = isLegacyBundle(b)
? `<span class="badge-product--legacy">${escapeText(t('generator.legacy_badge', 'Legacy'))}</span>`
: '';
return `<button type="button" class="bundle-item ${active} ${revoked}" data-id="${escapeText(b.bundle_id)}">
<span class="bundle-item-name">${escapeText(b.name)}</span>
<span class="bundle-item-title">
<span class="bundle-item-name">${escapeText(b.name)}</span>
${badge}
</span>
<span class="bundle-item-meta">${escapeText(b.slug || b.public_id || '')}</span>
</button>`;
}).join('');
@@ -252,7 +294,7 @@
const key = `${b.platform}/${b.arch}/${b.format}`;
const label = (state.platforms.find((p) => platformKey(p) === key) || {}).label || key;
const err = b.error_message ? `<div class="build-error">${escapeText(b.error_message)}</div>` : '';
const retry = b.status === 'failed'
const retry = !isCurrentLegacy() && b.status === 'failed'
? `<button type="button" class="btn btn-sm btn-secondary gen-retry-build"
data-platform="${escapeText(b.platform)}" data-arch="${escapeText(b.arch)}" data-format="${escapeText(b.format)}">Retry</button>`
: '';
@@ -286,6 +328,7 @@
}
function setEditorForNew() {
if (!state.moduleReady) return;
stopBuildsPoll();
state.currentId = 'new';
state.currentBundle = null;
@@ -293,6 +336,7 @@
state.slugManual = false;
state.productType = 'betterdesk-support';
setEditorVisible(true);
applyLegacyEditorMode(false);
els['gen-editor-title'].textContent = t('generator.new_bundle', 'New BetterDesk Support');
els['gen-name'].value = '';
els['gen-slug'].value = '';
@@ -319,15 +363,17 @@
state.slugManual = true;
state.productType = 'betterdesk-support';
setEditorVisible(true);
applyLegacyEditorMode(isLegacyBundle(bundle));
els['gen-editor-title'].textContent = bundle.name;
els['gen-name'].value = bundle.name || '';
els['gen-slug'].value = bundle.slug || '';
writeBranding(bundle.branding || {});
selectAllPlatforms();
setFormLocked(isLegacyBundle(bundle));
els['gen-download-info'].classList.remove('hidden');
els['gen-revoke-btn'].classList.remove('hidden');
els['gen-delete-btn'].classList.remove('hidden');
els['gen-rebuild-btn'].classList.remove('hidden');
els['gen-rebuild-btn'].classList.toggle('hidden', isLegacyBundle(bundle));
els['gen-revoke-btn'].textContent = bundle.revoked
? t('generator.unrevoke', 'Restore')
: t('generator.revoke', 'Revoke');
@@ -336,13 +382,15 @@
renderBuilds();
renderBundleList();
updateDownloadLinkPreview();
startBuildsPoll();
if (isLegacyBundle(bundle)) stopBuildsPoll();
else startBuildsPoll();
} catch (err) {
notify.error(err.message);
}
}
async function saveBundle() {
if (isCurrentLegacy() || !state.moduleReady) return;
const name = els['gen-name'].value.trim();
if (!name) {
notify.error(t('generator.errors.name_required', 'Name is required'));
@@ -378,14 +426,10 @@
}
async function loadBundles() {
const data = await api('GET', '/api/generator/bundles?includeRevoked=1');
// Only BetterDesk Support (v2) profiles — hide legacy CDAP Support Agent rows
const all = (data.data && data.data.bundles) || [];
state.bundles = all.filter((b) => {
const br = b.branding || {};
return br.sku === 'betterdesk-support' || br.generator_kind === 'betterdesk-support';
});
const data = await api('GET', '/api/generator/bundles?includeRevoked=1&includeLegacy=1');
state.bundles = (data.data && data.data.bundles) || [];
renderBundleList();
applyGeneratorLayout();
}
async function loadPlatforms() {
@@ -473,19 +517,108 @@
}
}
function showModuleGate(show) {
if (els['gen-module-gate']) els['gen-module-gate'].classList.toggle('hidden', !show);
if (els['gen-main']) els['gen-main'].classList.toggle('hidden', show);
function leftoverPublicId(bundle) {
return bundle.slug || bundle.public_id || bundle.bundle_id || '';
}
function renderLeftoverGateList() {
const wrap = els['gen-legacy-leftovers'];
const list = els['gen-legacy-leftovers-list'];
if (!wrap || !list) return;
const leftovers = leftoverBundles();
const show = leftovers.length > 0 && !state.moduleReady;
wrap.classList.toggle('hidden', !show);
if (!show) {
list.innerHTML = '';
return;
}
list.innerHTML = leftovers.map((b) => {
const publicId = leftoverPublicId(b);
const revokeLabel = b.revoked
? t('generator.unrevoke', 'Restore')
: t('generator.revoke', 'Revoke');
return `<div class="gen-legacy-row" data-id="${escapeText(b.bundle_id)}">
<div>
<div class="bundle-item-title">
<span class="bundle-item-name">${escapeText(b.name)}</span>
<span class="badge-product--legacy">${escapeText(t('generator.legacy_badge', 'Legacy'))}</span>
</div>
<div class="bundle-item-meta">${escapeText(publicId ? `/d/${publicId}` : '')}</div>
</div>
<div class="gen-legacy-row-actions">
<a class="btn btn-secondary btn-sm" href="${escapeText(publicId ? `/d/${publicId}` : '#')}" target="_blank" rel="noopener">${escapeText(t('generator.open_portal', 'Open'))}</a>
<button type="button" class="btn btn-warning btn-sm" data-action="revoke">${escapeText(revokeLabel)}</button>
<button type="button" class="btn btn-danger btn-sm" data-action="delete">${escapeText(t('common.delete', 'Delete'))}</button>
</div>
</div>`;
}).join('');
list.querySelectorAll('.gen-legacy-row').forEach((row) => {
const id = row.dataset.id;
const bundle = leftovers.find((b) => b.bundle_id === id);
const revokeBtn = row.querySelector('[data-action="revoke"]');
const deleteBtn = row.querySelector('[data-action="delete"]');
if (revokeBtn) {
revokeBtn.addEventListener('click', () => revokeBundle(id, !(bundle && bundle.revoked)));
}
if (deleteBtn) {
deleteBtn.addEventListener('click', () => deleteBundle(id));
}
});
}
function applyGeneratorLayout() {
const ready = !!state.moduleReady;
if (els['gen-module-gate']) els['gen-module-gate'].classList.toggle('hidden', ready);
if (els['gen-main']) els['gen-main'].classList.toggle('hidden', !ready);
if (els['gen-new-support']) els['gen-new-support'].classList.toggle('hidden', !ready);
renderLeftoverGateList();
}
async function refreshModuleStatus() {
const data = await api('GET', '/api/generator/module/status');
const status = data.data || {};
renderModuleStatus(status);
showModuleGate(!status.ready);
applyGeneratorLayout();
return status;
}
async function revokeBundle(id, revoked) {
if (!id) return;
const confirmKey = revoked ? 'generator.confirm_revoke' : 'generator.confirm_unrevoke';
const confirmDef = revoked
? 'Revoke this bundle? The download link will stop working.'
: 'Re-enable this bundle?';
if (!window.confirm(t(confirmKey, confirmDef))) return;
try {
await api('POST', `/api/generator/bundles/${id}/revoke`, { revoked });
notify.success(revoked
? t('generator.revoked_ok', 'Bundle revoked')
: t('generator.unrevoked_ok', 'Bundle re-enabled'));
await loadBundles();
if (state.currentId === id) await openBundle(id);
} catch (err) {
notify.error(err.message);
}
}
async function deleteBundle(id) {
if (!id) return;
if (!window.confirm(t('generator.confirm_delete', 'Delete this bundle permanently? This cannot be undone.'))) return;
try {
await api('DELETE', `/api/generator/bundles/${id}`);
notify.success(t('generator.deleted', 'Bundle deleted'));
if (state.currentId === id) {
state.currentId = null;
state.currentBundle = null;
if (els['gen-empty-state']) setEditorVisible(false);
stopBuildsPoll();
}
await loadBundles();
} catch (err) {
notify.error(err.message);
}
}
function bindEvents() {
if (els['gen-new-support']) {
els['gen-new-support'].addEventListener('click', () => setEditorForNew());
@@ -520,40 +653,22 @@
}
if (els['gen-revoke-btn']) {
els['gen-revoke-btn'].addEventListener('click', async () => {
els['gen-revoke-btn'].addEventListener('click', () => {
if (!state.currentId || state.currentId === 'new') return;
const revoked = !(state.currentBundle && state.currentBundle.revoked);
try {
await api('POST', `/api/generator/bundles/${state.currentId}/revoke`, { revoked });
notify.success(revoked ? t('generator.revoked', 'Revoked') : t('generator.restored', 'Restored'));
await loadBundles();
await openBundle(state.currentId);
} catch (err) {
notify.error(err.message);
}
revokeBundle(state.currentId, !(state.currentBundle && state.currentBundle.revoked));
});
}
if (els['gen-delete-btn']) {
els['gen-delete-btn'].addEventListener('click', async () => {
els['gen-delete-btn'].addEventListener('click', () => {
if (!state.currentId || state.currentId === 'new') return;
if (!window.confirm(t('generator.confirm_delete', 'Delete this bundle?'))) return;
try {
await api('DELETE', `/api/generator/bundles/${state.currentId}`);
notify.success(t('common.deleted', 'Deleted'));
state.currentId = null;
setEditorVisible(false);
stopBuildsPoll();
await loadBundles();
} catch (err) {
notify.error(err.message);
}
deleteBundle(state.currentId);
});
}
if (els['gen-rebuild-btn']) {
els['gen-rebuild-btn'].addEventListener('click', async () => {
if (!state.currentId || state.currentId === 'new') return;
if (!state.currentId || state.currentId === 'new' || isCurrentLegacy() || !state.moduleReady) return;
try {
await api('POST', `/api/generator/bundles/${state.currentId}/rebuild`, {
platforms: selectedPlatformPayload(),
@@ -598,7 +713,6 @@
els['gen-finish-install'].addEventListener('click', async () => {
const status = await refreshModuleStatus();
if (status.ready) {
showModuleGate(false);
await initGeneratorMain();
}
});
@@ -616,13 +730,13 @@
bindEvents();
try {
const status = await refreshModuleStatus();
await loadBundles();
if (status.ready) {
showModuleGate(false);
await initGeneratorMain();
}
} catch (err) {
notify.error(err.message);
showModuleGate(true);
applyGeneratorLayout();
}
}
+2 -8
View File
@@ -22,7 +22,7 @@ const config = require('../config/config');
const brandingService = require('../services/brandingService');
const conn = require('../services/agentBundleConnection');
const clientConfigHost = require('../services/clientConfigHost');
const { PRODUCT_TYPES, normalizeProductType } = require('../lib/generatorBuildTypes');
const { PRODUCT_TYPES, normalizeProductType, isBetterDeskSupportBundle } = require('../lib/generatorBuildTypes');
// Branding payloads may carry a base64-encoded logo up to 10 MB; expand the
// default 2 MB JSON body limit on the bundle CRUD + preview endpoints only.
@@ -50,6 +50,7 @@ function serializeBundle(row) {
branding: publicBrandingView(parseBranding(row.branding)),
branding_hash: row.branding_hash,
revoked: !!row.revoked,
legacy: !isBetterDeskSupportBundle(row),
download_count: Number(row.download_count || 0),
created_at: row.created_at,
updated_at: row.updated_at,
@@ -215,13 +216,6 @@ router.post('/api/generator/module/install', requireAuth, requireAdmin, async (r
// Bundle management API (admin only)
// =========================================================================
function isBetterDeskSupportBundle(row) {
const branding = parseBranding(row?.branding);
return branding.sku === 'betterdesk-support'
|| branding.generator_kind === 'betterdesk-support'
|| Number(branding.generator_version) >= 2;
}
router.get('/api/generator/bundles', requireAuth, requireAdmin, async (req, res) => {
try {
const includeRevoked = req.query.includeRevoked === '1';
@@ -3,6 +3,7 @@
const {
PRODUCT_TYPES,
normalizeProductType,
isBetterDeskSupportBundle,
isQueuedBuildStatus,
normalizeBuildStatus,
} = require('../lib/generatorBuildTypes');
@@ -19,6 +20,19 @@ describe('generator build type compatibility', () => {
expect(normalizeProductType('unknown')).toBe(PRODUCT_TYPES.BETTERDESK_SUPPORT);
});
test('detects BetterDesk Support v2 bundles vs leftover generator rows', () => {
expect(isBetterDeskSupportBundle({ branding: { sku: 'betterdesk-support' } })).toBe(true);
expect(isBetterDeskSupportBundle({ branding: { generator_kind: 'betterdesk-support' } })).toBe(true);
expect(isBetterDeskSupportBundle({ branding: { generator_version: 2 } })).toBe(true);
expect(isBetterDeskSupportBundle({ branding: { generator_version: '2' } })).toBe(true);
expect(isBetterDeskSupportBundle({ branding: '{"sku":"betterdesk-support"}' })).toBe(true);
expect(isBetterDeskSupportBundle({ branding: {} })).toBe(false);
expect(isBetterDeskSupportBundle({ branding: { app_name: 'Old Support Agent' } })).toBe(false);
expect(isBetterDeskSupportBundle({ branding: '{}' })).toBe(false);
expect(isBetterDeskSupportBundle({ product_type: 'betterdesk-support' })).toBe(false);
expect(isBetterDeskSupportBundle(null)).toBe(false);
});
test('treats queued and legacy pending jobs as the same queue state', () => {
expect(isQueuedBuildStatus('queued')).toBe(true);
expect(isQueuedBuildStatus('PENDING')).toBe(true);
+6
View File
@@ -43,6 +43,11 @@
${_('generator.module_finish')}
</button>
</div>
<div id="gen-legacy-leftovers" class="gen-legacy-leftovers hidden">
<h3 class="section-title">${_('generator.legacy_leftovers_title')}</h3>
<p class="form-hint">${_('generator.legacy_leftovers_hint')}</p>
<div id="gen-legacy-leftovers-list" class="gen-legacy-leftovers-list"></div>
</div>
</div>
</div>
@@ -99,6 +104,7 @@
</div>
<div id="gen-editor-form" class="editor-form hidden">
<div id="gen-legacy-hint" class="gen-legacy-hint hidden">${_('generator.legacy_hint')}</div>
<div class="editor-fields">
<div class="form-group">
<label class="form-label" for="gen-name">${_('generator.bundle_name')}</label>