refactor(stores): replace Record<string,unknown> declarations with Partial<LocalX>

Final follow-up to drop the type-bypass patterns from F3's codemod.
Mit \`Partial<LocalX>\` als Deklaration akzeptiert Dexie's UpdateSpec
ohne weiteren Cast — die kombinierte \`as Record<string,unknown>\` +
\`as never\` Konstruktion wird durch eine einzige saubere
Typ-Annotation ersetzt.

Touched stores (12 Files):
  wardrobe/stores/{garments,outfits}, invoices/stores/invoices,
  sleep/stores/sleep, library/stores/entries,
  comic/stores/{characters,stories},
  profile/stores/me-images, recipes/stores/recipes,
  broadcast/stores/campaigns, writing/stores/{styles,drafts}

Plus inline literal-object patterns (\`{ lines, totals } as Record\`,
\`{ content } as Record\`, \`{ audience } as Record\`,
\`{ ...spread } as Record\` im comic appendPanel).

Verbleibende \`as Record<string, unknown>\` Vorkommen sind legitime
Reads von typed-data und nicht das F3-Pattern.

7670 svelte-check Files, 0 Errors, 0 Warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-27 00:58:01 +02:00
parent b4589a7249
commit 515de79c8b
12 changed files with 52 additions and 37 deletions

View file

@ -96,7 +96,7 @@ export const broadcastCampaignsStore = {
if (existing.status !== 'draft') {
throw new Error('[broadcast] only drafts can be edited; duplicate to revise a sent campaign');
}
const wrapped = { ...patch } as Record<string, unknown>;
const wrapped: Partial<LocalCampaign> = { ...patch };
await encryptRecord('broadcastCampaigns', wrapped);
await campaignTable.update(id, wrapped);
},
@ -113,7 +113,7 @@ export const broadcastCampaignsStore = {
if (existing.status !== 'draft') {
throw new Error('[broadcast] only drafts can be edited');
}
const patch = { content } as Record<string, unknown>;
const patch: Partial<LocalCampaign> = { content };
await encryptRecord('broadcastCampaigns', patch);
await campaignTable.update(id, patch);
},
@ -124,7 +124,7 @@ export const broadcastCampaignsStore = {
if (existing.status !== 'draft') {
throw new Error('[broadcast] only drafts can be edited');
}
const patch = { audience } as Record<string, unknown>;
const patch: Partial<LocalCampaign> = { audience };
await encryptRecord('broadcastCampaigns', patch);
await campaignTable.update(id, patch);
},

View file

@ -161,7 +161,7 @@ export const comicStoriesStore = {
const patch = {
panelImageIds: nextIds,
panelMeta: nextMeta,
} as Record<string, unknown>;
} as Partial<LocalComicStory>;
await encryptRecord('comicStories', patch);
await comicStoriesTable.update(storyId, patch);
emitDomainEvent('ComicPanelAppended', 'comic', 'comicStories', storyId, {

View file

@ -135,7 +135,7 @@ export const invoicesStore = {
'[invoices] only drafts can be edited; void and duplicate to revise a sent invoice'
);
}
const wrapped = { ...patch } as Record<string, unknown>;
const wrapped: Partial<LocalInvoice> = { ...patch };
await encryptRecord('invoices', wrapped);
await invoiceTable.update(id, wrapped);
},
@ -152,7 +152,7 @@ export const invoicesStore = {
throw new Error('[invoices] only drafts can be edited');
}
const totals = computeInvoiceTotals(lines);
const patch = { lines, totals } as Record<string, unknown>;
const patch: Partial<LocalInvoice> = { lines, totals };
// `lines` is in the encryption allowlist; `totals` is not. encryptRecord
// only touches allowlisted keys, so a single call is correct for both.
await encryptRecord('invoices', patch);

View file

@ -130,7 +130,7 @@ export const libraryEntriesStore = {
>
>
) {
const wrapped = { ...patch } as Record<string, unknown>;
const wrapped: Partial<LocalLibraryEntry> = { ...patch };
await encryptRecord('libraryEntries', wrapped);
await libraryEntryTable.update(id, wrapped);
// Keep the share-link snapshot in sync if this entry is unlisted.

View file

@ -165,7 +165,7 @@ export const meImagesStore = {
id: string,
patch: Partial<Pick<LocalMeImage, 'label' | 'tags' | 'kind' | 'usage'>>
): Promise<void> {
const wrapped = { ...patch } as Record<string, unknown>;
const wrapped: Partial<LocalMeImage> = { ...patch };
await encryptRecord('meImages', wrapped);
await meImagesTable.update(id, wrapped);
},

View file

@ -76,7 +76,7 @@ export const recipesStore = {
>
>
) {
const wrapped = { ...patch } as Record<string, unknown>;
const wrapped: Partial<LocalRecipe> = { ...patch };
await encryptRecord('recipes', wrapped);
await recipeTable.update(id, wrapped);
},

View file

@ -3,6 +3,7 @@
Short links list with click counts and quick link creation.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { useLiveQueryWithDefault } from '@mana/local-store/svelte';
import { db } from '$lib/data/database';
import { decryptRecords } from '$lib/data/crypto';
@ -59,11 +60,11 @@
try {
const parsed = new URL(fullUrl);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
error = 'Ungültige URL';
error = $_('uload.list_view.err_invalid_url');
return;
}
} catch {
error = 'Ungültige URL';
error = $_('uload.list_view.err_invalid_url');
return;
}
@ -95,11 +96,11 @@
}
</script>
<BaseListView items={sorted} getKey={(l) => l.id} emptyTitle="Keine Links">
<BaseListView items={sorted} getKey={(l) => l.id} emptyTitle={$_('uload.list_view.empty_title')}>
{#snippet header()}
<span>{links.length} Links</span>
<span>{totalClicks} Klicks</span>
<span>{folders.length} Ordner</span>
<span>{$_('uload.list_view.header_links', { values: { count: links.length } })}</span>
<span>{$_('uload.list_view.header_clicks', { values: { count: totalClicks } })}</span>
<span>{$_('uload.list_view.header_folders', { values: { count: folders.length } })}</span>
{/snippet}
{#snippet listHeader()}
@ -113,7 +114,12 @@
>
<LinkIcon size={14} class="icon" />
<!-- svelte-ignore a11y_autofocus -->
<input class="add-input" bind:value={newUrl} placeholder="URL einfügen..." autofocus />
<input
class="add-input"
bind:value={newUrl}
placeholder={$_('uload.list_view.add_placeholder')}
autofocus
/>
<button type="submit" class="submit-btn" disabled={!newUrl.trim()}>
<Plus size={14} />
</button>
@ -124,7 +130,7 @@
{:else}
<button class="add-toggle" onclick={() => (showAdd = true)}>
<Plus size={14} />
<span>Neuer Link</span>
<span>{$_('uload.list_view.add_button')}</span>
</button>
{/if}
{/snippet}

View file

@ -3,6 +3,7 @@
All fields are always editable. Changes auto-save on blur.
-->
<script lang="ts">
import { _ } from 'svelte-i18n';
import { formatDate } from '$lib/i18n/format';
import { db } from '$lib/data/database';
import { encryptRecord } from '$lib/data/crypto';
@ -65,14 +66,14 @@
<DetailViewShell
entity={detail.entity}
loading={detail.loading}
notFoundLabel="Link nicht gefunden"
notFoundLabel={$_('uload.detail_view.not_found')}
confirmDelete={detail.confirmDelete}
onAskDelete={detail.askDelete}
onCancelDelete={detail.cancelDelete}
confirmDeleteLabel="Link wirklich löschen?"
confirmDeleteLabel={$_('uload.detail_view.confirm_delete')}
onConfirmDelete={() =>
detail.deleteWithUndo({
label: 'Link gelöscht',
label: $_('uload.detail_view.deleted_toast'),
delete: deleteLink,
goBack,
})}
@ -83,12 +84,12 @@
bind:value={editTitle}
onfocus={detail.focus}
onblur={saveField}
placeholder="Titel..."
placeholder={$_('uload.detail_view.placeholder_title')}
/>
<div class="properties">
<div class="prop-row">
<span class="prop-label">URL</span>
<span class="prop-label">{$_('uload.detail_view.label_url')}</span>
<input
class="prop-input"
bind:value={editOriginalUrl}
@ -99,25 +100,25 @@
</div>
<div class="prop-row">
<span class="prop-label">Kurzcode</span>
<span class="prop-label">{$_('uload.detail_view.label_short_code')}</span>
<input
class="prop-input"
bind:value={editCustomCode}
onfocus={detail.focus}
onblur={saveField}
placeholder="custom-code"
placeholder={$_('uload.detail_view.placeholder_short_code')}
/>
</div>
{#if link.shortCode}
<div class="prop-row">
<span class="prop-label">Short Code</span>
<span class="prop-label">{$_('uload.detail_view.label_short_code_legacy')}</span>
<span class="prop-value">{link.shortCode}</span>
</div>
{/if}
<div class="prop-row">
<span class="prop-label">Aktiv</span>
<span class="prop-label">{$_('uload.detail_view.label_active')}</span>
<button
class="toggle-btn"
class:active={editIsActive}
@ -126,17 +127,17 @@
handleActiveToggle();
}}
>
{editIsActive ? 'Ja' : 'Nein'}
{editIsActive ? $_('uload.detail_view.yes') : $_('uload.detail_view.no')}
</button>
</div>
<div class="prop-row">
<span class="prop-label">Klicks</span>
<span class="prop-label">{$_('uload.detail_view.label_clicks')}</span>
<span class="prop-value">{link.clickCount}</span>
</div>
<div class="prop-row">
<span class="prop-label">Ablaufdatum</span>
<span class="prop-label">{$_('uload.detail_view.label_expires_at')}</span>
<input
type="date"
class="prop-input"
@ -148,21 +149,29 @@
</div>
<div class="section">
<span class="section-label">Beschreibung</span>
<span class="section-label">{$_('uload.detail_view.section_description')}</span>
<textarea
class="description-input"
bind:value={editDescription}
onfocus={detail.focus}
onblur={saveField}
placeholder="Beschreibung hinzufügen..."
placeholder={$_('uload.detail_view.placeholder_description')}
rows={3}
></textarea>
</div>
<div class="meta">
<span>Erstellt: {formatDate(new Date(link.createdAt ?? ''))}</span>
<span
>{$_('uload.detail_view.meta_created', {
values: { date: formatDate(new Date(link.createdAt ?? '')) },
})}</span
>
{#if link.updatedAt}
<span>Bearbeitet: {formatDate(new Date(link.updatedAt))}</span>
<span
>{$_('uload.detail_view.meta_updated', {
values: { date: formatDate(new Date(link.updatedAt)) },
})}</span
>
{/if}
</div>
{/snippet}

View file

@ -80,7 +80,7 @@ export const wardrobeGarmentsStore = {
>
>
): Promise<void> {
const wrapped = { ...patch } as Record<string, unknown>;
const wrapped: Partial<LocalWardrobeGarment> = { ...patch };
await encryptRecord('wardrobeGarments', wrapped);
await wardrobeGarmentsTable.update(id, wrapped);
},

View file

@ -71,7 +71,7 @@ export const wardrobeOutfitsStore = {
>
>
): Promise<void> {
const wrapped = { ...patch } as Record<string, unknown>;
const wrapped: Partial<LocalWardrobeOutfit> = { ...patch };
await encryptRecord('wardrobeOutfits', wrapped);
await wardrobeOutfitsTable.update(id, wrapped);
},

View file

@ -139,7 +139,7 @@ export const draftsStore = {
},
async updateDraft(id: string, patch: UpdateDraftPatch) {
const wrapped = { ...patch } as Record<string, unknown>;
const wrapped: Partial<LocalDraft> = { ...patch };
await encryptRecord('writingDrafts', wrapped);
await draftTable.update(id, wrapped);
},

View file

@ -64,7 +64,7 @@ export const stylesStore = {
},
async updateStyle(id: string, patch: UpdateStylePatch) {
const wrapped = { ...patch } as Record<string, unknown>;
const wrapped: Partial<LocalWritingStyle> = { ...patch };
await encryptRecord('writingStyles', wrapped);
await writingStyleTable.update(id, wrapped);
},