diff --git a/controller/internal/web/settings_split_test.go b/controller/internal/web/settings_split_test.go
index fad2460..d2d3bd0 100644
--- a/controller/internal/web/settings_split_test.go
+++ b/controller/internal/web/settings_split_test.go
@@ -252,3 +252,41 @@ func TestStorageAgentDownNote(t *testing.T) {
t.Error("enrichment JS lacks the warn-note error path")
}
}
+
+// TestNoEmojiInTemplates (D1 §10, Group G): no emoji/pictographs in web templates. Go-side
+// codepoint scan (the D0 grep-based gate false-negatived multibyte emoji on Windows).
+func TestNoEmojiInTemplates(t *testing.T) {
+ allow := map[rune]bool{}
+ for _, r := range "✓✗✔✘•●○■▶" {
+ allow[r] = true
+ }
+ isEmoji := func(r rune) bool {
+ if allow[r] {
+ return false
+ }
+ switch {
+ case r >= 0x1F300 && r <= 0x1FAFF,
+ r >= 0x2600 && r <= 0x26FF,
+ r >= 0x2700 && r <= 0x27BF,
+ r >= 0xFE00 && r <= 0xFE0F,
+ r >= 0x1F1E6 && r <= 0x1F1FF:
+ return true
+ }
+ return false
+ }
+ entries, _ := templateFS.ReadDir("templates")
+ for _, e := range entries {
+ if !strings.HasSuffix(e.Name(), ".html") {
+ continue
+ }
+ b, err := templateFS.ReadFile("templates/" + e.Name())
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, r := range string(b) {
+ if isEmoji(r) {
+ t.Errorf("%s contains emoji %q (U+%04X)", e.Name(), r, r)
+ }
+ }
+ }
+}
diff --git a/controller/internal/web/templates/backups.html b/controller/internal/web/templates/backups.html
index 7734cb2..e915d0f 100644
--- a/controller/internal/web/templates/backups.html
+++ b/controller/internal/web/templates/backups.html
@@ -434,7 +434,7 @@
{{end}}
{{if .Tier2SizeHuman}}{{.Tier2SizeHuman}}{{end}}
{{.BackupContents}}
- 📁
+
@@ -743,7 +743,7 @@ function onRestoreAppChange() {
var hasVolumes = opt.getAttribute('data-has-volumes') === 'true';
if (hasHDD || hasVolumes) {
- typeInfo.innerHTML = '🔄 Teljes visszaállítás: adatbázis + konfiguráció + felhasználói adatok a kiválasztott pillanatképből.';
+ typeInfo.innerHTML = 'Teljes visszaállítás: adatbázis + konfiguráció + felhasználói adatok a kiválasztott pillanatképből.';
typeInfo.className = 'restore-info';
} else if (hasDB) {
typeInfo.innerHTML = 'Adatbázis és konfiguráció visszaállítása — az alkalmazásnak nincs külön felhasználói adata.';
diff --git a/controller/internal/web/templates/debug.html b/controller/internal/web/templates/debug.html
index ac146eb..986a493 100644
--- a/controller/internal/web/templates/debug.html
+++ b/controller/internal/web/templates/debug.html
@@ -362,7 +362,7 @@ function renderDiagnostic(d) {
html += 'Ütemező
| Név | Típus | Utolsó futás | Fut |
';
d.scheduler.forEach(function(j) {
var type = j.type === 'daily' ? j.schedule : (j.interval || '-');
- html += '| ' + j.name + ' | ' + type + ' | ' + (j.last_run ? fmtTime(j.last_run) : '-') + ' | ' + (j.running ? '🔄' : '-') + ' |
';
+ html += '| ' + j.name + ' | ' + type + ' | ' + (j.last_run ? fmtTime(j.last_run) : '-') + ' | ' + (j.running ? 'fut' : '-') + ' |
';
});
html += '
';
}
@@ -835,7 +835,7 @@ function renderAppBundles(bundles) {
html += '' + (b.exported_at || '-') + ' | ';
html += '' + (b.size_human || '-') + ' | ';
html += '' + escapeHtml(b.drive_label || b.drive_path) + ' | ';
- html += '' + (b.encrypted ? '🔒' : '-') + ' | ';
+ html += '' + (b.encrypted ? 'titkosított' : '-') + ' | ';
html += '' + (b.has_db ? 'igen' : '-') + ' | ';
html += '' + (b.needs_hdd ? 'igen' : '-') + ' | ';
html += '' + escapeHtml(b.path) + ' | ';
diff --git a/controller/internal/web/templates/deploy.html b/controller/internal/web/templates/deploy.html
index 7ce2ff3..1c26ad0 100644
--- a/controller/internal/web/templates/deploy.html
+++ b/controller/internal/web/templates/deploy.html
@@ -69,7 +69,7 @@
{{if .OtherStoragePaths}}
- 📦 Mozgatás másik tárolóra
+ Mozgatás másik tárolóra
{{end}}
@@ -565,7 +565,7 @@
{{range $.StoragePaths}}
{{end}}
diff --git a/controller/internal/web/templates/storage.html b/controller/internal/web/templates/storage.html
index 60f810d..8f2b6b1 100644
--- a/controller/internal/web/templates/storage.html
+++ b/controller/internal/web/templates/storage.html
@@ -102,7 +102,7 @@
{{.Name}}
{{if .SizeHuman}}
{{.SizeHuman}}{{end}}
-
📦 Mozgatás
+
Mozgatás
{{end}}
@@ -151,7 +151,7 @@
{{$src := .Path}}
diff --git a/controller/scripts/emoji_gate.py b/controller/scripts/emoji_gate.py
new file mode 100644
index 0000000..2a5a937
--- /dev/null
+++ b/controller/scripts/emoji_gate.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+"""D1 §10 Python emoji gate — Windows grep silently fails to match multibyte emoji (the D0
+grep-gate zero was a false negative). This scans templates by Unicode codepoint.
+
+Run from controller/: python scripts/emoji_gate.py
+Exit 1 if any emoji/pictograph remains in web+setup templates.
+"""
+import io, os, sys, unicodedata
+
+ROOTS = [
+ os.path.join("internal", "web", "templates"),
+ os.path.join("internal", "setup", "templates"),
+]
+
+# Codepoint ranges that count as "emoji / pictographs / dingbats" for UI-copy purposes.
+# Deliberately does NOT flag Hungarian letters, typographic quotes/dashes, the middle dot (·),
+# arrows used as affordances (→ ↗ ↻ ↑ ↓), the multiplication sign (×), or box-drawing.
+def is_emoji(ch):
+ o = ord(ch)
+ ranges = [
+ (0x1F300, 0x1FAFF), # Misc symbols & pictographs, emoticons, transport, supplemental, symbols-ext
+ (0x2600, 0x26FF), # Misc symbols (☀ ⚙ ⚠ ☁ …)
+ (0x2700, 0x27BF), # Dingbats (✅ ✂ ✈ ✏ ✓? no — see allow)
+ (0x1F000, 0x1F0FF), # Mahjong/dominoes/cards
+ (0xFE00, 0xFE0F), # Variation selectors (emoji presentation)
+ (0x1F1E6, 0x1F1FF), # Regional indicators
+ ]
+ if any(a <= o <= b for a, b in ranges):
+ return True
+ return False
+
+# Dingbat codepoints that are legitimate UI glyphs (checkmarks/crosses used as plain text marks,
+# not emoji). We keep these OUT of the ban — they render as monochrome text, not color emoji.
+ALLOW = set("✓✗✔✘•●○■▶") # ✓ ✗ ✔ ✘ • ● ○ ■ ▶
+
+
+def scan(path):
+ hits = []
+ for lineno, line in enumerate(io.open(path, encoding="utf-8"), 1):
+ for ch in line:
+ if ch in ALLOW:
+ continue
+ if is_emoji(ch):
+ try:
+ name = unicodedata.name(ch)
+ except ValueError:
+ name = "U+%04X" % ord(ch)
+ hits.append((lineno, ch, name))
+ return hits
+
+
+def main():
+ total = 0
+ for root in ROOTS:
+ for fn in sorted(os.listdir(root)):
+ if not fn.endswith(".html"):
+ continue
+ path = os.path.join(root, fn)
+ for lineno, ch, name in scan(path):
+ total += 1
+ print("%s:%d %s %s" % (fn, lineno, ch, name))
+ if total:
+ print("EMOJI GATE FAILED: %d emoji found" % total)
+ sys.exit(1)
+ print("emoji gate OK — no emoji in web/setup templates")
+
+
+if __name__ == "__main__":
+ main()