release workflow: publish multi-arch images; settings: confirm language switch

- release.yml: add QEMU + platforms: linux/amd64,linux/arm64 to both
  build-push steps so the published images work on Apple Silicon (fixes
  'no matching manifest for linux/arm64/v8' on docker compose pull).
- Settings -> Profile language picker is now a select that opens a
  confirmation dialog before applying, instead of switching instantly.
- CLAUDE.md: require a dated docs changelog entry for every release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-07-01 21:54:47 +03:00
parent 7838dd68a5
commit 5bbfe551fc
5 changed files with 72 additions and 17 deletions
+7
View File
@@ -35,6 +35,11 @@ jobs:
id: meta id: meta
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
# QEMU lets the amd64 runner emulate arm64 so the images below build for
# both platforms (Intel + Apple Silicon self-hosters).
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Buildx - name: Set up Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -49,6 +54,7 @@ jobs:
with: with:
context: ./backend context: ./backend
push: true push: true
platforms: linux/amd64,linux/arm64
tags: | tags: |
${{ env.REGISTRY_NAMESPACE }}/temetro-backend:${{ steps.meta.outputs.version }} ${{ env.REGISTRY_NAMESPACE }}/temetro-backend:${{ steps.meta.outputs.version }}
${{ env.REGISTRY_NAMESPACE }}/temetro-backend:latest ${{ env.REGISTRY_NAMESPACE }}/temetro-backend:latest
@@ -58,6 +64,7 @@ jobs:
with: with:
context: ./frontend context: ./frontend
push: true push: true
platforms: linux/amd64,linux/arm64
tags: | tags: |
${{ env.REGISTRY_NAMESPACE }}/temetro-frontend:${{ steps.meta.outputs.version }} ${{ env.REGISTRY_NAMESPACE }}/temetro-frontend:${{ steps.meta.outputs.version }}
${{ env.REGISTRY_NAMESPACE }}/temetro-frontend:latest ${{ env.REGISTRY_NAMESPACE }}/temetro-frontend:latest
+5
View File
@@ -80,6 +80,11 @@ accurate (e.g. a new backend route needs an `content/docs/api/*.mdx` entry; a UI
in the matching guide; status changes belong in the roadmap). Commit docs changes inside that in the matching guide; status changes belong in the roadmap). Commit docs changes inside that
repo, separately from this one. repo, separately from this one.
**Every release must also get a dated entry in the docs changelog**
(`content/docs/changelog.mdx`, newest first) — not just the monorepo `CHANGELOG.md`. When you cut a
version (see "Always release after pushing"), add a matching, user-facing section to that page in the
same session so `../temetro/docs` never falls behind the shipped version.
## Running the stack ## Running the stack
From `backend/`: ensure a `.env` exists (`cp .env.example .env`, then set `BETTER_AUTH_SECRET` via From `backend/`: ensure a `.env` exists (`cp .env.example .env`, then set `BETTER_AUTH_SECRET` via
@@ -6,7 +6,15 @@ import { useTranslation } from "react-i18next";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { DeleteAccountDialog } from "@/components/settings/delete-account-dialog"; import { DeleteAccountDialog } from "@/components/settings/delete-account-dialog";
import { import {
CopyField, CopyField,
@@ -68,6 +76,10 @@ export function ProfilePanel() {
const [baselineName, setBaselineName] = useState(""); const [baselineName, setBaselineName] = useState("");
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
// A language picked in the Select but not yet applied — its presence opens the
// confirmation dialog. The Select stays bound to `activeLang`, so cancelling
// (clearing this) automatically reverts the shown selection.
const [pendingLang, setPendingLang] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -222,21 +234,23 @@ export function ProfilePanel() {
> >
<SettingsCard className="space-y-1.5 p-5"> <SettingsCard className="space-y-1.5 p-5">
<FieldLabel>{t("settings.profile.language.label")}</FieldLabel> <FieldLabel>{t("settings.profile.language.label")}</FieldLabel>
<div className="flex flex-wrap gap-2"> <Select
{supportedLanguages.map((lng) => ( onValueChange={(value) => {
<Button if (value !== activeLang) setPendingLang(value);
aria-pressed={activeLang === lng} }}
className="rounded-3xl" value={activeLang}
key={lng} >
onClick={() => void i18n.changeLanguage(lng)} <SelectTrigger>
size="sm" <SelectValue />
type="button" </SelectTrigger>
variant={activeLang === lng ? "default" : "outline"} <SelectPopup>
> {supportedLanguages.map((lng) => (
{t(`settings.profile.language.${lng}`)} <SelectItem key={lng} value={lng}>
</Button> {t(`settings.profile.language.${lng}`)}
))} </SelectItem>
</div> ))}
</SelectPopup>
</Select>
</SettingsCard> </SettingsCard>
</SettingsSection> </SettingsSection>
@@ -316,6 +330,27 @@ export function ProfilePanel() {
<DeleteAccountDialog onOpenChange={setDeleteOpen} open={deleteOpen} /> <DeleteAccountDialog onOpenChange={setDeleteOpen} open={deleteOpen} />
<ConfirmDialog
cancelLabel={t("settings.profile.language.cancel")}
confirmLabel={t("settings.profile.language.confirmCta")}
description={
pendingLang
? t("settings.profile.language.confirmBody", {
language: t(`settings.profile.language.${pendingLang}`),
})
: undefined
}
onConfirm={() => {
if (pendingLang) void i18n.changeLanguage(pendingLang);
}}
onOpenChange={(open) => {
if (!open) setPendingLang(null);
}}
open={pendingLang !== null}
title={t("settings.profile.language.confirmTitle")}
variant="default"
/>
{dirty ? ( {dirty ? (
<div className="sticky bottom-4 z-10"> <div className="sticky bottom-4 z-10">
<div className="flex items-center justify-between gap-4 rounded-2xl border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur"> <div className="flex items-center justify-between gap-4 rounded-2xl border border-border bg-background/95 px-4 py-3 shadow-lg backdrop-blur">
@@ -1689,7 +1689,11 @@
"description": "The language temetro's interface is shown in on this device.", "description": "The language temetro's interface is shown in on this device.",
"label": "Display language", "label": "Display language",
"en": "English", "en": "English",
"fr": "Français" "fr": "Français",
"confirmTitle": "Change display language?",
"confirmBody": "Switch the interface to {{language}}? The app will reload its text in the new language.",
"confirmCta": "Change language",
"cancel": "Cancel"
}, },
"patientNotifications": "Patient notifications", "patientNotifications": "Patient notifications",
"patientNotificationsDescription": "Emails sent to patients about their records, results, and pending approvals", "patientNotificationsDescription": "Emails sent to patients about their records, results, and pending approvals",
@@ -1689,7 +1689,11 @@
"description": "La langue dans laquelle l'interface de temetro s'affiche sur cet appareil.", "description": "La langue dans laquelle l'interface de temetro s'affiche sur cet appareil.",
"label": "Langue d'affichage", "label": "Langue d'affichage",
"en": "English", "en": "English",
"fr": "Français" "fr": "Français",
"confirmTitle": "Changer la langue d'affichage ?",
"confirmBody": "Basculer l'interface en {{language}} ? L'application rechargera ses textes dans la nouvelle langue.",
"confirmCta": "Changer la langue",
"cancel": "Annuler"
}, },
"patientNotifications": "Notifications patients", "patientNotifications": "Notifications patients",
"patientNotificationsDescription": "E-mails envoyés aux patients à propos de leurs dossiers, résultats et approbations en attente", "patientNotificationsDescription": "E-mails envoyés aux patients à propos de leurs dossiers, résultats et approbations en attente",