feat(assets): add dimension analysis and authorized user previews
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
Refactor asset drilldowns with combined dimension filters, parent summaries and progressive loading. Add the hidden /who read-only permission preview with scoped authorization, identity mapping and audit logging. Refine inventory counts and AI问数 presentation.
This commit is contained in:
@@ -58,13 +58,15 @@ export const gapStore = {
|
||||
[userId, fingerprint, demand, reason],
|
||||
);
|
||||
},
|
||||
async list(userId: string | null): Promise<DemandGap[]> {
|
||||
await ensure();
|
||||
await purge();
|
||||
async list(userId: string | null, options: { readOnly?: boolean } = {}): Promise<DemandGap[]> {
|
||||
if (!options.readOnly) {
|
||||
await ensure();
|
||||
await purge();
|
||||
}
|
||||
const [rows] = await pool.query<RowDataPacket[]>(
|
||||
`SELECT id,demand,reason,status,occurrences,
|
||||
DATE_FORMAT(first_seen,'%Y-%m-%dT%H:%i:%sZ') firstSeen, DATE_FORMAT(last_seen,'%Y-%m-%dT%H:%i:%sZ') lastSeen
|
||||
FROM bi_asset_ai_gaps ${userId ? "WHERE user_id = ?" : ""} ORDER BY last_seen DESC LIMIT 200`,
|
||||
FROM bi_asset_ai_gaps WHERE ${userId ? "user_id = ? AND " : ""}last_seen >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 90 DAY) ORDER BY last_seen DESC LIMIT 200`,
|
||||
userId ? [userId] : [],
|
||||
);
|
||||
return rows as DemandGap[];
|
||||
|
||||
@@ -59,6 +59,7 @@ router.get("/gaps", async (c) => {
|
||||
return c.json({
|
||||
items: await gapStore.list(
|
||||
c.req.query("scope") === "all" ? null : user.userId,
|
||||
{ readOnly: !!user.preview },
|
||||
),
|
||||
canManage: manage,
|
||||
});
|
||||
@@ -502,8 +503,10 @@ router.post("/ask", async (c) => {
|
||||
if (unanswered?.length) demand = `${unanswered.map(item => `${item.question}:${item.reason}`).join(";")}(原问题:${demand})`.slice(0, 600);
|
||||
let recorded = false;
|
||||
try {
|
||||
await gapStore.record(user.userId, demand, gapReason);
|
||||
recorded = true;
|
||||
if (!user.preview) {
|
||||
await gapStore.record(user.userId, demand, gapReason);
|
||||
recorded = true;
|
||||
}
|
||||
} catch {
|
||||
/* Report failure explicitly, never pretend it was persisted. */
|
||||
}
|
||||
@@ -518,8 +521,10 @@ router.post("/ask", async (c) => {
|
||||
if (!controller.signal.aborted) {
|
||||
let recorded = false;
|
||||
try {
|
||||
await gapStore.record(user.userId, demand, "generation");
|
||||
recorded = true;
|
||||
if (!user.preview) {
|
||||
await gapStore.record(user.userId, demand, "generation");
|
||||
recorded = true;
|
||||
}
|
||||
} catch {
|
||||
/* No raw error or query payload in logs. */
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ const owner = (u: AuthUser) =>
|
||||
u.depCode,
|
||||
u.permissionLevel,
|
||||
[...u.roles].sort(),
|
||||
[...(u.managerIds ?? [u.userId])].sort(),
|
||||
u.preview?.actorId ?? null,
|
||||
u.preview?.sessionId ?? null,
|
||||
]),
|
||||
)
|
||||
.digest("hex");
|
||||
|
||||
Reference in New Issue
Block a user