fix(web-app-socialhome): rename the household to the inventory title and back

SH_INSTANCE_NAME names the household on the first boot only, and
GET /api/instance/config prefers the stored identity row, so a design
title set later never reached the household. Upstream renames that row
through PATCH /api/admin/instance (admin only, re-broadcast to paired
households); the earlier TODO claimed no such route exists, which was
wrong.

The design tasks now read the current name and converge it with the
same semantics as Decidim: household-name.json holds the name the role
applied and the one before it. The role renames while the current name
is its own, restores the previous name once the title is gone, and
leaves a name an administrator chose in the app alone. A fresh install
already carries the title from its first boot, so the role records it
without a rename; otherwise a later administrator rename would be
overwritten on the next deploy.

The token, theme and rename tasks moved into tasks/02_design/converge.yml
behind one include, because a block may hold at most three tasks.

The design spec now checks the instance name and the feed title; the
title assertion alone passed with a stale household name, because the
design script only replaces the brand suffix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-16 22:56:58 +02:00
1 parent bb4646cfc0
commit 92c1f78771
6 files changed
+111 -42

No files matched your search

-1
View File
@@ -40,7 +40,6 @@ Open items that only a live run can settle:
## Open from the design pass
- Title override: `SH_INSTANCE_NAME` reaches the home name only on the first boot, because `ensure_instance_identity` (upstream `socialhome/identity_bootstrap.py`) writes `display_name` once and `GET /api/instance/config` prefers that row; an inventory title set later changes the env but not the shown name. No route writes that row at 2026.10.8 (measured: `routes/instance.py` only reads `federation_repo.get_local_identity()`, and no `set`/`update` of the local identity exists in the package), and the row is the identity peers receive. Options: ask upstream for an admin route that renames the household, or accept first-boot only.
- Shared sign-in budget: `POST /api/auth/token` allows 5 attempts per 15 minutes per `request.remote`, and the app ignores `X-Forwarded-For`, so every browser behind the front proxy and the deploy's own token request share one bucket (measured: all logins in `/api/admin/auth-audit` come from `192.168.75.1`). At 2026.10.11 the budget is the module constant `AUTH_TOKEN_RATE_LIMIT` in `socialhome/routes/users.py` with no env or TOML key, and `trusted_proxies` exists for the GFS only (`socialhome/global_server/config.py`); the design spec keeps one administrator token in the Playwright staging volume. Options: let the proxy reach the app with the client address (PROXY protocol or upstream trusted-proxy support), or ask upstream to honour a trusted forwarded header.
- `tasks/01_setup.yml` comment is stale: at 2026.10.8 the headless seed marks setup complete itself (upstream `platform/standalone/adapter.py`), so the task always answers 409 (measured: `ok`, never `changed`). Options: drop the task or rewrite the comment.
- `meta/csp.yml` keeps `script-src-elem unsafe-inline` for an inline theme script the 2026.10.8 shell no longer has (it loads `assets/theme-boot.js`; measured: the served `/feed` carries no inline script of its own). Options: drop the flag after checking `hlth-csp`; the injected design script then needs its CSP hash, which `sys-front-inj-design` adds.
@@ -569,6 +569,12 @@ exports.register = function () {
}
await expect(page.locator("link[rel~='icon']").first()).toHaveAttribute("href", faviconUrl);
}
if (title) {
const config = await apiGetOnion(page.request, `${base}/api/instance/config`);
expect((await config.json()).instance_name, "the household carries the configured title").toBe(title);
await visit(page, `${base}/feed`);
await expect(page.locator(TOPBAR_TITLE), "the feed is named after the household").toHaveText(title);
}
});
test("design: the sign-in page shows the generated logo", async ({ page }) => {
+28 -40
View File
@@ -5,48 +5,20 @@
checksum_algorithm: sha1
register: socialhome_theme_marker
- name: "🎨 Converge the household primary colour"
when: SOCIALHOME_DESIGN_ENABLED | bool or socialhome_theme_marker.stat.exists
block:
- name: "🔑 Request an administrator token for the household theme"
uri_retry:
url: "{{ SOCIALHOME_INTERNAL_URL }}/api/auth/token"
method: POST
body_format: json
body:
username: "{{ SOCIALHOME_ADMIN_USERNAME }}"
password: "{{ SOCIALHOME_ADMIN_PASSWORD }}"
status_code: 200
register: socialhome_theme_token
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "📥 Look up the household name the role wrote last"
ansible.builtin.stat:
path: "{{ SOCIALHOME_NAME_STATE_FILE }}"
register: socialhome_name_marker
- name: "🎨 Read the household theme"
uri_retry:
url: "{{ SOCIALHOME_INTERNAL_URL }}/api/theme"
method: GET
headers:
Authorization: "Bearer {{ socialhome_theme_token.json.token }}"
status_code: 200
register: socialhome_theme
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "📥 Read the household name the role wrote last"
ansible.builtin.slurp:
src: "{{ SOCIALHOME_NAME_STATE_FILE }}"
register: socialhome_name_state_file
when: socialhome_name_marker.stat.exists
- name: "🎨 Write the household primary colour"
uri_retry:
url: "{{ SOCIALHOME_INTERNAL_URL }}/api/theme"
method: PUT
body_format: json
body:
primary_color: "{{ SOCIALHOME_THEME_PRIMARY }}"
headers:
Authorization: "Bearer {{ socialhome_theme_token.json.token }}"
status_code: 200
register: socialhome_theme_written
changed_when: true
when:
- socialhome_theme.json.primary_color | lower != SOCIALHOME_THEME_PRIMARY
- socialhome_theme.json.primary_color | lower == SOCIALHOME_BRAND_PRIMARY or (socialhome_theme_marker.stat.exists and
socialhome_theme_marker.stat.checksum == (socialhome_theme.json.primary_color | lower | hash('sha1')))
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🎨 Converge the household primary colour and name"
include_tasks: 02_design/converge.yml
when: SOCIALHOME_DESIGN_ENABLED | bool or socialhome_theme_marker.stat.exists or socialhome_name_marker.stat.exists
- name: "🏷️ Mark the household primary colour as the role's"
stack_host_copy:
@@ -57,8 +29,24 @@
- SOCIALHOME_DESIGN_ENABLED | bool
- socialhome_theme_written is changed
- name: "🏷️ Mark the household name as the role's"
stack_host_copy:
content: "{{ {'applied': SOCIALHOME_DESIGN_TITLE, 'previous': (socialhome_name_state.previous if socialhome_name_state | length > 0 else socialhome_name_current)}
| to_json }}"
dest: "{{ SOCIALHOME_NAME_STATE_FILE }}"
mode: "0644"
when:
- SOCIALHOME_DESIGN_TITLE | length > 0
- socialhome_name_written is changed or (socialhome_name_state | length == 0 and socialhome_name_current == SOCIALHOME_DESIGN_TITLE)
- name: "🧹 Drop the household theme marker"
ansible.builtin.file:
path: "{{ SOCIALHOME_THEME_MARKER_FILE }}"
state: absent
when: not (SOCIALHOME_DESIGN_ENABLED | bool)
- name: "🧹 Drop the household name marker"
ansible.builtin.file:
path: "{{ SOCIALHOME_NAME_STATE_FILE }}"
state: absent
when: SOCIALHOME_DESIGN_TITLE | length == 0
@@ -0,0 +1,73 @@
---
- name: "🔑 Request an administrator token for the household theme"
uri_retry:
url: "{{ SOCIALHOME_INTERNAL_URL }}/api/auth/token"
method: POST
body_format: json
body:
username: "{{ SOCIALHOME_ADMIN_USERNAME }}"
password: "{{ SOCIALHOME_ADMIN_PASSWORD }}"
status_code: 200
register: socialhome_theme_token
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🎨 Read the household theme"
uri_retry:
url: "{{ SOCIALHOME_INTERNAL_URL }}/api/theme"
method: GET
headers:
Authorization: "Bearer {{ socialhome_theme_token.json.token }}"
status_code: 200
register: socialhome_theme
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🎨 Write the household primary colour"
uri_retry:
url: "{{ SOCIALHOME_INTERNAL_URL }}/api/theme"
method: PUT
body_format: json
body:
primary_color: "{{ SOCIALHOME_THEME_PRIMARY }}"
headers:
Authorization: "Bearer {{ socialhome_theme_token.json.token }}"
status_code: 200
register: socialhome_theme_written
changed_when: true
when:
- socialhome_theme.json.primary_color | lower != SOCIALHOME_THEME_PRIMARY
- socialhome_theme.json.primary_color | lower == SOCIALHOME_BRAND_PRIMARY or (socialhome_theme_marker.stat.exists and socialhome_theme_marker.stat.checksum
== (socialhome_theme.json.primary_color | lower | hash('sha1')))
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🏷️ Read the household name"
uri_retry:
url: "{{ SOCIALHOME_INTERNAL_URL }}/api/instance/config"
method: GET
status_code: 200
register: socialhome_instance_config
- name: "🏷️ Hold the household name and the name the role wrote last"
ansible.builtin.set_fact:
socialhome_name_current: "{{ socialhome_instance_config.json.instance_name }}"
socialhome_name_state: "{{ (socialhome_name_state_file.content | b64decode | from_json) if socialhome_name_marker.stat.exists else {} }}"
- name: "🏷️ Rename the household"
uri_retry:
url: "{{ SOCIALHOME_INTERNAL_URL }}/api/admin/instance"
method: PATCH
body_format: json
body:
display_name: "{{ socialhome_name_target }}"
headers:
Authorization: "Bearer {{ socialhome_theme_token.json.token }}"
status_code: 200
vars:
socialhome_name_ours: "{{ socialhome_name_state | length > 0 and socialhome_name_current == socialhome_name_state.applied }}"
socialhome_name_target: >-
{{ (socialhome_name_state.previous if socialhome_name_ours | bool else '') if SOCIALHOME_DESIGN_TITLE | length == 0
else ('' if (socialhome_name_state | length > 0 and not (socialhome_name_ours | bool)) or socialhome_name_current == SOCIALHOME_DESIGN_TITLE
else SOCIALHOME_DESIGN_TITLE) }}
register: socialhome_name_written
changed_when: true
when: socialhome_name_target | length > 0
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
+1 -1
View File
@@ -22,5 +22,5 @@
- name: "🔐 Complete the standalone first-boot setup for '{{ application_id }}'"
include_tasks: 01_setup.yml
- name: "🎨 Converge the household theme for '{{ application_id }}'"
- name: "🎨 Converge the household theme and name for '{{ application_id }}'"
include_tasks: 02_design.yml
+3
View File
@@ -16,6 +16,9 @@ SOCIALHOME_CONFIG_HOST_FILE: "{{ [lookup('container', application_id, 'directori
SOCIALHOME_MAX_STORAGE_BYTES: 4294967296
SOCIALHOME_DESIGN_ENABLED: "{{ lookup('config', application_id, 'services.design.enabled') | bool }}"
SOCIALHOME_DESIGN_TITLE: "{{ lookup('design', application_id).title if SOCIALHOME_DESIGN_ENABLED | bool and lookup('design', application_id).title is string else
'' }}"
SOCIALHOME_NAME_STATE_FILE: "{{ [lookup('container', application_id, 'directories.volumes'), 'household-name.json'] | path_join }}"
SOCIALHOME_BRAND_PRIMARY: "#d2542a"
SOCIALHOME_THEME_PRIMARY: >-
{{ (lookup('design_palette', lookup('config', 'web-svc-design', 'services.design.colors.base')).light['--design-primary'] | lower)