Skip to content

Commit ace62a8

Browse files
committed
selected-users sidebar mode and panel language override
1 parent a3ab890 commit ace62a8

11 files changed

Lines changed: 425 additions & 58 deletions

File tree

CHANGELOG.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
11
# Changelog
22

3+
## [0.2.19-beta2] — 2026-05-30
4+
5+
Beta release. Two additions: a new 'Selected users' sidebar visibility mode, and a panel UI language override.
6+
7+
**Sidebar panel visibility**
8+
9+
- New `selected_users` mode. The Sidebar panel dropdown now reads: Admins only / Selected users / Everyone / Hidden.
10+
- New `Selected users for sidebar` multi-select field in the install + reconfigure flows. Picks which HA users can access the panel when the mode is `selected_users`. Owner is implicitly allowed (pre-checked + stripped on save).
11+
- Implementation: HA's `frontend.async_register_built_in_panel` only takes `require_admin: bool` — no per-user allowlist at the platform layer. In `selected_users` mode, the panel registers with `require_admin=False` and the panel.js side reads the allowlist from the registration config and gates the body client-side. The sidebar entry is visible to all users in this mode; non-allowed users see an access-denied placeholder when they click through.
12+
- `CONF_PANEL_SELECTED_USERS` is added to `_RELOAD_REQUIRING_KEYS` so changes take effect on save.
13+
14+
**Panel UI language**
15+
16+
- New `Panel language` field in the install + reconfigure flows. Options: `auto` (default, follows each user's HA language), `en`, `sv`.
17+
- `panel.js` resolves the effective language on render and looks up its hardcoded strings against a translation table. The most-visible strings are covered in this beta: bulk action buttons (Take all / Take due / Take missed), Saving spinner, modal section titles (Identity / Notes / Codes / Visibility / Prescriptions), modal buttons (Save / Cancel / Delete / + Add prescription), Add medicine label, Undo, loading/empty/access-denied placeholders. Status badges, schedule descriptions, and detailed form hints are not yet translated and stay English regardless of language setting — they remain a TODO for the next beta.
18+
- HA-managed translations (the config flow forms) follow each user's HA language natively and are unaffected by this setting.
19+
320
## [0.2.19-beta1] — 2026-05-30
421

5-
Beta release. Access control: per-medicine visibility.
22+
Beta release. Per-medicine visibility.
623

724
- New per-medicine `visibility` setting with four modes: `everyone` (default), `linked_person`, `admins_only`, `specific_users`. Owner and managers always pass regardless of mode — visibility is the gate for everyone else.
825
- Visibility section added to the panel's Add and Edit medicine modal. When `specific_users` is selected, a multi-select of HA users (lazy-fetched via the new `pillpilot/get_users` WS endpoint) appears with the owner pre-checked as an informational hint.
926
- Panel filters its rendering through `_canSeeMedicine`. The `_getMedicines` cache key now includes the current user_id so the filter result invalidates when sessions change.
1027
- WS mutating commands (`update_medicine`, `delete_medicine`) carry a new `@_require_can_see_medicine` decorator stacked after `@_require_manager`. Both must pass: a manager who can't see the medicine cannot edit it either.
1128
- Backend strips owner_id from `visibility_users` on save (v0.2.18 pattern applied here too). Owner remains implicitly allowed via `user.is_owner`.
12-
- Sensors stay globally readable — HA's entity registry permissions are admin / non-admin only, so per-user filtering is panel-level. Sensor gating for hard privacy is Tier 3, deferred.
29+
- Sensors stay globally readable — HA's entity registry permissions are admin / non-admin only, so per-user filtering is panel-level. Sensor gating for hard privacy is deferred.
1330
- HA Settings reconfigure flow does not yet expose visibility — panel-only for this beta. Pre-v0.2.19 medicines keep `everyone` as their effective mode.
1431
- New constants in `const.py`: `CONF_MED_VISIBILITY`, `CONF_MED_VISIBILITY_USERS`, `VIS_EVERYONE`, `VIS_LINKED_PERSON`, `VIS_ADMINS_ONLY`, `VIS_SPECIFIC_USERS`, `DEFAULT_MED_VISIBILITY`, `MED_VISIBILITY_OPTIONS`.
1532

RELEASE_NOTES.md

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,34 @@
1-
# v0.2.19-beta1
1+
# v0.2.19-beta2
22

3-
Beta release. Access control: per-medicine visibility.
3+
Beta release. Two additions: a fourth sidebar visibility mode and a panel UI language override.
44

5-
## What's new
5+
## Sidebar panel visibility
66

7-
Each medicine now has a **Visibility** setting in the panel's Add and Edit modal. Four modes:
7+
The Sidebar panel dropdown in Settings → Devices & services → PillPilot → Configure now reads:
88

9-
- **Everyone**default, backward-compat with pre-v0.2.19 medicines. Anyone with panel access can see and (if a manager) edit.
10-
- **Linked person only** — only the HA users whose person entity is the `person_id` of any prescription on this medicine, plus owner and managers.
11-
- **Admins only**only owner, managers, and HA admins.
12-
- **Specific users**explicit allowlist. A multi-select of HA users appears beneath the mode dropdown; pick the ones who should see the medicine. Owner is pre-checked as an informational hint (owner always has access regardless) and stripped from the stored list on save.
9+
- **Admins only**only HA admins see the sidebar entry.
10+
- **Selected users** — only HA users on the new allowlist see the useful panel content. Owner always has access.
11+
- **Everyone**anyone with PillPilot access sees the sidebar entry.
12+
- **Hidden**panel not registered at all.
1313

14-
Owner and managers always have access regardless of mode — visibility is the gate for everyone else.
14+
When `Selected users` is picked, a multi-select user list appears under the dropdown. Add the users you want to grant access to. The owner is pre-checked as an informational hint (owner always has access regardless) and stripped from the stored list on save.
1515

16-
Mutating WS commands (`update_medicine`, `delete_medicine`) now check both manager status and visibility. A manager who can't see a medicine cannot edit it via the WS either.
16+
**Platform limitation worth noting.** Home Assistant's panel registration API does not support per-user allowlists at the frontend layer — a panel is either registered (visible to everyone or admins-only) or not registered at all. The `Selected users` mode works around this by registering the panel for everyone and gating the panel body client-side in `panel.js`. The sidebar entry itself is visible to all users in this mode; clicking it as a non-allowed user shows an access-denied placeholder instead of the medicines view. The mutating WS commands (`create_medicine`, `update_medicine`, `delete_medicine`) remain gated server-side and aren't affected by this limitation.
1717

18-
## Known limitations
18+
## Panel UI language
1919

20-
Sensors stay globally readable. Any HA user with access to Developer Tools can still read the sensor state of a restricted medicine. That's a Home Assistant platform limitation — entity registry permissions are admin / non-admin only, no per-user filtering. Tier 2 stops at panel-level visibility. Sensor gating (skip creating sensors for restricted medicines) is Tier 3 and deferred.
20+
New **Panel language** field in the Configure form. Three options:
2121

22-
HA Settings reconfigure flow does not yet expose visibility. Panel-only for this beta. Visibility can only be set via the Add / Edit medicine modal in the panel.
22+
- **Auto** (default) — follows each user's HA language at render time.
23+
- **English** — force English regardless of HA locale.
24+
- **Svenska** — force Swedish regardless of HA locale.
25+
26+
The most-visible panel strings are translated in this beta: bulk action buttons (Take all / Take due / Take missed), Saving spinner, modal section titles, modal buttons (Save / Cancel / Delete / Add prescription), and loading / empty / access-denied placeholders. Status badges (Due / Taken / Missed / etc), schedule descriptions, and detailed form hints are not yet translated and stay English for now — TODO for the next beta.
27+
28+
The Configure form translations (the labels for each field) follow HA's normal translation system and respect each user's HA locale automatically.
2329

2430
## Upgrading
2531

26-
Replace the `pillpilot` directory in `custom_components/` with the contents of this zip and restart Home Assistant. HACS users on the beta channel: update normally. Hard-reload the panel (Ctrl+Shift+R) — HACS doesn't bust the browser cache of `panel.js`.
32+
Replace the `pillpilot` directory in `custom_components/` with the contents of this zip and restart Home Assistant. Hard-reload the panel (Ctrl+Shift+R) — the `?v=...` query string on the panel.js URL should bust the cache, but a hard reload guarantees it.
2733

28-
Pre-v0.2.19 medicines keep their current behavior — `everyone` is the effective mode until you edit a medicine and pick a different one.
34+
No data migration. Existing installs keep their current sidebar visibility setting and default to `Auto` language. Pre-v0.2.19 medicines keep `Everyone` visibility.

custom_components/pillpilot/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
from homeassistant.helpers.aiohttp_client import async_get_clientsession
1616

1717
from .const import (
18+
CONF_LANGUAGE,
1819
CONF_MANAGERS,
1920
CONF_MED_ATC_CODE,
2021
CONF_MED_PERSON,
2122
CONF_MED_VISIBILITY,
2223
CONF_MED_VISIBILITY_USERS,
24+
CONF_PANEL_SELECTED_USERS,
2325
CONF_MED_ID,
2426
CONF_MED_NAME,
2527
CONF_MED_NPL_ID,
@@ -388,7 +390,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
388390
# panel, etc.). Everything else can be picked up by the coordinator
389391
# without unloading. v0.2.18 adds CONF_MANAGERS — switching the
390392
# allowlist needs the WS handler's resolved entry to be re-read.
391-
_RELOAD_REQUIRING_KEYS = (CONF_PANEL_VISIBILITY, CONF_MANAGERS)
393+
_RELOAD_REQUIRING_KEYS = (
394+
CONF_PANEL_VISIBILITY,
395+
CONF_PANEL_SELECTED_USERS,
396+
CONF_MANAGERS,
397+
CONF_LANGUAGE,
398+
)
392399

393400

394401
def _reload_keys_snapshot(entry: ConfigEntry) -> tuple:

custom_components/pillpilot/config_flow.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,12 @@
8181
CONF_MED_VARUNUMMER,
8282
CONF_MEDICINES_DB_REFRESH_NOW,
8383
CONF_MEDICINES_DB_URL,
84+
CONF_LANGUAGE,
8485
CONF_MANAGERS,
86+
CONF_PANEL_SELECTED_USERS,
8587
CONF_PANEL_VISIBILITY,
88+
DEFAULT_LANGUAGE,
89+
LANGUAGE_OPTIONS,
8690
DEFAULT_PANEL_VISIBILITY,
8791
DEFAULT_REMIND_WINDOW,
8892
DOMAIN,
@@ -1142,6 +1146,14 @@ async def async_step_user(
11421146
if owner_id and owner_id in managers_selected:
11431147
managers_selected = [u for u in managers_selected if u != owner_id]
11441148
self._draft[CONF_MANAGERS] = managers_selected
1149+
# Panel-side selected users — same owner-strip pattern.
1150+
panel_users = list(user_input.get(CONF_PANEL_SELECTED_USERS) or [])
1151+
if owner_id and owner_id in panel_users:
1152+
panel_users = [u for u in panel_users if u != owner_id]
1153+
self._draft[CONF_PANEL_SELECTED_USERS] = panel_users
1154+
self._draft[CONF_LANGUAGE] = (
1155+
user_input.get(CONF_LANGUAGE) or DEFAULT_LANGUAGE
1156+
)
11451157
self._draft[CONF_MEDICINES_DB_URL] = (
11461158
user_input.get(CONF_MEDICINES_DB_URL) or DEFAULT_MEDICINES_DB_URL
11471159
)
@@ -1172,6 +1184,26 @@ async def async_step_user(
11721184
mode=SelectSelectorMode.LIST,
11731185
)
11741186
),
1187+
vol.Optional(
1188+
CONF_PANEL_SELECTED_USERS,
1189+
default=default_managers,
1190+
): SelectSelector(
1191+
SelectSelectorConfig(
1192+
options=manager_options,
1193+
multiple=True,
1194+
mode=SelectSelectorMode.LIST,
1195+
)
1196+
),
1197+
vol.Required(
1198+
CONF_LANGUAGE,
1199+
default=DEFAULT_LANGUAGE,
1200+
): SelectSelector(
1201+
SelectSelectorConfig(
1202+
options=LANGUAGE_OPTIONS,
1203+
translation_key="language",
1204+
mode=SelectSelectorMode.DROPDOWN,
1205+
)
1206+
),
11751207
vol.Optional(
11761208
CONF_MEDICINES_DB_URL,
11771209
default=DEFAULT_MEDICINES_DB_URL,
@@ -1224,6 +1256,13 @@ async def async_step_reconfigure(
12241256
if owner_id and owner_id in managers_selected:
12251257
managers_selected = [u for u in managers_selected if u != owner_id]
12261258
self._draft[CONF_MANAGERS] = managers_selected
1259+
panel_users = list(user_input.get(CONF_PANEL_SELECTED_USERS) or [])
1260+
if owner_id and owner_id in panel_users:
1261+
panel_users = [u for u in panel_users if u != owner_id]
1262+
self._draft[CONF_PANEL_SELECTED_USERS] = panel_users
1263+
self._draft[CONF_LANGUAGE] = (
1264+
user_input.get(CONF_LANGUAGE) or DEFAULT_LANGUAGE
1265+
)
12271266
self._draft[CONF_MEDICINES_DB_URL] = (
12281267
user_input.get(CONF_MEDICINES_DB_URL) or DEFAULT_MEDICINES_DB_URL
12291268
)
@@ -1243,6 +1282,13 @@ async def async_step_reconfigure(
12431282
default_managers = list(stored_managers)
12441283
if owner_id and owner_id not in default_managers:
12451284
default_managers.insert(0, owner_id)
1285+
# Same pattern for the panel selected-users list.
1286+
stored_panel_users = list(
1287+
existing.data.get(CONF_PANEL_SELECTED_USERS) or []
1288+
)
1289+
default_panel_users = list(stored_panel_users)
1290+
if owner_id and owner_id not in default_panel_users:
1291+
default_panel_users.insert(0, owner_id)
12461292
return self.async_show_form(
12471293
step_id="reconfigure",
12481294
description_placeholders={
@@ -1277,6 +1323,28 @@ async def async_step_reconfigure(
12771323
mode=SelectSelectorMode.LIST,
12781324
)
12791325
),
1326+
vol.Optional(
1327+
CONF_PANEL_SELECTED_USERS,
1328+
default=default_panel_users,
1329+
): SelectSelector(
1330+
SelectSelectorConfig(
1331+
options=manager_options,
1332+
multiple=True,
1333+
mode=SelectSelectorMode.LIST,
1334+
)
1335+
),
1336+
vol.Required(
1337+
CONF_LANGUAGE,
1338+
default=existing.data.get(
1339+
CONF_LANGUAGE, DEFAULT_LANGUAGE
1340+
),
1341+
): SelectSelector(
1342+
SelectSelectorConfig(
1343+
options=LANGUAGE_OPTIONS,
1344+
translation_key="language",
1345+
mode=SelectSelectorMode.DROPDOWN,
1346+
)
1347+
),
12801348
vol.Optional(
12811349
CONF_MEDICINES_DB_URL,
12821350
default=existing.data.get(

custom_components/pillpilot/const.py

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,20 +163,29 @@
163163
STATE_SNOOZED = "snoozed"
164164

165165
# ---------------------------------------------------------------------------
166-
# Sidebar panel visibility (added in v0.2.3)
166+
# Sidebar panel visibility (added in v0.2.3, expanded in v0.2.19)
167167
#
168168
# Stored in entry.data[CONF_PANEL_VISIBILITY]. Defaults to "everyone" for
169-
# entries that pre-date this feature, so existing v0.2.1/v0.2.2 installs
170-
# keep the same behavior they had before.
169+
# entries that pre-date this feature, so older installs keep their behavior.
170+
#
171+
# HA's frontend.async_register_built_in_panel takes require_admin: bool
172+
# only — there's no per-user panel registration. The "selected_users" mode
173+
# works around this by registering the panel for everyone and gating the
174+
# panel body client-side in panel.js. The sidebar entry is still visible
175+
# to all users in that mode; clicking it as a non-allowed user shows an
176+
# access-denied placeholder instead of the medicines view.
171177
# ---------------------------------------------------------------------------
172178
CONF_PANEL_VISIBILITY = "panel_visibility"
173-
PANEL_VIS_EVERYONE = "everyone" # registered, require_admin=False
174-
PANEL_VIS_ADMINS = "admins" # registered, require_admin=True
175-
PANEL_VIS_HIDDEN = "hidden" # not registered at all
179+
CONF_PANEL_SELECTED_USERS = "panel_selected_users"
180+
PANEL_VIS_EVERYONE = "everyone" # registered, require_admin=False
181+
PANEL_VIS_ADMINS = "admins" # registered, require_admin=True
182+
PANEL_VIS_SELECTED_USERS = "selected_users" # registered for everyone; panel.js gates
183+
PANEL_VIS_HIDDEN = "hidden" # not registered at all
176184
DEFAULT_PANEL_VISIBILITY = PANEL_VIS_EVERYONE
177185
PANEL_VISIBILITY_OPTIONS = [
178-
PANEL_VIS_EVERYONE,
179186
PANEL_VIS_ADMINS,
187+
PANEL_VIS_SELECTED_USERS,
188+
PANEL_VIS_EVERYONE,
180189
PANEL_VIS_HIDDEN,
181190
]
182191

@@ -201,7 +210,9 @@
201210
# medicine in the panel and who can mutate it via the WS commands.
202211
# Sensors stay global — HA's entity-registry permissions are admin/non-admin
203212
# only, so a non-allowed user can still read sensor state via Developer
204-
# Tools. Tier 2 stops at panel-level visibility; sensor gating is Tier 3.
213+
# Tools. This setting is panel-level only; entity-level gating would
214+
# require skipping sensor creation for restricted medicines and is not
215+
# part of this release.
205216
#
206217
# Modes:
207218
# * everyone — default. Backward-compat for pre-v0.2.19 entries
@@ -227,3 +238,22 @@
227238
VIS_ADMINS_ONLY,
228239
VIS_SPECIFIC_USERS,
229240
]
241+
242+
# ---------------------------------------------------------------------------
243+
# Panel UI language override (added in v0.2.19)
244+
#
245+
# Stored in entry.data[CONF_LANGUAGE]. Controls which translations the
246+
# panel.js side uses for its hardcoded strings (bulk action buttons,
247+
# status badges, modal section titles, etc). "auto" resolves to the
248+
# current user's HA language at render time; "en" and "sv" override it.
249+
#
250+
# The HA-managed translations (strings.json / translations/en.json /
251+
# translations/sv.json) cover the config flow forms — those follow each
252+
# user's HA locale natively and are unaffected by this setting.
253+
# ---------------------------------------------------------------------------
254+
CONF_LANGUAGE = "language"
255+
LANG_AUTO = "auto"
256+
LANG_EN = "en"
257+
LANG_SV = "sv"
258+
DEFAULT_LANGUAGE = LANG_AUTO
259+
LANGUAGE_OPTIONS = [LANG_AUTO, LANG_EN, LANG_SV]

0 commit comments

Comments
 (0)