Files
felhom.eu/documentation/audits/website-refresh-2026-10-08/tools/apps_build.py
T
admin 67b3fd23d1
gates / gates (push) Failing after 13m1s
website refresh: true numbers (56 apps), claims checked against the capability map, ten missing apps, an English twin of every public page (public, choice B), 404 page; site gates 13-18 + 11 decoys; R-902 opened
Part A: claim table documentation/audits/website-refresh-2026-10-08/claims.md; "100% open source" corrected
from the licence read; unbacked claims cut (firewall, RAID, snapshots, new-machine restore, self-managed
mode, household VPN, "never lost"). Part B: dawarich, docmost, grimmory, homebox, karakeep, mealie, metube,
radicale, recipe-importer, sparkyfitness cards; plant-it and wger cut (not offered). Part C: /en/ twins,
nav language switch, hreflang both ways, sitemap with xhtml:link, og-image-en.png. Part D: site_gates.py
twins/lang/same-apps/no-Hungarian/FAQ-JSON-LD/tail. Operator ruling 9 + choice B in 10-localisation.md §11,
§10.7. Register: R-902 (contact mailer source in no repo); R-813, R-784, R-559 annotated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
2026-10-08 08:29:17 +02:00

242 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Part B + C: /alkalmazasok (HU) and /en/apps (EN) from one card list. Starts from HEAD each run."""
import html, os, re, sys
sys.path.insert(0, __file__.rsplit("/", 1)[0])
from common import *
from cards_data import *
src = head_text("alkalmazasok.html")
cats = re.findall(r'(<div class="category-section" id="([^"]+)">\s*<div class="category-header">\s*<span class="category-icon">(.*?)</span>).*?\n </div>\n\n', src, re.S)
icons = {cid: icon for _, cid, icon in cats}
cards = [] # dicts in page order
for m in re.finditer(r'<div class="category-section" id="([^"]+)">(.*?)\n </div>\n\n', src, re.S):
cid, body = m.group(1), m.group(2)
for cm, c in re.findall(r'<!-- (.*?) -->\n\s*<div class="app-card">(.*?)\n </div>\n(?=\s*(?:<!--|</div>))', body, re.S):
if cm in REMOVE:
continue
logo = re.search(r'<img src="/assets/([^"]+)" alt="([^"]*)" class="app-logo"', c).group(1)
shot = re.search(r'<img src="/assets/([^"]+)" alt="[^"]*" class="app-screenshot">', c)
href = re.search(r'<a href="([^"]*)"[^>]*class="github-link"', c).group(1)
lab = re.search(r'alt="GitHub">(.*?)</a>', c).group(1)
d = dict(comment=cm, cat=cid, name=re.search(r"<h3>(.*?)</h3>", c).group(1), logo=logo,
shot=shot.group(1) if shot else None, src=href, srclabel="site" if lab == "Weboldal" else "code",
badge="oss",
hu=(re.search(r'class="app-tagline">(.*?)</span>', c).group(1),
re.search(r'class="app-description">(.*?)</p>', c, re.S).group(1),
re.findall(r'class="app-feature">(.*?)</span>', c)))
d.update(OVERRIDE.get(cm, {}))
cards.append(d)
for after, nc in NEW_CARDS:
i = next(k for k, c in enumerate(cards) if c["comment"] == after)
nc = dict(nc)
nc.setdefault("srclabel", "code")
nc["hu"] = NEW_HU[nc["comment"]]
nc["shot"] = nc["slug"] + "-screenshot-1.webp"
cards.insert(i + 1, nc)
def asset_ok(name):
return name and os.path.exists(os.path.join(W, "assets", name))
for c in cards:
# a logo referenced as .svg that exists only as .png: point at the .png (saves a 404 per load)
if not asset_ok(c["logo"]):
alt = c["logo"].rsplit(".", 1)[0] + ".png"
if asset_ok(alt):
c["logo"] = alt
else:
c["logo_missing"] = True
if not asset_ok(c["shot"]):
c["shot"] = None
c["en"] = EN[c["comment"]]
missing = [c["comment"] for c in cards if c.get("logo_missing")]
noshot = [c["comment"] for c in cards if not c["shot"]]
print("cards:", len(cards), "| logo missing:", missing, "| no screenshot:", noshot)
def card_html(c, lang):
tag, desc, feats = c[lang]
name = c["name"]
out = [" <!-- %s -->" % c["comment"],
' <div class="app-card">',
' <div class="app-card-header">']
if not c.get("logo_missing"):
out.append(' <img src="/assets/%s" alt="%s" class="app-logo">' % (c["logo"], name))
out += [' <div class="app-title-area"><h3>%s</h3><span class="app-tagline">%s</span></div>' % (name, tag),
' </div>',
' <div class="app-card-body">',
' <p class="app-description">%s</p>' % desc,
' <div class="app-features">%s</div>' % "".join('<span class="app-feature">%s</span>' % f for f in feats)]
if c["shot"]:
out.append(' <img src="/assets/%s" alt="%s screenshot" class="app-screenshot">' % (c["shot"], name))
out += [' </div>', ' <div class="app-card-footer">']
if c.get("src"):
out.append(' <a href="%s" target="_blank" rel="noopener" class="github-link"><img src="/assets/github.svg" alt="GitHub">%s</a>'
% (c["src"], SRCLABEL[lang][c["srclabel"]]))
b = c["badge"]
if b == "oss":
out.append(' <span class="open-source-badge"><svg class="ico ico-yes" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-check"/></svg>%s</span>' % BADGE[lang][b])
elif b == "builtin":
out.append(' <span class="open-source-badge"><svg class="ico ico-yes" aria-hidden="true"><use href="/assets/icons.svg?v=1#i-check"/></svg>Open Source · %s</span>' % BADGE[lang][b])
else:
out.append(' <span class="open-source-badge" style="color: var(--warn);"><img src="/assets/checkmark-yellow.svg" alt="">%s</span>' % BADGE[lang][b])
out += [' </div>', ' </div>']
return "\n".join(out)
def sections(lang):
out = []
for n, (cid, icon_name, hu_t, en_id, en_t) in enumerate(CATEGORIES, 1):
cc = [c for c in cards if c["cat"] == cid]
builtin = sum(1 for c in cc if c["badge"] == "builtin")
k = len(cc) - builtin
if lang == "hu":
count = "%d alkalmazás" % k + (" + %d beépített" % builtin if builtin else "")
sec_id, title = cid, hu_t
else:
count = ("%d app" % k if k == 1 else "%d apps" % k) + (" + %d built in" % builtin if builtin else "")
sec_id, title = en_id, en_t
out.append(""" <!-- ==================== -->
<!-- %d. %s -->
<!-- ==================== -->
<div class="category-section" id="%s">
<div class="category-header">
<span class="category-icon">%s</span>
<h2>%s</h2>
<span class="app-count">%s</span>
</div>
<div class="app-grid">
%s
</div>
</div>
""" % (n, html.unescape(title).upper() if lang == "en" else title.upper(), sec_id, icons[cid], title, count,
"\n".join(card_html(c, lang) for c in cc)))
return "\n".join(out)
total = sum(1 for c in cards if c["badge"] != "builtin")
oss = sum(1 for c in cards if c["badge"] == "oss")
assert total == 56, total
print("apps:", total, "| OSI open source:", oss)
# ---------------------------------------------------------------- the Hungarian page
start = src.index(" <!-- ==================== -->\n <!-- 1.")
end = src.index(" </div>\n </section>\n\n <!-- Stats Section -->")
hu = src[:start] + sections("hu") + "\n" + src[end:]
hu = sub1(hu, "<title>Telepíthető alkalmazások — 53 nyílt forráskódú app otthoni szerverre | Felhom.eu</title>",
"<title>Telepíthető alkalmazások — %d app otthoni szerverre | Felhom.eu</title>" % total)
hu = sub1(hu, "Nextcloud, Immich, Jellyfin, Vaultwarden és 53 további nyílt forráskódú alkalmazás otthoni szerveredre.",
"Nextcloud, Immich, Jellyfin, Vaultwarden — összesen %d alkalmazás otthoni szerveredre, szinte mind nyílt forráskódú." % total)
hu = sub1(hu, '<meta property="og:title" content="53 nyílt forráskódú alkalmazás otthoni szerverre">',
'<meta property="og:title" content="%d alkalmazás otthoni szerverre">' % total)
hu = sub1(hu, '<meta name="twitter:title" content="53 nyílt forráskódú alkalmazás otthoni szerverre">',
'<meta name="twitter:title" content="%d alkalmazás otthoni szerverre">' % total)
hu = sub1(hu, '"numberOfItems": 45,', '"numberOfItems": %d,' % total)
hu = sub1(hu, '"description": "Nyílt forráskódú, önhostolt alkalmazások otthoni szerverre",',
'"description": "Önhostolt alkalmazások otthoni szerverre, szinte mind nyílt forráskódú",')
hu = sub1(hu, "<p>Válassz a nyílt forráskódú alkalmazások széles kínálatából. Mindegyik ingyenes, közösség által fejlesztett szoftver, amit a saját szervereden futtathatsz.</p>",
"<p>Válassz %d alkalmazás közül. Szinte mind ingyenes, nyílt forráskódú szoftver, és mind a saját szervereden fut.</p>" % total)
hu = sub1(hu, "<li>Az alkalmazások <strong>nyílt forráskódúak</strong> – bárki ellenőrizheti a működésüket, nincs rejtett funkció</li>",
"<li>Az alkalmazások szinte mind <strong>nyílt forráskódúak</strong> – bárki ellenőrizheti a működésüket. A kivételeket a kártyán jelöljük: <em>Zárt forráskód</em> (Plex, Emby), <em>Korlátozott licenc</em> (a forrás olvasható, de a licenc korlátoz), <em>Felhom saját</em> (a mi fejlesztésünk)</li>")
hu = sub1(hu, "<li>Egyedi igények esetén további alkalmazások is telepíthetők – <a href=\"/#kapcsolat\">kérdezz bátran!</a></li>",
"<li>Ha egy alkalmazás hiányzik, megnézzük, felvehető-e a katalógusba – <a href=\"/kapcsolat\">kérdezz bátran!</a></li>")
hu = sub1(hu, """ <h3>49</h3>
<p>Elérhető alkalmazás</p>""", """ <h3>%d</h3>
<p>Elérhető alkalmazás</p>""" % total)
hu = sub1(hu, """ <h3>100%</h3>
<p>Nyílt forráskódú</p>""", """ <h3>%d</h3>
<p>Nyílt forráskódú</p>""" % oss)
hu = sub1(hu, "<p>Ez csak egy részlet a telepíthető alkalmazásokból. Ha van egy konkrét igényed, keress minket és megtaláljuk a megfelelő megoldást!</p>",
"<p>Ez a teljes katalógus. Ha van egy konkrét igényed, keress minket — megnézzük, felvehető-e a megfelelő alkalmazás.</p>")
hu = set_alternates(hu, "/alkalmazasok", "/en/apps")
hu = set_nav(hu, "hu", "/alkalmazasok", "/en/apps")
write("alkalmazasok.html", hu)
# ---------------------------------------------------------------- the English page
en = src[:start] + sections("en") + "\n" + src[end:]
en = en.replace('<html lang="hu">', '<html lang="en">', 1)
head_old = en[: en.index("<!-- shared assets")]
head_new = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apps to install — %(n)d apps for a home server | Felhom.eu</title>
<meta name="description" content="Nextcloud, Immich, Jellyfin, Vaultwarden — %(n)d apps for your home server in Hungary, almost all of them open source. Alternatives to Google Drive, iCloud Photos and Spotify.">
<meta name="keywords" content="Nextcloud, Immich, Jellyfin, Plex, Sonarr, Radarr, Vaultwarden, open-source apps, self-hosted apps, home server apps, Google Photos alternative, Dropbox alternative">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://felhom.eu/en/apps">
<meta property="og:type" content="website">
<meta property="og:url" content="https://felhom.eu/en/apps">
<meta property="og:title" content="%(n)d apps for a home server">
<meta property="og:description" content="Nextcloud, Immich, Jellyfin, Vaultwarden and many more. Files, photos, media, documents — all from your own server.">
<meta property="og:image" content="https://felhom.eu/assets/og-image.png">
<meta property="og:locale" content="en_GB">
<meta property="og:site_name" content="Felhom.eu">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="%(n)d apps for a home server">
<meta name="twitter:description" content="Nextcloud, Immich, Jellyfin and many more — from your own server.">
<meta name="twitter:image" content="https://felhom.eu/assets/og-image.png">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ItemList",
"name": "Felhom.eu — Apps to install",
"description": "Self-hosted apps for a home server, almost all of them open source",
"numberOfItems": %(n)d,
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Nextcloud", "description": "Files and office suite"},
{"@type": "ListItem", "position": 2, "name": "Immich", "description": "Photos and videos (a Google Photos alternative)"},
{"@type": "ListItem", "position": 3, "name": "Jellyfin", "description": "Media player and server"},
{"@type": "ListItem", "position": 4, "name": "Vaultwarden", "description": "Password manager (Bitwarden compatible)"},
{"@type": "ListItem", "position": 5, "name": "Paperless-ngx", "description": "Document management"}
]
}
</script>
""" % {"n": total}
en = head_new + en[len(head_old):]
en = sub1(en, "<h1>Telepíthető <span>Alkalmazások</span></h1>", "<h1>Apps to <span>install</span></h1>")
en = sub1(en, re.search(r"<p>Válassz a nyílt.*?</p>", en).group(0),
"<p>Choose from %d apps. Almost all of them are free, open-source software, and every one runs on your own server.</p>" % total)
en = sub1(en, re.search(r"<h3>(<svg[^>]*><use[^>]*/></svg>) Fontos tudnivalók</h3>", en).group(0),
re.search(r"<h3>(<svg[^>]*><use[^>]*/></svg>) Fontos tudnivalók</h3>", en).group(0).replace("Fontos tudnivalók", "Good to know"))
ul_old = re.search(r'(<div class="disclaimer-banner">.*?<ul>)(.*?)(</ul>)', en, re.S)
en = en[:ul_old.start(2)] + """
<li>Almost all of the apps are <strong>open source</strong> – anyone can check how they work. The exceptions are marked on the card: <em>Closed source</em> (Plex, Emby), <em>Restricted licence</em> (the source can be read, but the licence limits its use), <em>Felhom's own</em> (built by us)</li>
<li>Our service covers <strong>installing</strong> the apps, not developing or selling them</li>
<li>The original developers are responsible for how the apps work and for their updates</li>
<li>If an app is missing, we look at whether it can join the catalog – <a href="/en/contact">just ask!</a></li>
""" + en[ul_old.end(2):]
stats = [("Elérhető alkalmazás", "Apps available"), ("Kategória", "Categories"), ("Nyílt forráskódú", "Open source"),
("Licenc költség", "Licence cost")]
for a, b in stats:
en = sub1(en, "<p>%s</p>" % a, "<p>%s</p>" % b)
en = sub1(en, """ <h3>49</h3>""", """ <h3>%d</h3>""" % total)
en = sub1(en, """ <h3>100%</h3>""", """ <h3>%d</h3>""" % oss)
en = sub1(en, "<h3>0 Ft</h3>", "<h3>0 Ft</h3>")
en = sub1(en, "<h2>Nem találod amit keresel?</h2>", "<h2>Can't find what you are looking for?</h2>")
en = sub1(en, re.search(r"<p>Ez csak egy részlet.*?</p>", en).group(0),
"<p>This is the whole catalog. If you need something specific, get in touch — we will look at whether the right app can join it.</p>")
en = sub1(en, '<a href="/kapcsolat" class="cta-button">Kapcsolatfelvétel →</a>', '<a href="/en/contact" class="cta-button">Get in touch →</a>')
en = sub1(en, 'aria-label="Bezárás"', 'aria-label="Close"')
en = sub1(en, 'aria-label="Előző"', 'aria-label="Previous"')
en = sub1(en, 'aria-label="Következő"', 'aria-label="Next"')
en = sub1(en, "`Ugrás ${i + 1}. képhez`", "`Go to image ${i + 1}`")
en = sub1(en, "Betöltés...", "Loading...")
en = sub1(en, "'Menü bezárása'", "'Close menu'")
en = sub1(en, "'Menü megnyitása'", "'Open menu'")
en = sub1(en, '<body class="page-alkalmazasok">', '<body class="page-alkalmazasok">')
en = set_alternates(en, "/alkalmazasok", "/en/apps")
en = set_nav(en, "en", "/en/apps", "/alkalmazasok")
en = set_footer(en, "en")
write("en/apps.html", en)
print("written: alkalmazasok.html, en/apps.html")