Hero text shadow and an owner Add screenshots flow in the More menu

The hero overlay title, tagline and author row get a dark text shadow
so they stay readable over bright cover art. The owner's More menu
gains Add screenshots: a modal with the shared attachment uploader
whose POST /projects/{slug}/screenshots (ProjectScreenshotsForm,
owner-only, audit project.screenshots.add, catalogued in events.md)
links the uploaded uids through the same attachments.link_attachments
choke point as create/edit and returns to #screenshots. Documented as
projects-screenshots in the API docs, exposed to Devii as
project_add_screenshots (upload via upload_file/attach_url first), and
covered by an api test for the owner flow, the gallery render, and the
non-owner 403.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-09 22:27:27 +02:00
co-authored by Claude Fable 5
parent 530ae2b5bb
commit 8f9f461244
10 changed files with 155 additions and 4 deletions
+41
View File
@@ -397,6 +397,47 @@ def test_owner_uploaded_cover_and_logo_render_in_hero(app_server):
assert '"thumbnailUrl"' in html, "Expected the logo as schema thumbnailUrl"
def test_owner_adds_screenshots_from_the_page(app_server):
"""POST /projects/{slug}/screenshots links uploaded images into the gallery; non-owners are refused."""
import io
from PIL import Image
session, _ = _member()
project = _create_project(session)
slug = project["slug"] or project["uid"]
html = session.get(f"{BASE_URL}/projects/{slug}").text
assert "project-screenshot-grid" not in html
buf = io.BytesIO()
Image.new("RGB", (10, 10), (5, 120, 60)).save(buf, "PNG")
r = session.post(
f"{BASE_URL}/uploads/upload",
files={"file": ("gallery.png", buf.getvalue(), "image/png")},
)
assert r.status_code == 201, r.text[:300]
uid = r.json()["uid"]
r = session.post(
f"{BASE_URL}/projects/{slug}/screenshots",
headers=JSON,
data={"attachment_uids": uid},
)
assert r.status_code == 200, r.text[:300]
assert r.json()["data"]["linked"] == 1
html = session.get(f"{BASE_URL}/projects/{slug}").text
assert "project-screenshot-grid" in html, "Expected the gallery after linking"
intruder, _ = _member()
r = intruder.post(
f"{BASE_URL}/projects/{slug}/screenshots",
headers=JSON,
data={"attachment_uids": uid},
)
assert r.status_code == 403, r.text[:300]
def test_project_page_json_ld_rating_from_stars(app_server):
"""Stars surface as an aggregateRating; zero stars emit none."""
session, _ = _member()