Lint / Shell (shellcheck) (push) Successful in 18s
Lint / JS (eslint) (push) Successful in 11s
Lint / Landing page is rendered (matrix (push) Successful in 6s
Lint / Python (ruff) (push) Successful in 7s
Lint / Python deps (pip-audit) (push) Successful in 1m5s
Lint / Secret scan (gitleaks) (push) Successful in 10s
- The ~1,800-word "our fork adds" paragraph becomes seven titled groups (Calls & Voice, Messaging, Media & Links, Privacy & Security, Look & Feel, Desktop App, Rooms & Moderation), one short line per feature, with a link to LOTUS_FEATURES.md instead of duplicating the changelog. - The comparison table lives in landing/data/comparison.json (converted losslessly: all 71 rows render with identical cell text) and the feature groups in landing/data/features.json. landing/build.py renders them between <!-- BEGIN/END --> markers; everything else stays hand-edited. - CSS moved to landing/style.css and inlined by build.py (the LXC 139 deploy copies index.html only, so a linked stylesheet wouldn't ship). The fork paragraph's 16 repeated inline styles are gone. - Visible "Client comparison last reviewed <date>" stamp from comparison.json, replacing the stale "June 2026 (latest)" title. - CI: `landing-fresh` fails if index.html doesn't match its sources. Page is ~1,850 px shorter on desktop and ~2,500 px on phones; no horizontal overflow at 1280 px or Pixel 7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
112 lines
4.4 KiB
Python
112 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Render the data-driven sections of landing/index.html.
|
|
|
|
The fork feature list and the client comparison table used to be edited by
|
|
hand in two places per feature (matrix #11). They now live in landing/data/:
|
|
|
|
features.json - the "our fork adds" groups, one short line per feature
|
|
comparison.json - the client comparison table + its "reviewed" date
|
|
|
|
and the stylesheet lives in landing/style.css. It is inlined into index.html
|
|
rather than linked, because the LXC 139 deploy copies index.html only.
|
|
|
|
Run `python3 landing/build.py` after editing either file and commit the
|
|
regenerated index.html (LXC 139 serves the files as-is; there is no build step
|
|
there). `--check` exits 1 if index.html is out of date, for CI.
|
|
|
|
Only the text between `<!-- BEGIN:name -->` and `<!-- END:name -->` markers is
|
|
rewritten; everything else in index.html stays hand-edited.
|
|
"""
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
INDEX = ROOT / "index.html"
|
|
MARK = {"yes": "✓", "no": "✗", "part": "~"}
|
|
|
|
|
|
def render_features(groups):
|
|
out = ['<div class="fork-features">']
|
|
for g in groups:
|
|
out.append(' <div class="fork-group">')
|
|
out.append(f' <h4>{g["title"]}</h4>')
|
|
out.append(" <ul>")
|
|
out.extend(f" <li>{item}</li>" for item in g["items"])
|
|
out.append(" </ul>")
|
|
out.append(" </div>")
|
|
out.append("</div>")
|
|
return "\n".join(out)
|
|
|
|
|
|
def render_cell(cell, ours):
|
|
cls = ' class="ours"' if ours else ""
|
|
if "mark" in cell:
|
|
inner = f'<span class="{cell["mark"]}">{MARK[cell["mark"]]}</span>'
|
|
else:
|
|
inner = cell["text"]
|
|
if cell.get("note"):
|
|
inner += f'<small>{cell["note"]}</small>'
|
|
return f"<td{cls}>{inner}</td>"
|
|
|
|
|
|
def render_comparison(data):
|
|
clients = data["clients"]
|
|
cols = len(clients) + 1
|
|
out = ["<table>", " <thead>", " <tr>", " <th></th>"]
|
|
for i, c in enumerate(clients):
|
|
cls = ' class="ours"' if i == 0 else ""
|
|
sub = f'<small>{c["sub"]}</small>' if c["sub"] else ""
|
|
out.append(f' <th{cls}>{c["name"]}{sub}</th>')
|
|
out += [" </tr>", " </thead>", " <tbody>"]
|
|
for sec in data["sections"]:
|
|
out.append(f' <tr class="section-header"><td colspan="{cols}">{sec["title"]}</td></tr>')
|
|
for row in sec["rows"]:
|
|
if len(row["cells"]) != len(clients):
|
|
sys.exit(f'comparison.json: "{row["feature"]}" has {len(row["cells"])} cells, expected {len(clients)}')
|
|
out.append(" <tr>")
|
|
out.append(f' <td>{row["feature"]}</td>')
|
|
out.extend(f" {render_cell(c, i == 0)}" for i, c in enumerate(row["cells"]))
|
|
out.append(" </tr>")
|
|
out += [" </tbody>", "</table>"]
|
|
return "\n".join(out)
|
|
|
|
|
|
def replace_block(html, name, content):
|
|
pattern = re.compile(
|
|
rf"(?P<indent>[ \t]*)<!-- BEGIN:{name} -->.*?<!-- END:{name} -->", re.S
|
|
)
|
|
m = pattern.search(html)
|
|
if not m:
|
|
sys.exit(f"index.html: missing <!-- BEGIN:{name} --> / <!-- END:{name} --> markers")
|
|
indent = m.group("indent")
|
|
body = "\n".join(indent + line if line else line for line in content.split("\n"))
|
|
block = f"{indent}<!-- BEGIN:{name} -->\n{body}\n{indent}<!-- END:{name} -->"
|
|
return html[: m.start()] + block + html[m.end() :]
|
|
|
|
|
|
def main():
|
|
features = json.loads((ROOT / "data" / "features.json").read_text(encoding="utf-8"))
|
|
comparison = json.loads((ROOT / "data" / "comparison.json").read_text(encoding="utf-8"))
|
|
css = (ROOT / "style.css").read_text(encoding="utf-8").rstrip("\n")
|
|
html = INDEX.read_text(encoding="utf-8")
|
|
styles = "<style>\n" + "\n".join(" " + line if line else line for line in css.split("\n")) + "\n</style>"
|
|
new = replace_block(html, "styles", styles)
|
|
new = replace_block(new, "features", render_features(features))
|
|
new = replace_block(new, "comparison", render_comparison(comparison))
|
|
new = replace_block(
|
|
new, "reviewed", f'Client comparison last reviewed <time datetime="{comparison["reviewed"]}">{comparison["reviewed"]}</time>'
|
|
)
|
|
if "--check" in sys.argv:
|
|
if new != html:
|
|
sys.exit("landing/index.html is out of date: run python3 landing/build.py")
|
|
print("landing/index.html is up to date")
|
|
return
|
|
INDEX.write_text(new, encoding="utf-8")
|
|
print("wrote", INDEX)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|