67b3fd23d1
gates / gates (push) Failing after 13m1s
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
242 lines
16 KiB
Python
242 lines
16 KiB
Python
# -*- 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")
|