diff --git a/coulomb_social/apps/core/views.py b/coulomb_social/apps/core/views.py index 20b9d08..99b10f9 100644 --- a/coulomb_social/apps/core/views.py +++ b/coulomb_social/apps/core/views.py @@ -2,6 +2,7 @@ from django.contrib.auth.decorators import login_required from django.http import HttpRequest, HttpResponse, HttpResponseForbidden, JsonResponse from django.shortcuts import redirect, render +from coulomb_social.apps.spaces.assets import local_asset_url from coulomb_social.apps.spaces.services import spaces_for_member from .principal import build_principal @@ -23,6 +24,8 @@ def app_home(request: HttpRequest) -> HttpResponse: member = principal.get("member") spaces = list(spaces_for_member(member)) + for space in spaces: + space.visual_url = local_asset_url(space.slug, space.visual) return render( request, "core/app_home.html", diff --git a/coulomb_social/templates/core/app_home.html b/coulomb_social/templates/core/app_home.html index 516d4e5..755ca83 100644 --- a/coulomb_social/templates/core/app_home.html +++ b/coulomb_social/templates/core/app_home.html @@ -17,13 +17,17 @@ {% for space in spaces %}
  • + {% if space.visual_url %} + {{ space.title }} cover + {% endif %} {{ space.title }} · {{ space.slug }} {% if space.description %}

    {{ space.description }}

    {% endif %} - {% if space.visual %} + {% if space.visual and not space.visual_url %}

    Visual: {{ space.visual }}

    {% endif %}
  • diff --git a/docs/usecases/registry.json b/docs/usecases/registry.json index 13e81ea..98b7b87 100644 --- a/docs/usecases/registry.json +++ b/docs/usecases/registry.json @@ -23,7 +23,8 @@ "tests/test_reichelag_workflow.py::test_full_protocol_duplicate_edit_and_clipboard_control", "tests/test_reichelag_workflow.py::test_protocol_naming_rename_and_collision_safety", "tests/test_private_assets.py::test_owner_and_reader_get_exact_private_image", - "tests/test_private_assets.py::test_outsider_foreign_tenant_revoked_archived_and_anonymous_denied" + "tests/test_private_assets.py::test_outsider_foreign_tenant_revoked_archived_and_anonymous_denied", + "tests/test_private_assets.py::test_display_uses_private_images_without_clipboard_changes" ], "browser": [ "tests/usecases/test_reichelag_browser.py::test_weekly_call_through_real_ui" diff --git a/scripts/test-usecase-browser.mjs b/scripts/test-usecase-browser.mjs index b122e19..8b9a787 100644 --- a/scripts/test-usecase-browser.mjs +++ b/scripts/test-usecase-browser.mjs @@ -50,6 +50,7 @@ try { } await login('writer'); assert.match(await page.locator('main').innerText(), new RegExp(scenario.space_title)); + await page.waitForFunction(() => document.querySelector('.space-list .space-cover')?.naturalWidth === 1); await page.locator(`a[href="/app/spaces/${scenario.space_slug}/"]`).click(); await page.getByRole('link', {name: scenario.previous_title, exact: true}).click(); await page.waitForFunction(() => document.querySelector('#page-content img')?.naturalWidth === 1); @@ -124,6 +125,7 @@ try { assert.match(await page.locator('#page-content').innerText(), /Completed point/); assert.doesNotMatch(await page.locator('#page-content').innerText(), /New notes/); await login('reader'); + await page.waitForFunction(() => document.querySelector('.space-list .space-cover')?.naturalWidth === 1); await page.goto(`${detail}?page=${scenario.next_slug}`); assert.match(await page.locator('#page-content').innerText(), /New notes/); await page.waitForFunction(() => document.querySelector('#page-content img')?.naturalWidth === 1); @@ -143,6 +145,7 @@ try { const denied = await page.goto(`${detail}pages/${scenario.next_slug}/edit/`); assert.equal(denied.status(), 403); await login('outsider'); + assert.equal(await page.locator('.space-list .space-cover').count(), 0); const forbidden = await page.goto(`${detail}?page=${scenario.next_slug}`); assert.equal(forbidden.status(), 404); assert.equal(await page.locator('#page-content').count(), 0); diff --git a/tests/test_private_assets.py b/tests/test_private_assets.py index 4e7be10..7cc36ec 100644 --- a/tests/test_private_assets.py +++ b/tests/test_private_assets.py @@ -135,6 +135,7 @@ def test_nested_raster_allowed_but_html_disguised_as_png_denied(images, client): @pytest.mark.django_db +@pytest.mark.usecase("CSOC-UC-008") def test_display_uses_private_images_without_clipboard_changes(images, client): space, owner, _, _ = images body = '![Body visual](../assets/cover.png)\n\n[Open visual](assets/cover.png)\n' @@ -149,6 +150,14 @@ def test_display_uses_private_images_without_clipboard_changes(images, client): response = client.get(reverse("spaces:detail", kwargs={"slug": space.slug}) + "?page=notes") assert b'alt="Pictures cover"' in response.content assert b'alt="Notes visual"' in response.content + response = client.get(reverse("core:app_home")) + assert b'class="space-cover"' in response.content + assert f'src="{image_url(space)}"'.encode() in response.content + assert b"Visual: assets/cover.png" not in response.content + client.force_login(images[3].user) + response = client.get(reverse("core:app_home")) + assert b'class="space-cover"' not in response.content + assert image_url(space).encode() not in response.content @pytest.mark.parametrize("reference", ["https://remote.test/image.png", "//remote.test/a.png", diff --git a/tests/usecases/test_reichelag_browser.py b/tests/usecases/test_reichelag_browser.py index fd204ec..832e338 100644 --- a/tests/usecases/test_reichelag_browser.py +++ b/tests/usecases/test_reichelag_browser.py @@ -36,7 +36,7 @@ def test_weekly_call_through_real_ui(tmp_path, settings, request): user_engine_user_id=subject, issuer="https://local.dev/issuer", subject=subject, ) space = create_space(actors["writer"], title=scenario["space_title"], - slug=scenario["space_slug"]) + slug=scenario["space_slug"], visual=scenario["visual"]) pageops.assets_dir(space.slug).joinpath("cover.png").write_bytes( base64.b64decode(scenario["visual_base64"]) ) diff --git a/workplans/CSOC-WP-0008-production-transfer-acceptance.md b/workplans/CSOC-WP-0008-production-transfer-acceptance.md index 2a92362..463d717 100644 --- a/workplans/CSOC-WP-0008-production-transfer-acceptance.md +++ b/workplans/CSOC-WP-0008-production-transfer-acceptance.md @@ -585,3 +585,13 @@ revision/concurrency, cross-space body/incoming links and broader fidelity remai T09 still awaits founder Visual and complete workflow acceptance; user asked to refresh ReichelAG. No task/workplan completion or Bubble retirement inferred. Receipt: docs/release/private-visuals-2026-10-07.md. + +## User Visual review: missing Spaces-list preview — 2026-10-07 + +Founder confirms the Visual appears on the page, but not in Spaces-list entries. +T05/T09 retain this concrete review gap. Add the space cover to its existing +list entry using the same private route and membership-filtered query. Extend +UC-008 fast and real-browser checks for writer/viewer list-preview loading and +outsider exclusion. Preserve page/clipboard/content/access behavior. This is a +correction within the approved private-Visual image-only upgrade scope, not +acceptance of the complete use case. No wider UI framework work included.