mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 19:06:52 +00:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user