forked from EduCraft/curriculum-project-hub
feat(hub): drop redundant /admin/org/:slug path + release v0.0.35 (#11)
Silo hostname already carries tenancy. Admin SPA routes become /admin/..., legacy bookmarks redirect, login lands on /admin. Co-authored-by: Hong Jiarong <me@jrhim.com> Co-committed-by: Hong Jiarong <me@jrhim.com>
This commit is contained in:
+6
-4
@@ -2,6 +2,7 @@
|
||||
import { page } from '$app/state';
|
||||
import { api, type OrgMembership } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import { fmtCost, fmtNum, orgRoleLabel } from '$lib/format';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import StatCard from '$lib/components/StatCard.svelte';
|
||||
@@ -10,7 +11,8 @@
|
||||
import SwitchControl from '$lib/components/SwitchControl.svelte';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
let orgSlug = $derived(page.params.slug ?? '');
|
||||
const orgFromSession = $derived(resolveOrg($session.me, page.url.search));
|
||||
let orgSlug = $derived(orgFromSession?.slug ?? '');
|
||||
let org = $derived($session.me?.organizations.find((o) => o.slug === orgSlug) as OrgMembership | undefined);
|
||||
|
||||
let settings = $state<{ membersCanCreateProjects: boolean } | null>(null);
|
||||
@@ -96,7 +98,7 @@
|
||||
<h2 class="saas-section-title">用量概览</h2>
|
||||
<p class="saas-muted">totals 含全部 UsageFact;分账明细见用量页。</p>
|
||||
</div>
|
||||
<a class="saas-btn-secondary py-1.5! text-sm" href={`/admin/org/${orgSlug}/usage`}>完整用量报告</a>
|
||||
<a class="saas-btn-secondary py-1.5! text-sm" href={`/admin/usage`}>完整用量报告</a>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@@ -115,7 +117,7 @@
|
||||
<h3 class="text-sm font-semibold text-surface-800">消费来源(Top)</h3>
|
||||
<p class="saas-muted text-xs">模型完成 vs 外部能力,按成本降序前 5</p>
|
||||
</div>
|
||||
<a class="text-sm text-primary-700 hover:underline" href={`/admin/org/${orgSlug}/usage`}>查看全部分账</a>
|
||||
<a class="text-sm text-primary-700 hover:underline" href={`/admin/usage`}>查看全部分账</a>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="data-table">
|
||||
@@ -167,7 +169,7 @@
|
||||
{#each usage.projects as p}
|
||||
<tr>
|
||||
<td class="font-medium">
|
||||
<a class="hover:text-primary-700 hover:underline" href={`/admin/org/${orgSlug}/projects/${p.projectId}`}>
|
||||
<a class="hover:text-primary-700 hover:underline" href={`/admin/projects/${p.projectId}`}>
|
||||
{p.projectName}
|
||||
</a>
|
||||
</td>
|
||||
+4
-1
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { api, type CapabilityConnection } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import { fmtDate } from '$lib/format';
|
||||
import { Label } from 'bits-ui';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
@@ -8,7 +10,8 @@
|
||||
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
|
||||
const KNOWN_CAPABILITIES = [
|
||||
{ id: 'pdf_to_md_bundle', label: 'PDF → Markdown', description: '将 PDF 转换为带图片的 Markdown bundle(阿里云文档智能,含公式 LaTeX 识别)' },
|
||||
+4
-1
@@ -1,13 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { api, type CapacityDimension, type CapacityDimensionRow, type CapacityPolicyView } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import LoadingState from '$lib/components/LoadingState.svelte';
|
||||
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
|
||||
// Friendlier, user-facing labels. No spec jargon (墙钟 → 运行时长, etc.).
|
||||
const DIMENSION_LABELS: Record<CapacityDimension, string> = {
|
||||
+4
-1
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { api, type FeishuApplicationConnection } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import { fmtDate } from '$lib/format';
|
||||
import { Label } from 'bits-ui';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
@@ -8,7 +10,8 @@
|
||||
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
|
||||
let connection = $state<FeishuApplicationConnection | null>(null);
|
||||
let loading = $state(true);
|
||||
+4
-1
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { api, type OrgMember } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import { fmtDate } from '$lib/format';
|
||||
import { ORG_ROLES, ORG_ROLE_LABELS, PERMISSION_ROLE_LABELS } from '$lib/constants';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
@@ -10,7 +12,8 @@
|
||||
import SelectField from '$lib/components/SelectField.svelte';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
const roleItems = ORG_ROLES.map((r) => ({ value: r, label: ORG_ROLE_LABELS[r] }));
|
||||
const permHint = Object.values(PERMISSION_ROLE_LABELS).join(' / ');
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Legacy bookmarks: /admin/org/:slug[/...] → /admin[/...]
|
||||
* Tenancy lives on the silo host, not the path.
|
||||
*/
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
|
||||
onMount(() => {
|
||||
const raw = page.params.path ?? '';
|
||||
const segments = raw.split('/').filter(Boolean);
|
||||
// Drop the old org slug (first segment) when present.
|
||||
const rest = segments.length > 0 ? segments.slice(1).join('/') : '';
|
||||
const target = rest === '' ? '/admin' : `/admin/${rest}`;
|
||||
const q = page.url.search;
|
||||
void goto(`${target}${q}`, { replaceState: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="saas-status-panel">
|
||||
<p class="text-sm text-surface-600">正在重定向到新地址…</p>
|
||||
</div>
|
||||
+5
-4
@@ -2,6 +2,7 @@
|
||||
import { page } from '$app/state';
|
||||
import { api, type ExplorerData, type ExplorerFolder, type ExplorerProject, type OrgMembership } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import FolderTree from '$lib/components/FolderTree.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import LoadingState from '$lib/components/LoadingState.svelte';
|
||||
@@ -13,8 +14,8 @@
|
||||
import { fmtDate } from '$lib/format';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(($session.me?.organizations.find((o) => o.slug === slug) as OrgMembership | undefined) ?? null);
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
const isAdmin = $derived(!!org && (org.role === 'OWNER' || org.role === 'ADMIN'));
|
||||
|
||||
let data = $state<ExplorerData | null>(null);
|
||||
@@ -87,7 +88,7 @@
|
||||
projectName = '';
|
||||
projectFolder = '';
|
||||
showProjectModal = false;
|
||||
window.location.href = `/admin/org/${slug}/projects/${res.id}`;
|
||||
window.location.href = `/admin/projects/${res.id}`;
|
||||
} catch (err) {
|
||||
toastError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
@@ -207,7 +208,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each myProjects as p}
|
||||
<tr class="cursor-pointer" onclick={() => (window.location.href = `/admin/org/${slug}/projects/${p.id}`)}>
|
||||
<tr class="cursor-pointer" onclick={() => (window.location.href = `/admin/projects/${p.id}`)}>
|
||||
<td class="font-medium">{p.name}</td>
|
||||
<td class="font-mono text-xs">{p.binding ? `群 ${p.binding.chatId}` : '—'}</td>
|
||||
<td class="text-surface-700">{fmtDate(p.createdAt)}</td>
|
||||
+8
-5
@@ -9,6 +9,8 @@
|
||||
type ExplorerData,
|
||||
type ProjectUsageReport,
|
||||
} from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import {
|
||||
fmtCost,
|
||||
fmtDate,
|
||||
@@ -27,7 +29,8 @@
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
const projectId = $derived(page.params.projectId ?? '');
|
||||
const roleItems = PERMISSION_ROLES.map((r) => ({ value: r, label: PERMISSION_ROLE_LABELS[r] }));
|
||||
const roleChain = `${PERMISSION_ROLE_LABELS.READ} ⊂ ${PERMISSION_ROLE_LABELS.EDIT} ⊂ ${PERMISSION_ROLE_LABELS.MANAGE}`;
|
||||
@@ -109,7 +112,7 @@
|
||||
if (!confirm(`归档项目 ${proj?.name}?`)) return;
|
||||
try {
|
||||
await api.archiveProject(slug, projectId);
|
||||
window.location.href = `/admin/org/${slug}/projects`;
|
||||
window.location.href = `/admin/projects`;
|
||||
} catch (err) {
|
||||
toastError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
@@ -170,7 +173,7 @@
|
||||
{:else if proj}
|
||||
<div class="mb-2">
|
||||
<a
|
||||
href={`/admin/org/${slug}/projects`}
|
||||
href={`/admin/projects`}
|
||||
class="inline-flex items-center gap-1 text-sm text-surface-700 hover:text-primary-600"
|
||||
>
|
||||
<Icon name="arrow-left" class="h-4 w-4" />
|
||||
@@ -293,7 +296,7 @@
|
||||
{fmtTokens(projectUsage.inputTokens, projectUsage.outputTokens)}
|
||||
</p>
|
||||
</div>
|
||||
<a class="text-sm text-primary-700 hover:underline" href={`/admin/org/${slug}/usage`}>组织报告</a>
|
||||
<a class="text-sm text-primary-700 hover:underline" href={`/admin/usage`}>组织报告</a>
|
||||
</div>
|
||||
{#if projectUsage.breakdown.length === 0}
|
||||
<div class="px-5 py-4 text-sm text-surface-600">尚无 UsageFact。</div>
|
||||
@@ -368,7 +371,7 @@
|
||||
<td class="text-right">
|
||||
<a
|
||||
class="text-sm text-primary-700 hover:underline"
|
||||
href={`/admin/org/${slug}/sessions/${s.id}`}
|
||||
href={`/admin/sessions/${s.id}`}
|
||||
>
|
||||
详情
|
||||
</a>
|
||||
+4
-1
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { api, type ProviderConnectionRow } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import { fmtDate, providerModeLabel } from '$lib/format';
|
||||
import { Label } from 'bits-ui';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
@@ -8,7 +10,8 @@
|
||||
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
|
||||
let connections = $state<ProviderConnectionRow[]>([]);
|
||||
let loading = $state(true);
|
||||
+4
-1
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { api, type AgentRoleRow, type AgentModelRow, type AgentSkillRow } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import LoadingState from '$lib/components/LoadingState.svelte';
|
||||
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
|
||||
@@ -8,7 +10,8 @@
|
||||
import RoleCard from '$lib/components/RoleCard.svelte';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
|
||||
let roles = $state<AgentRoleRow[]>([]);
|
||||
let models = $state<AgentModelRow[]>([]);
|
||||
+6
-3
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { api, type SessionDetail, type SessionRunRow, type UsageFactRow } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import {
|
||||
fmtCost,
|
||||
fmtDate,
|
||||
@@ -16,7 +18,8 @@
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
const sessionId = $derived(page.params.sessionId ?? '');
|
||||
|
||||
let detail = $state<SessionDetail | null>(null);
|
||||
@@ -92,7 +95,7 @@
|
||||
<div class="mb-2">
|
||||
<a
|
||||
class="inline-flex items-center gap-1 text-sm text-surface-700 hover:text-primary-700"
|
||||
href={`/admin/org/${slug}/projects/${detail.project.id}`}
|
||||
href={`/admin/projects/${detail.project.id}`}
|
||||
>
|
||||
<Icon name="arrow-left" class="h-4 w-4" />
|
||||
返回项目 {detail.project.name}
|
||||
@@ -113,7 +116,7 @@
|
||||
<div>
|
||||
<dt class="text-surface-600">项目</dt>
|
||||
<dd class="mt-0.5">
|
||||
<a class="text-primary-700 hover:underline" href={`/admin/org/${slug}/projects/${detail.project.id}`}>
|
||||
<a class="text-primary-700 hover:underline" href={`/admin/projects/${detail.project.id}`}>
|
||||
{detail.project.name}
|
||||
</a>
|
||||
</dd>
|
||||
+4
-1
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { api, type AgentSkillRow, type SkillFileEntry } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import LoadingState from '$lib/components/LoadingState.svelte';
|
||||
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
|
||||
@@ -8,7 +10,8 @@
|
||||
import SkillEditor from '$lib/components/SkillEditor.svelte';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
|
||||
let skills = $state<AgentSkillRow[]>([]);
|
||||
let loading = $state(true);
|
||||
+4
-1
@@ -2,6 +2,8 @@
|
||||
import { Collapsible } from 'bits-ui';
|
||||
import { page } from '$app/state';
|
||||
import { api, type TeamRow, type TeamMemberRow } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import { fmtDate } from '$lib/format';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import LoadingState from '$lib/components/LoadingState.svelte';
|
||||
@@ -9,7 +11,8 @@
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
import { toastError, toastSuccess } from '$lib/toast';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
|
||||
let teams = $state<TeamRow[]>([]);
|
||||
let loading = $state(true);
|
||||
+5
-2
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { api, type UsageReport, type UsageBreakdownRow } from '$lib/api';
|
||||
import { session } from '$lib/session';
|
||||
import { resolveOrg } from '$lib/org';
|
||||
import {
|
||||
fmtCost,
|
||||
fmtDateOnly,
|
||||
@@ -15,7 +17,8 @@
|
||||
import ErrorBanner from '$lib/components/ErrorBanner.svelte';
|
||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||
|
||||
const slug = $derived(page.params.slug ?? '');
|
||||
const org = $derived(resolveOrg($session.me, page.url.search));
|
||||
const slug = $derived(org?.slug ?? '');
|
||||
|
||||
let usage = $state<UsageReport | null>(null);
|
||||
let loading = $state(true);
|
||||
@@ -224,7 +227,7 @@
|
||||
<td class="tabular-nums text-surface-600">{fmtTokens(p.inputTokens, p.outputTokens)}</td>
|
||||
<td class="tabular-nums">{fmtCost(p.costUsd)}</td>
|
||||
<td class="text-right">
|
||||
<a class="text-sm text-primary-700 hover:underline" href={`/admin/org/${slug}/projects/${p.projectId}`}>
|
||||
<a class="text-sm text-primary-700 hover:underline" href={`/admin/projects/${p.projectId}`}>
|
||||
查看项目
|
||||
</a>
|
||||
</td>
|
||||
Reference in New Issue
Block a user