mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 08:01:09 +02:00
fix(mana/web): unblock api-keys, reset-password, dashboard widgets, skilltree stats
Six unrelated type-error pockets that were each blocking a different
page from compiling clean. Grouped because none individually warrants
its own commit and they all touch the same module's call sites.
api-keys/+page.svelte
- Removed the `key: undefined as unknown as string` workaround for
stripping the secret from the local list. Replaced with a clean
object-rest destructure that produces a row matching the ApiKey
shape (no `key` field). The cast was the source of two type
errors AND was lying about the runtime shape.
- Badge `variant="secondary"` and `variant="outline"` aren't valid
BadgeVariant — narrowed to `default` and `info` respectively.
- Button `variant="destructive"` and Badge `variant="destructive"`
don't exist in the shared-ui union — both → `danger`.
- Rate-limit input bound a `number` to a `<Input>` component whose
`value` is typed `string`. Switched to a string state and
parseInt on submit. Prevents the binding cast that the type
checker (correctly) rejected.
reset-password/+page.svelte
- Calling `authStore.resetPassword(token, password)` with two args
on a method that takes one (sends the reset email). The method
that actually performs the reset is `resetPasswordWithToken`.
Two args, no API contract change needed.
- `<Input minlength={12}>` — minlength isn't a prop on the shared
Input component (it's not a passthrough wrapper). Removed; the
runtime check still gates submit.
dashboard/widgets/{Credits,Transactions}Widget.svelte
- `let state = $state<...>(...)` — variable named `state` shadows
the `$state` rune call, which TypeScript flags as
"Block-scoped variable '$state' used before its declaration"
+ "Untyped function calls may not accept type arguments".
Renamed both to `loadState`.
dashboard/widgets/TasksTodayWidget.svelte
- Referenced `task.dueTime`, which doesn't exist on LocalTask
(only `dueDate`, ISO timestamp). Dropped the dead branch — the
time was already encoded in `dueDate` and the widget never
surfaced anything actionable from it anyway.
skilltree/components/StatsOverview.svelte
- Was manually wiring `.subscribe()` callbacks because the old
queries.ts returned raw Dexie Observables. After the
Observable→useLiveQueryWithDefault migration, those return
`{value, loading, error}` instead — `subscribe` doesn't exist
on them. Replaced the manual state plumbing with direct
`.value` reads inside `$derived`. Net: less code, fewer
levels of indirection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
05d9d1962c
commit
697d96daec
6 changed files with 35 additions and 50 deletions
|
|
@ -9,23 +9,23 @@
|
|||
import WidgetSkeleton from '../WidgetSkeleton.svelte';
|
||||
import WidgetError from '../WidgetError.svelte';
|
||||
|
||||
let state = $state<'loading' | 'success' | 'error'>('loading');
|
||||
let loadState = $state<'loading' | 'success' | 'error'>('loading');
|
||||
let data = $state<CreditBalance | null>(null);
|
||||
let error = $state<string | null>(null);
|
||||
let retrying = $state(false);
|
||||
|
||||
async function load() {
|
||||
if (!data) state = 'loading';
|
||||
if (!data) loadState = 'loading';
|
||||
retrying = true;
|
||||
|
||||
try {
|
||||
const balance = await creditsService.getBalance();
|
||||
data = balance;
|
||||
state = 'success';
|
||||
loadState = 'success';
|
||||
} catch (e) {
|
||||
if (!data) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load credits';
|
||||
state = 'error';
|
||||
loadState = 'error';
|
||||
}
|
||||
} finally {
|
||||
retrying = false;
|
||||
|
|
@ -45,9 +45,9 @@
|
|||
{$_('dashboard.widgets.credits.title')}
|
||||
</h3>
|
||||
|
||||
{#if state === 'loading'}
|
||||
{#if loadState === 'loading'}
|
||||
<WidgetSkeleton lines={3} />
|
||||
{:else if state === 'error'}
|
||||
{:else if loadState === 'error'}
|
||||
<WidgetError {error} onRetry={load} {retrying} />
|
||||
{:else if data}
|
||||
<div class="space-y-3">
|
||||
|
|
|
|||
|
|
@ -152,14 +152,9 @@
|
|||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if task.dueTime || getSubtaskProgress(task)}
|
||||
{#if getSubtaskProgress(task)}
|
||||
<div class="mt-0.5 flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{#if task.dueTime}
|
||||
<span>{task.dueTime}</span>
|
||||
{/if}
|
||||
{#if getSubtaskProgress(task)}
|
||||
<span>{getSubtaskProgress(task)}</span>
|
||||
{/if}
|
||||
<span>{getSubtaskProgress(task)}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,22 +9,22 @@
|
|||
import WidgetSkeleton from '../WidgetSkeleton.svelte';
|
||||
import WidgetError from '../WidgetError.svelte';
|
||||
|
||||
let state = $state<'loading' | 'success' | 'error'>('loading');
|
||||
let loadState = $state<'loading' | 'success' | 'error'>('loading');
|
||||
let data = $state<CreditTransaction[]>([]);
|
||||
let error = $state<string | null>(null);
|
||||
let retrying = $state(false);
|
||||
|
||||
async function load() {
|
||||
state = 'loading';
|
||||
loadState = 'loading';
|
||||
retrying = true;
|
||||
|
||||
try {
|
||||
const transactions = await creditsService.getTransactions(5);
|
||||
data = transactions;
|
||||
state = 'success';
|
||||
loadState = 'success';
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Failed to load transactions';
|
||||
state = 'error';
|
||||
loadState = 'error';
|
||||
} finally {
|
||||
retrying = false;
|
||||
}
|
||||
|
|
@ -63,9 +63,9 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
{#if state === 'loading'}
|
||||
{#if loadState === 'loading'}
|
||||
<WidgetSkeleton lines={4} />
|
||||
{:else if state === 'error'}
|
||||
{:else if loadState === 'error'}
|
||||
<WidgetError {error} onRetry={load} {retrying} />
|
||||
{:else if data.length === 0}
|
||||
<p class="py-4 text-center text-sm text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -3,23 +3,17 @@
|
|||
import { buildAchievementStatus, getAchievementStats } from '../stores/achievements.svelte';
|
||||
import { Trophy, Lightning, Target, Fire, Medal } from '@mana/shared-icons';
|
||||
|
||||
// Reactive live queries
|
||||
// Reactive live queries — useLiveQueryWithDefault wraps Dexie's
|
||||
// liveQuery and exposes a `.value` getter backed by $state, so we
|
||||
// just read it inside $derived without manual subscribe plumbing.
|
||||
const allSkills = useAllSkills();
|
||||
const allActivities = useAllActivities();
|
||||
const allAchievementsRaw = useAllAchievements();
|
||||
|
||||
let skills = $state<import('../types').Skill[]>([]);
|
||||
let activities = $state<import('../types').Activity[]>([]);
|
||||
let achievementsRaw = $state<import('../types').LocalAchievement[]>([]);
|
||||
|
||||
$effect(() => {
|
||||
allSkills.subscribe((v) => (skills = v ?? []));
|
||||
allActivities.subscribe((v) => (activities = v ?? []));
|
||||
allAchievementsRaw.subscribe((v) => (achievementsRaw = v ?? []));
|
||||
});
|
||||
|
||||
const userStats = $derived(computeUserStats(skills, activities));
|
||||
const achievementStats = $derived(getAchievementStats(buildAchievementStatus(achievementsRaw)));
|
||||
const userStats = $derived(computeUserStats(allSkills.value, allActivities.value));
|
||||
const achievementStats = $derived(
|
||||
getAchievementStats(buildAchievementStatus(allAchievementsRaw.value))
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@
|
|||
let creating = $state(false);
|
||||
let newKeyName = $state('');
|
||||
let newKeyScopes = $state<{ stt: boolean; tts: boolean }>({ stt: true, tts: true });
|
||||
let newKeyRateLimit = $state(60);
|
||||
// Stored as string because the shared <Input> component binds to a
|
||||
// string value; we parseInt at submit time.
|
||||
let newKeyRateLimit = $state('60');
|
||||
let createdKey = $state<ApiKeyWithSecret | null>(null);
|
||||
let copied = $state(false);
|
||||
|
||||
|
|
@ -62,21 +64,17 @@
|
|||
const result = await apiKeysService.create({
|
||||
name: newKeyName.trim(),
|
||||
scopes,
|
||||
rateLimitRequests: newKeyRateLimit,
|
||||
rateLimitRequests: parseInt(newKeyRateLimit, 10) || 60,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
error = result.error;
|
||||
} else if (result.data) {
|
||||
createdKey = result.data;
|
||||
// Add to list (without the secret key)
|
||||
apiKeys = [
|
||||
...apiKeys,
|
||||
{
|
||||
...result.data,
|
||||
key: undefined as unknown as string, // Remove secret from local state
|
||||
},
|
||||
];
|
||||
// Add to list — strip the secret `key` field that only appears on
|
||||
// creation responses, so the local list matches the ApiKey shape.
|
||||
const { key: _omit, ...withoutSecret } = result.data;
|
||||
apiKeys = [...apiKeys, withoutSecret];
|
||||
}
|
||||
|
||||
creating = false;
|
||||
|
|
@ -110,7 +108,7 @@
|
|||
createdKey = null;
|
||||
newKeyName = '';
|
||||
newKeyScopes = { stt: true, tts: true };
|
||||
newKeyRateLimit = 60;
|
||||
newKeyRateLimit = '60';
|
||||
copied = false;
|
||||
}
|
||||
|
||||
|
|
@ -186,8 +184,8 @@
|
|||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="font-medium">{key.name}</span>
|
||||
<Badge variant="secondary">{key.scopes.join(', ')}</Badge>
|
||||
<Badge variant="outline">{key.rateLimitRequests}/min</Badge>
|
||||
<Badge variant="default">{key.scopes.join(', ')}</Badge>
|
||||
<Badge variant="info">{key.rateLimitRequests}/min</Badge>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-4 mt-1 text-sm text-muted-foreground flex-wrap"
|
||||
|
|
@ -200,7 +198,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
variant="danger"
|
||||
size="sm"
|
||||
loading={revoking === key.id}
|
||||
onclick={() => handleRevoke(key.id)}
|
||||
|
|
@ -240,7 +238,7 @@
|
|||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium line-through">{key.name}</span>
|
||||
<Badge variant="destructive">Revoked</Badge>
|
||||
<Badge variant="danger">Revoked</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 mt-1 text-sm text-muted-foreground">
|
||||
<code class="bg-muted px-2 py-0.5 rounded font-mono text-xs"
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
loading = true;
|
||||
|
||||
try {
|
||||
const result = await authStore.resetPassword(token, password);
|
||||
const result = await authStore.resetPasswordWithToken(token, password);
|
||||
|
||||
if (!result.success) {
|
||||
error = result.error || 'Failed to reset password';
|
||||
|
|
@ -116,7 +116,6 @@
|
|||
autocomplete="new-password"
|
||||
placeholder="Enter new password"
|
||||
required
|
||||
minlength={12}
|
||||
bind:value={password}
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
|
|
@ -138,7 +137,6 @@
|
|||
autocomplete="new-password"
|
||||
placeholder="Confirm new password"
|
||||
required
|
||||
minlength={12}
|
||||
bind:value={confirmPassword}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue