Files
pl_mail/templates/settings/_push_devices.html.twig
Paul LütznerandClaude Opus 5.5 8ad3e83c52 fix(ui): the last hard-coded colours in the templates use the theme's tokens
Four kinds, each of which ignored the theme it was drawn in:

- Checkboxes and radios asked for the accent through `text-accent`. A
  native box is coloured by accent-color; `color` reaches it only
  through @tailwindcss/forms, which this app does not load, so every
  one of them was ticked in the browser's own blue. They use
  `accent-accent`, and modal_form_theme says why.
- Icons and icon buttons in admin, settings and the thread row's hover
  actions were Tailwind's zinc-400 and gray-400, with dark: variants and
  grey hover washes. They are ink-faint, with hover:text-ink and
  hover:bg-hover, or hover:bg-danger-soft for the destructive ones.
- The state pills on bg-raised wrote ink-faint, a tier held to 4.5:1 on
  the surface, which the raised tint under them takes as low as 3.85:1
  (14 of 39 themes under 4.5). They use ink-muted.
- Accent buttons wrote text-white, which a light accent cannot carry.
  They use the accent's own ink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:29:34 +02:00

116 lines
6.3 KiB
Twig

{#
settings/_push_devices.html.twig — the devices this account can be pushed to.
Accepts:
devices — list<App\Entity\User\PushSubscription>, newest first
lastDeliveries — map of deviceClientId => App\Domain\DTO\Push\LastDelivery,
with no entry for a device nothing has been sent to yet
Sits under the browser toggle, and answers a different question from it. The
toggle is about THIS browser and is resolved by JavaScript on connect; this is
every device registered against the account — the phone, the tablet, the other
laptop — which the person asking "why did my phone stop buzzing?" cannot see
anywhere else.
Three facts per row, and each is here because its absence produced a support
question. The **transport** because a device on Firebase and one on a
UnifiedPush distributor fail for entirely different reasons. **Verified**
because an unverified subscription receives NOTHING by design (RFC 8620
§7.2.2), which looks exactly like a broken one. And the **last delivery**,
because "we tried and Firebase said the token is dead" and "nothing has ever
been sent to this device" are the two answers, and they are opposites.
Each row can also be removed, which is the only way to be rid of a subscription
that is working exactly as designed and should not exist — the classic case
being an old build of an app still registered under a stale device id, so one
phone receives everything twice. The delivery log is addressed by device id and
not by a relation, so the removed device's history stays readable afterwards.
What is deliberately NOT shown: what was pushed. The log stores the payload's
`@type` and nothing else — see the PushDelivery docblock — so this line can say
a state change was delivered and can never say which mail it was about.
#}
<div class="px-4 py-3.5 border-t border-line">
<p class="text-sm text-ink">{{ 'settings.notifications.devices.heading'|trans }}</p>
{% if devices|length == 0 %}
<p class="mt-1.5 text-[11px] text-ink-faint leading-relaxed">
{{ 'settings.notifications.devices.empty'|trans }}
</p>
{% else %}
<ul class="mt-2.5 space-y-2.5">
{% for device in devices %}
{# Keyed by the client's own device id, which is what the log
records — a subscription that rotated its endpoint or its FCM
token keeps it, so its history survives the rotation. #}
{% set last = lastDeliveries[device.deviceClientId] ?? null %}
{% set toneClass = last is null ? 'badge-neutral' : {
success: 'badge-success',
danger: 'badge-danger',
warning: 'badge-warning',
neutral: 'badge-neutral',
}[last.outcome.tone] %}
<li class="flex flex-wrap items-center gap-x-2.5 gap-y-1">
<span class="text-xs text-ink-soft font-mono truncate max-w-[12rem]">{{ device.deviceClientId }}</span>
<span class="text-[10px] font-medium px-2 py-0.5 rounded-full bg-raised text-ink-muted">
{{ ('settings.notifications.devices.transport.' ~ device.transport.value)|trans }}
</span>
{% if device.verified %}
<span class="text-[10px] font-medium px-2 py-0.5 rounded-full badge-success">
{{ 'settings.notifications.devices.verified'|trans }}
</span>
{% else %}
{# Amber rather than red: the handshake being incomplete
is the normal state for the first few seconds after
registering, and only a problem if it stays. #}
<span class="text-[10px] font-medium px-2 py-0.5 rounded-full badge-warning">
{{ 'settings.notifications.devices.unverified'|trans }}
</span>
{% endif %}
<span class="w-full @2xl:w-auto text-[11px] text-ink-faint">
{% if last is null %}
{{ 'settings.notifications.devices.never_delivered'|trans }}
{% else %}
<span class="text-[10px] font-medium px-2 py-0.5 rounded-full {{ toneClass }}">
{{ ('settings.notifications.devices.outcome.' ~ last.outcome.value)|trans }}
</span>
{{ last.at|date('Y-m-d H:i') }}
{% if last.payloadType is not null %}
· {{ last.payloadType }}
{% endif %}
{% endif %}
</span>
{# The only way to be rid of a device that is working
perfectly and should not be — an old build of an app
still registered under a device id nobody uses, pushing
a second copy of everything at the same phone. Automatic
retirement never reaches it, because nothing about it
fails.
Confirmed inline like the app-password row rather than
through a modal, and the confirm text carries the honest
half: this stops deliveries, it does not uninstall
anything, and a device whose app still wants push will
register itself again. #}
<form method="post"
action="{{ path('app_push_device_remove', { id: device.id }) }}"
data-turbo-stream class="ml-auto shrink-0"
data-turbo-confirm="{{ 'settings.notifications.devices.remove_confirm'|trans({ '%device%': device.deviceClientId }) }}">
<input type="hidden" name="_token" value="{{ csrf_token('push-device-remove' ~ device.id) }}">
<button type="submit"
class="text-[11px] text-ink-faint hover:text-red-400 transition-colors cursor-pointer">
{{ 'settings.notifications.devices.remove'|trans }}
</button>
</form>
</li>
{% endfor %}
</ul>
{% endif %}
</div>