diff --git a/dashboard/src/routes/users/+page.svelte b/dashboard/src/routes/users/+page.svelte
new file mode 100644
index 0000000..88fbbeb
--- /dev/null
+++ b/dashboard/src/routes/users/+page.svelte
@@ -0,0 +1,980 @@
+
+
+
+
+
+{#if banner}
+
{banner.message}
+{/if}
+
+{#if loadError}
+
+ {loadError}
+
+
+{:else if admins.length === 0}
+ No users yet. Invite one to get started.
+{:else}
+
+
+
Username
+
Role
+
Email
+
Status
+
Created
+
Last login
+
+
+ {#each filtered as row (row.id)}
+
+
+ {row.username}
+ {#if me && me.id === row.id}
+ (you)
+ {/if}
+
+
+
{row.email ?? '—'}
+
+ {#if row.is_active}
+ ● Active
+ {:else}
+ ○ Inactive
+ {/if}
+
+
{shortDate(row.created_at)}
+
{relative(row.last_login_at)}
+
+
+
+ {#if canDelete(row)}
+
+ {/if}
+
+
+ {/each}
+ {#if filtered.length === 0 && admins.length > 0}
+
No matches for "{search}".
+ {/if}
+
+{/if}
+
+
+{#if inviteOpen}
+ {
+ if (e.target === e.currentTarget && !invitePending) inviteOpen = false;
+ }}
+ >
+
+
+{/if}
+
+
+{#if editTarget}
+ {@const target = editTarget}
+ {
+ if (e.target === e.currentTarget && !editPending) editTarget = null;
+ }}
+ >
+
+
+{/if}
+
+
+{#if revealPassword}
+
+
+
+
+ {revealKind === 'invite' ? 'User created' : 'Password reset'} — {revealForUsername}
+
+
+
+ Save this password now — it will never be shown again. PiCloud cannot send email yet,
+ so copy it and share through your own channel.
+
+
+ {revealPassword}
+
+
+
+
+
+
+
+
+{/if}
+
+
+{#if deleteTarget}
+ {@const dt = deleteTarget}
+ (deleteTarget = null)}
+ >
+ {#if me && me.id === dt.id}
+
+ You're about to delete your own account. You'll be signed out
+ immediately and won't be able to sign back in.
+
+ {:else}
+
+ This permanently removes {dt.username}, all their sessions, and all
+ their API keys. This cannot be undone.
+
+ {/if}
+
+ If they're the only remaining owner or active admin the server will reject the request
+ with a 422 — promote/activate someone else first.
+
+
+{/if}
+
+