feat: separate manifest.displayLabel for catalog UI from manifest.description

manifest.description is for composer / dev consumption (often a long
English text). The admin catalog needs short Hebrew labels. Introduces
optional manifest.displayLabel field — only that field is synced into
extensions.description (the column the admin UI reads). When
displayLabel is missing the DB row is left untouched, so curated
labels never get clobbered by long English manifest descriptions.

Adds POST /admin/set-labels for bulk backfilling labels in one call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 17:55:59 +00:00
parent 536b03b34d
commit 740866eb1c
+39 -12
View File
@@ -171,10 +171,12 @@ def sync_extension_registry():
) sub WHERE e.id = sub.extension_id AND (e.latest_version IS NULL OR e.latest_version != sub.max_ver)""", ) sub WHERE e.id = sub.extension_id AND (e.latest_version IS NULL OR e.latest_version != sub.max_ver)""",
fetch=None) fetch=None)
# Sync description from each extension's manifest.json (main branch) — manifest is source of truth for the catalog label # Sync catalog display label from each extension's manifest.json (main branch).
# Source: optional `displayLabel` field. `manifest.description` is intentionally NOT used —
# it's for composer/dev consumption and is often long English; the catalog needs short Hebrew.
description_updates = [] description_updates = []
for name in gitea_extensions: for name in gitea_extensions:
if _sync_description_from_manifest(name): if _sync_display_label_from_manifest(name):
description_updates.append(name) description_updates.append(name)
# Reload MODULES from DB # Reload MODULES from DB
@@ -189,9 +191,12 @@ def sync_extension_registry():
} }
def _sync_description_from_manifest(module): def _sync_display_label_from_manifest(module):
"""Fetch manifest.json from the extension's Gitea main branch and update extensions.description. """Fetch manifest.json from the extension's Gitea main branch and update extensions.description
Returns True if the description was changed.""" from manifest.displayLabel (the short Hebrew catalog label). Leaves DB untouched if displayLabel
is missing — `manifest.description` is intentionally NOT consulted (it's for composer/dev use
and is often a long English text unsuitable for the admin UI).
Returns True if the row was changed."""
resp = gitea_api('GET', f'/repos/{GITEA_ORG}/{module}/contents/manifest.json') resp = gitea_api('GET', f'/repos/{GITEA_ORG}/{module}/contents/manifest.json')
if not isinstance(resp, dict) or not resp.get('content'): if not isinstance(resp, dict) or not resp.get('content'):
return False return False
@@ -199,14 +204,14 @@ def _sync_description_from_manifest(module):
manifest = json.loads(base64.b64decode(resp['content'])) manifest = json.loads(base64.b64decode(resp['content']))
except Exception: except Exception:
return False return False
desc = (manifest.get('description') or '').strip() label = (manifest.get('displayLabel') or '').strip()
if not desc: if not label:
return False return False
current = query("SELECT description FROM extensions WHERE name = %s", (module,), fetch='one') current = query("SELECT description FROM extensions WHERE name = %s", (module,), fetch='one')
if current and (current.get('description') or '').strip() == desc: if current and (current.get('description') or '').strip() == label:
return False return False
query("UPDATE extensions SET description = %s, updated_at = NOW() WHERE name = %s", query("UPDATE extensions SET description = %s, updated_at = NOW() WHERE name = %s",
(desc, module), fetch=None) (label, module), fetch=None)
return True return True
SSL_CTX = ssl.create_default_context() SSL_CTX = ssl.create_default_context()
@@ -409,10 +414,12 @@ def build_and_publish(module, version):
cur.execute("""UPDATE extensions SET latest_version = %s, updated_at = NOW() cur.execute("""UPDATE extensions SET latest_version = %s, updated_at = NOW()
WHERE name = %s AND (latest_version IS NULL OR string_to_array(latest_version, '.')::int[] < string_to_array(%s, '.')::int[])""", WHERE name = %s AND (latest_version IS NULL OR string_to_array(latest_version, '.')::int[] < string_to_array(%s, '.')::int[])""",
(version, module, version)) (version, module, version))
manifest_desc = (manifest.get('description') or '').strip() # Sync catalog display label from manifest.displayLabel (short Hebrew). Skip manifest.description
if manifest_desc: # — it's for composer/dev use, often long English, and unsuitable for the admin UI.
manifest_label = (manifest.get('displayLabel') or '').strip()
if manifest_label:
cur.execute("UPDATE extensions SET description = %s, updated_at = NOW() WHERE name = %s", cur.execute("UPDATE extensions SET description = %s, updated_at = NOW() WHERE name = %s",
(manifest_desc, module)) (manifest_label, module))
conn.commit() conn.commit()
cur.close() cur.close()
conn.close() conn.close()
@@ -792,6 +799,8 @@ class APIHandler(BaseHTTPRequestHandler):
self._handle_publish(body) self._handle_publish(body)
elif path == '/release-deleted': elif path == '/release-deleted':
self._handle_release_deleted(body) self._handle_release_deleted(body)
elif path == '/admin/set-labels':
self._handle_admin_set_labels(body)
elif path == '/deploy': elif path == '/deploy':
self._handle_deploy(body) self._handle_deploy(body)
elif path == '/deploy-all': elif path == '/deploy-all':
@@ -940,6 +949,24 @@ class APIHandler(BaseHTTPRequestHandler):
result = build_and_publish(module, version) result = build_and_publish(module, version)
self._respond(200, result) self._respond(200, result)
def _handle_admin_set_labels(self, body):
"""Bulk-set extensions.description (the catalog display label) for one or many extensions.
Body: { "labels": { "ModuleName": "תווית קצרה", ... } }"""
labels = body.get('labels', {})
if not isinstance(labels, dict) or not labels:
self._respond(400, {'error': 'labels dict required'}); return
updated, skipped = [], []
for name, label in labels.items():
if not isinstance(label, str):
skipped.append({'name': name, 'reason': 'label must be string'}); continue
ext = query("SELECT id FROM extensions WHERE name = %s", (name,), fetch='one')
if not ext:
skipped.append({'name': name, 'reason': 'not found'}); continue
query("UPDATE extensions SET description = %s, updated_at = NOW() WHERE id = %s",
(label.strip(), ext['id']), fetch=None)
updated.append(name)
self._respond(200, {'status': 'ok', 'updated': updated, 'skipped': skipped})
def _handle_release_deleted(self, body): def _handle_release_deleted(self, body):
"""Triggered by Gitea release-deleted webhook (via n8n). """Triggered by Gitea release-deleted webhook (via n8n).
Removes the version row, recomputes latest_version, and deactivates the extension if no versions remain. Removes the version row, recomputes latest_version, and deactivates the extension if no versions remain.