Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions web/src/pages/MyKbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,16 @@ import {
Td,
Th,
Tr,
localDate,
} from "../ui";
import { fetchMe } from "../authSession";

const ymd = (iso: string) => iso.slice(0, 10);

function joinInfo(row: MyKb): string {
if (row.my_role === "owner") return S.account.deploymentAdmin;
if (row.joined_at) {
return row.added_by_name
? S.account.addedBy(row.added_by_name, ymd(row.joined_at))
: S.account.joinedOn(ymd(row.joined_at));
? S.account.addedBy(row.added_by_name, localDate(row.joined_at))
: S.account.joinedOn(localDate(row.joined_at));
}
return S.account.openToEveryone;
}
Expand Down
11 changes: 6 additions & 5 deletions web/src/pages/Review.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
SearchSelect,
Status,
cn,
localDate,
type ChipTone,
} from "../ui";

Expand Down Expand Up @@ -478,7 +479,7 @@ function MergeRow({
? S.review.mergedBy(merge.merged_by_name)
: S.review.mergedByAi}
{" · "}
{merge.created_at.slice(0, 10)}
{localDate(merge.created_at)}
{merge.reason ? ` · ${escalationText(merge.reason)}` : ""}
</div>
</div>
Expand Down Expand Up @@ -523,7 +524,7 @@ function precedentText(p: AgentPrecedent): string {
: p.action === "review.keep"
? S.review.agentPrecedentKept
: S.review.agentPrecedentMerged;
const line = `${p.left} ≟ ${p.right} · ${verb} · ${p.at.slice(0, 10)}`;
const line = `${p.left} ≟ ${p.right} · ${verb} · ${localDate(p.at)}`;
// 人写的那一句(0026)跟在后面:先例不只是结果
return p.why ? `${line} · “${p.why}”` : line;
}
Expand Down Expand Up @@ -596,8 +597,8 @@ function AgentRow({
<div className="mt-2 flex items-center gap-2">
<span className="text-fine text-ink-2">
{d.decided_by_name
? S.review.agentAnsweredBy(d.decided_by_name, (d.decided_at ?? d.created_at).slice(0, 10))
: d.created_at.slice(0, 10)}
? S.review.agentAnsweredBy(d.decided_by_name, localDate(d.decided_at ?? d.created_at))
: localDate(d.created_at)}
</span>
<div className="ml-auto flex items-center gap-2 shrink-0">
{/* 回答 agent 也能带一句理由(0026)——它走的正是人的裁决路径 */}
Expand Down Expand Up @@ -691,7 +692,7 @@ function DecisionRow({ e }: { e: ReviewHistoryEvent }) {
<span className="ml-auto shrink-0 text-small text-ink-2">
{e.actor_name ?? S.review.aiActor}
{" · "}
<span className="u-num">{e.created_at.slice(0, 10)}</span>
<span className="u-num">{localDate(e.created_at)}</span>
</span>
</div>
);
Expand Down
3 changes: 2 additions & 1 deletion web/src/pages/RulesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
Th,
THead,
Tr,
localDate,
} from "../ui";
import { toast } from "../toast";
import { expressionText, metadataOnly, metadataPatch } from "./ruleExpressions";
Expand Down Expand Up @@ -307,7 +308,7 @@ function History({ kbId, ruleId, attributes }: { kbId: string; ruleId: string; a
<span className="u-num text-small text-ink">{S.ontology.ruleVersion(v.seq)}</span>
{!v.superseded_at && <Chip tone="success">{S.ontology.ruleVersionCurrent}</Chip>}
<span className="u-num text-fine text-ink-2">
{S.ontology.ruleVersionSince(v.recorded_at.slice(0, 10), v.superseded_at ? v.superseded_at.slice(0, 10) : null)}
{S.ontology.ruleVersionSince(localDate(v.recorded_at), v.superseded_at ? localDate(v.superseded_at) : null)}
</span>
<span className="u-num text-fine text-ink-2">{S.ontology.ruleVersionStanding(v.derived_count)}</span>
</div>
Expand Down
13 changes: 7 additions & 6 deletions web/src/pages/Tokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ import {
Th,
THead,
Tr,
buttonLike,} from "../ui";
buttonLike,
localDate,
} from "../ui";
import { toast } from "../toast";
import { copyAndSay } from "../clipboard";

const ymd = (iso: string) => iso.slice(0, 10);
const EXPIRY_CHOICES = [30, 90, 365, 0] as const;

/** Claude Code / Claude Desktop 一族的 Streamable HTTP 写法。每个库一个端点(0014:
Expand Down Expand Up @@ -164,15 +165,15 @@ function TokenRow({
{t.kb_ids?.length ? S.account.nBases(t.kb_ids.length) : S.account.allBases}
</Td>
<Td className="u-num text-small text-ink-2">
{t.last_used_at ? ymd(t.last_used_at) : S.account.neverUsed}
{t.last_used_at ? localDate(t.last_used_at) : S.account.neverUsed}
</Td>
<Td className="u-num text-small text-ink-2">
{t.expires_at ? ymd(t.expires_at) : S.account.noExpiry}
{t.expires_at ? localDate(t.expires_at) : S.account.noExpiry}
</Td>
<Td className="u-num text-small text-ink-2">{ymd(t.created_at)}</Td>
<Td className="u-num text-small text-ink-2">{localDate(t.created_at)}</Td>
<Td className="text-right">
{revoked ? (
<Chip tone="danger">{S.account.revokedOn(ymd(t.revoked_at!))}</Chip>
<Chip tone="danger">{S.account.revokedOn(localDate(t.revoked_at!))}</Chip>
) : arm ? (
<Button variant="danger" size="sm"
disabled={busy}
Expand Down
73 changes: 73 additions & 0 deletions web/src/pages/accountRecordDates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { createElement, type FunctionComponent } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { beforeAll, describe, expect, it, vi } from "vitest";
import type { Kb, MyKb, TokenView } from "../api";
vi.mock("../kb", () => ({
useKb: () => ({
kbs: [],
workspace: { id: "ws", org_id: "org", name: "Team", created_at: "2026-01-01T00:00:00Z" },
setKb: () => {},
}),
}));
vi.mock("@tanstack/react-router", async (original) => ({
...(await original<typeof import("@tanstack/react-router")>()),
useNavigate: () => () => {},
useSearch: () => ({}),
}));
import { MyKbs } from "./MyKbs";
import { Tokens } from "./Tokens";

// 上海早上八点前,UTC 还是前一天:记录时刻按 UTC 切出日子,看的人就看到前一天
beforeAll(() => {
process.env.TZ = "Asia/Shanghai";
});

function render(page: FunctionComponent, data: [string[], unknown][]) {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
for (const [key, value] of data) client.setQueryData(key, value);
return renderToStaticMarkup(createElement(QueryClientProvider, { client, children: createElement(page) }));
}

const days = (html: string) => html.match(/\d{4}-\d{2}-\d{2}/g);

describe("record moments on the account pages", () => {
it("dates tokens by the reader's calendar, not by UTC", () => {
// 10 月 16 日早上 07:30 发的 30 天令牌,11 月 15 日 07:30 到期,07:45 用过
const laptop: TokenView = {
id: "t1", name: "laptop", token_prefix: "utp_pat_ab12", scope: "read", kb_ids: null,
created_at: "2026-10-15T23:30:00Z", expires_at: "2026-11-14T23:30:00Z",
last_used_at: "2026-10-15T23:45:00Z", revoked_at: null,
};
// 10 月 1 日凌晨 04:00 发的旧令牌,16 日早上 06:10 撤掉
const old: TokenView = {
...laptop, id: "t2", name: "old", token_prefix: "utp_pat_cd34",
created_at: "2026-09-30T20:00:00Z", expires_at: null, last_used_at: null,
revoked_at: "2026-10-15T22:10:00Z",
};
const html = render(Tokens, [[["tokens"], { tokens: [laptop, old] }]]);
// 一行一行:用过、到期、发放;撤掉的那把只有发放和撤销
expect(days(html)).toEqual(["2026-10-16", "2026-11-15", "2026-10-16", "2026-10-01", "2026-10-16"]);
});

it("dates a membership by the reader's calendar, not by UTC", () => {
const kb: Kb = {
id: "kb1", workspace_id: "ws", name: "Research", kind: "knowledge", description: null,
visibility: "restricted", is_default: false, auto_extend_ontology: true,
materialize_inferences: false, auto_type_resolution: false, governance: false,
governance_since: null, inference_interval_minutes: 60, last_inference_at: null,
ontology_lang: "en",
};
// Alice 在上海早上 07:30 把我加进来;另一个库是我自己在零点整加入的
const research: MyKb = {
kb, my_role: "editor", joined_at: "2026-10-15T23:30:00Z", added_by_name: "Alice",
doc_count: 3, member_count: 2,
};
const notes: MyKb = {
...research, kb: { ...kb, id: "kb2", name: "Notes" },
joined_at: "2026-10-15T16:00:00Z", added_by_name: null,
};
const html = render(MyKbs, [[["myKbs", "ws"], { kbs: [research, notes] }]]);
expect(days(html)).toEqual(["2026-10-16", "2026-10-16"]);
});
});
Loading