feat: public sharing links for households
CI / test-and-package (push) Successful in 40s

Add hard-to-guess public URLs (/public/<token>) that show a read-only
meal status dashboard for each household. Only people with the link can
see the data — no login required.

- models.py: public_token + public_enabled columns on households,
  get_household_by_public_token(), generate_public_token(),
  set_public_enabled()
- meals.py: /public/<token> route, /admin/public-link for
  generate/disable/enable/regenerate
- templates/public.html: clean read-only public dashboard
- templates/public-not-found.html: 404 for invalid/disabled links
- templates/dashboard.html: admin UI with copy/regenerate/disable
- static/style.css: public page and URL box styles
- i18n.py: 18 new translation keys (en/fr/de)
This commit is contained in:
agentbox
2026-08-01 10:12:21 +00:00
parent 2a3101071b
commit ae95841b5b
7 changed files with 330 additions and 0 deletions
+62
View File
@@ -30,6 +30,9 @@ from models import (
get_all_households,
get_users_with_ntfy,
update_reminder_sent,
get_household_by_public_token,
generate_public_token,
set_public_enabled,
)
from i18n import t
from ntfy import send_test_notification, send_meal_reminder
@@ -369,6 +372,65 @@ def auto_login(token):
return redirect(target)
# ── Public sharing ───────────────────────────────────────────────────────────
@meals_bp.route("/public/<token>")
def public_dashboard(token):
"""Public, read-only dashboard showing the household's meal status.
No login required — the token is a hard-to-guess secret. Query params:
- date: YYYY-MM-DD (default: today)
"""
household = get_household_by_public_token(token)
if household is None:
return render_template("public-not-found.html"), 404
today = date.today()
date_str = request.args.get("date")
if date_str:
try:
today = datetime.strptime(date_str, "%Y-%m-%d").date()
except ValueError:
pass
data = get_dashboard_data(today, household["id"])
return render_template(
"public.html",
dashboard=data,
household=household,
viewing_date=today,
today=date.today(),
prev_date=today - timedelta(days=1),
next_date=today + timedelta(days=1),
)
@meals_bp.route("/admin/public-link", methods=["POST"])
@login_required
def admin_public_link():
"""Admin enables/disables or regenerates the public sharing link."""
if not session["is_admin"]:
flash(t("Only admins can manage the public link."), "error")
return redirect(url_for("meals.dashboard"))
action = request.form.get("action", "").strip()
household_id = session["household_id"]
if action == "generate":
token = generate_public_token(household_id)
flash(t("Public link generated!"), "success")
elif action == "disable":
set_public_enabled(household_id, False)
flash(t("Public link disabled."), "success")
elif action == "enable":
set_public_enabled(household_id, True)
flash(t("Public link enabled."), "success")
else:
flash(t("Unknown action."), "error")
return redirect(url_for("meals.dashboard"))
# ── Cron tick (called by meal-cron container) ─────────────────────────────────
REMINDER_HOURS = {"lunch": 10, "dinner": 16}