Comprehensive review fixes: security, performance, code quality, and UI polish
Some checks failed
Validate / Hassfest (push) Has been cancelled

Backend: Fix CORS wildcard+credentials, add secret key warning, remove raw
API keys from sync endpoint, fix N+1 queries in watcher/sync, fix
AttributeError on event_types, delete dead scheduled.py/templates.py,
add limit cap on history, re-validate server on URL/key update, apply
tracking/template config IDs in update_target.

HA Integration: Replace datetime.now() with dt_util.now(), fix notification
queue to only remove successfully sent items, use album UUID for entity
unique IDs, add shared links dirty flag and users cache hourly refresh,
deduplicate _is_quiet_hours, add HTTP timeouts, cache albums in config
flow, change iot_class to local_polling.

Frontend: Make i18n reactive via $state (remove window.location.reload),
add Modal transitions/a11y/Escape key, create ConfirmModal replacing all
confirm() calls, add error handling to all pages, replace Unicode nav
icons with MDI SVGs, add card hover effects, dashboard stat icons, global
focus-visible styles, form slide transitions, mobile responsive bottom
nav, fix password error color, add ~20 i18n keys (EN/RU).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 18:34:31 +03:00
parent a04d5618d0
commit 381de98c40
39 changed files with 785 additions and 626 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { slide } from 'svelte/transition';
import { api } from '$lib/api';
import { t } from '$lib/i18n';
import PageHeader from '$lib/components/PageHeader.svelte';
@@ -7,12 +8,14 @@
import Loading from '$lib/components/Loading.svelte';
import IconPicker from '$lib/components/IconPicker.svelte';
import MdiIcon from '$lib/components/MdiIcon.svelte';
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
let configs = $state<any[]>([]);
let loaded = $state(false);
let showForm = $state(false);
let editing = $state<number | null>(null);
let error = $state('');
let confirmDelete = $state<any>(null);
let previewSlot = $state('message_assets_added');
let previewResult = $state('');
let previewId = $state<number | null>(null);
@@ -78,7 +81,11 @@
];
onMount(load);
async function load() { try { configs = await api('/template-configs'); } catch {} finally { loaded = true; } }
async function load() {
try { configs = await api('/template-configs'); }
catch (err: any) { error = err.message || t('common.loadError'); }
finally { loaded = true; }
}
function openNew() { form = defaultForm(); editing = null; showForm = true; }
function edit(c: any) { form = { ...defaultForm(), ...c }; editing = c.id; showForm = true; }
@@ -100,9 +107,15 @@
} catch (err: any) { previewResult = `Error: ${err.message}`; }
}
async function remove(id: number) {
if (!confirm(t('common.delete') + '?')) return;
try { await api(`/template-configs/${id}`, { method: 'DELETE' }); await load(); } catch (err: any) { error = err.message; }
function remove(id: number) {
confirmDelete = {
id,
onconfirm: async () => {
try { await api(`/template-configs/${id}`, { method: 'DELETE' }); await load(); }
catch (err: any) { error = err.message; }
finally { confirmDelete = null; }
}
};
}
</script>
@@ -116,6 +129,7 @@
{#if !loaded}<Loading />{:else}
{#if showForm}
<div transition:slide>
<Card class="mb-6">
{#if error}<div class="bg-[var(--color-error-bg)] text-[var(--color-error-fg)] text-sm rounded-md p-3 mb-4">{error}</div>{/if}
<form onsubmit={save} class="space-y-5">
@@ -135,7 +149,7 @@
{#each group.slots as slot}
<div>
<label class="block text-xs text-[var(--color-muted-foreground)] mb-1">{t(`templateConfig.${slot.label}`)}</label>
<textarea bind:value={form[slot.key]} rows={slot.key.includes('message_asset_') || slot.key.includes('_template') || slot.key === 'favorite_indicator' || slot.key === 'date_format' || slot.key === 'location_format' ? 1 : 2}
<textarea bind:value={(form as any)[slot.key]} rows={slot.key.includes('message_asset_') || slot.key.includes('_template') || slot.key === 'favorite_indicator' || slot.key === 'date_format' || slot.key === 'location_format' ? 1 : 2}
class="w-full px-2 py-1 border border-[var(--color-border)] rounded text-sm bg-[var(--color-background)] font-mono"></textarea>
</div>
{/each}
@@ -148,6 +162,7 @@
</button>
</form>
</Card>
</div>
{/if}
{#if configs.length === 0 && !showForm}
@@ -155,7 +170,7 @@
{:else}
<div class="space-y-3">
{#each configs as config}
<Card>
<Card hover>
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2">
@@ -182,3 +197,6 @@
{/if}
{/if}
<ConfirmModal open={confirmDelete !== null} message={t('templateConfig.confirmDelete')}
onconfirm={() => confirmDelete?.onconfirm()} oncancel={() => confirmDelete = null} />