fix(push): reinstalled app replaces its old subscription; devices listed and removable
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 2m8s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-31 11:59:40 +01:00
1 parent f76b0f34ff
commit 5b636b66e2
9 files changed
+123 -14

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mindsift-frontend",
"version": "0.22.0",
"version": "0.22.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mindsift-frontend",
"version": "0.22.0",
"version": "0.22.1",
"license": "Unlicense",
"dependencies": {
"@fontsource-variable/inter": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mindsift-frontend",
"private": true,
"version": "0.22.0",
"version": "0.22.1",
"license": "Unlicense",
"type": "module",
"scripts": {
@@ -11,6 +11,7 @@ import {
MessageSquare,
Globe,
Send,
X,
AlertCircle,
Palette,
Upload,
@@ -50,7 +51,7 @@ import { SessionsPanel } from './SessionsPanel';
import { ToggleSwitch } from '../../components/ToggleSwitch';
import { setLinkPreviewsEnabled, useLinkPreviewsEnabled } from '../../lib/linkPreview';
import { isJournalEnabled, setJournalEnabled } from '../../lib/journal';
import { currentPushSubscription, disablePush, enablePush, pushSupported } from '../../lib/push';
import { currentPushSubscription, disablePush, enablePush, pushSupported, removePushDevice } from '../../lib/push';
import { FormSheet } from '../../components/FormSheet';
interface UserSettingsViewProps {
@@ -301,13 +302,28 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
// Web push: devices of this account and whether this browser is one of them
const [pushDevices, setPushDevices] = useState<PushDevice[]>([]);
const [pushHere, setPushHere] = useState(false);
const [pushEndpointHere, setPushEndpointHere] = useState<string | null>(null);
const [pushBusy, setPushBusy] = useState(false);
const [pushMsg, setPushMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null);
const reloadPush = useCallback(async () => {
const [devices, sub] = await Promise.all([api.getPushDevices().catch(() => []), currentPushSubscription()]);
setPushDevices(devices);
setPushHere(Boolean(sub && devices.some((d) => d.endpoint === sub.endpoint)));
setPushEndpointHere(sub?.endpoint ?? null);
}, []);
// An old installation or a device you no longer use: remove it from the list
const removeDevice = async (endpoint: string) => {
setPushBusy(true);
setPushMsg(null);
try {
await removePushDevice(endpoint);
await reloadPush();
} catch (err) {
setPushMsg({ type: 'err', text: err instanceof Error ? err.message : 'Gerät konnte nicht entfernt werden' });
} finally {
setPushBusy(false);
}
};
useEffect(() => {
void reloadPush();
}, [reloadPush]);
@@ -1783,10 +1799,36 @@ export const UserSettingsView: React.FC<UserSettingsViewProps> = ({
</div>
</div>
{pushDevices.length > 0 && (
<p className="pl-6 text-[11px] text-faint">
Aktiv auf {pushDevices.length} {pushDevices.length === 1 ? 'Gerät' : 'Geräten'}:{' '}
{pushDevices.map((d) => d.device || 'Gerät').join(', ')}
</p>
<div className="pl-6 space-y-1">
<p className="text-[11px] text-faint">
Aktiv auf {pushDevices.length} {pushDevices.length === 1 ? 'Gerät' : 'Geräten'}. Alte Installationen
verschwinden beim nächsten Push von selbst oder lassen sich hier entfernen.
</p>
<ul className="border border-border divide-y divide-border bg-panel">
{pushDevices.map((d) => (
<li key={d.id} className="flex items-center justify-between gap-2 pl-2.5 pr-1 py-1">
<span className="min-w-0 text-[11px] text-main truncate">
{d.device || 'Gerät'}
<span className="text-faint">
{' · seit '}
{new Date(d.createdAt).toLocaleDateString('de-DE')}
{d.endpoint === pushEndpointHere && ' · dieses Gerät'}
</span>
</span>
<button
type="button"
onClick={() => void removeDevice(d.endpoint)}
disabled={pushBusy || demoMode}
title="Gerät entfernen"
aria-label={`${d.device || 'Gerät'} entfernen`}
className="h-9 w-9 sm:h-7 sm:w-7 shrink-0 flex items-center justify-center text-faint hover:text-status-err disabled:opacity-40"
>
<X className="w-3.5 h-3.5" />
</button>
</li>
))}
</ul>
</div>
)}
{pushMsg && (
<p className={`pl-6 text-[11px] ${pushMsg.type === 'ok' ? 'text-status-ok' : 'text-status-err'}`}>{pushMsg.text}</p>
+2 -2
View File
@@ -734,8 +734,8 @@ export const api = {
return res.devices || [];
},
async subscribePush(sub: PushSubscriptionJSON, device: string): Promise<void> {
await request('/api/v1/push/subscribe', { method: 'POST', body: JSON.stringify({ ...sub, device }) });
async subscribePush(sub: PushSubscriptionJSON, device: string, replaces?: string): Promise<void> {
await request('/api/v1/push/subscribe', { method: 'POST', body: JSON.stringify({ ...sub, device, replaces: replaces || '' }) });
},
async unsubscribePush(endpoint: string): Promise<void> {
+35 -1
View File
@@ -64,7 +64,40 @@ export async function enablePush(): Promise<void> {
throw new Error('Der Browser hat die Anmeldung beim Push-Dienst abgelehnt (Berechtigung blockiert oder Push-Dienst nicht erreichbar)');
}
}
await api.subscribePush(sub.toJSON(), deviceName());
// The address this browser registered before is dead once it has a new one (e.g. after reinstalling
// the app): the server drops it right away instead of counting it as another device
const previous = readStoredEndpoint();
await api.subscribePush(sub.toJSON(), deviceName(), previous && previous !== sub.endpoint ? previous : undefined);
storeEndpoint(sub.endpoint);
}
const ENDPOINT_KEY = 'mindsift_push_endpoint';
function readStoredEndpoint(): string | null {
try {
return localStorage.getItem(ENDPOINT_KEY);
} catch {
return null;
}
}
function storeEndpoint(endpoint: string | null): void {
try {
if (endpoint) localStorage.setItem(ENDPOINT_KEY, endpoint);
else localStorage.removeItem(ENDPOINT_KEY);
} catch {
// storage blocked: the dead device is removed on the next push instead
}
}
/** Removes one of the own devices (e.g. an old installation) on the server. */
export async function removePushDevice(endpoint: string): Promise<void> {
const here = await currentPushSubscription();
if (here?.endpoint === endpoint) {
await disablePush();
return;
}
await api.unsubscribePush(endpoint);
}
export async function disablePush(): Promise<void> {
@@ -73,4 +106,5 @@ export async function disablePush(): Promise<void> {
const endpoint = sub.endpoint;
await sub.unsubscribe().catch(() => false);
await api.unsubscribePush(endpoint).catch(() => {});
storeEndpoint(null);
}