fix: harden the Homarr provider

This commit is contained in:
ajnart committed 2026-08-12 12:33:04 +02:00
1 parent 0774b02878
commit bffa3e723c
43 files changed
+515 -999

No files matched your search

+5 -35
View File
@@ -13,7 +13,6 @@ import type { HomarrCustomWidgetV2 } from "@homarr/custom-widgets/core";
import type {
WorkshopComment,
WorkshopAssistantActivity,
WorkshopAssistantUsage,
WorkshopReport,
WorkshopReportSummary,
@@ -30,7 +29,6 @@ import {
WORKSHOP_REQUEST_TIMEOUT_MS,
WORKSHOP_SCHEMA_BY_TYPE,
workshopCommentSchema,
workshopAssistantActivitySchema,
workshopAssistantUsageSchema,
workshopReportSchema,
workshopReportSummarySchema,
@@ -144,19 +142,6 @@ export interface WorkshopCommentRecord extends WorkshopBaseRecord {
expand?: { author?: WorkshopUserRecord };
}
export interface WorkshopAssistantActivityRecord extends WorkshopBaseRecord {
status: "processing" | "completed" | "failed";
model: string;
requestUnits: number;
inputTokens: number;
outputTokens: number;
totalTokens: number;
durationMs: number;
cost: number;
created: string;
updated: string;
}
export interface TypedWorkshopPocketBase extends PocketBase {
collection(idOrName: "users"): RecordService<WorkshopUserRecord>;
collection(idOrName: "submissions"): RecordService<WorkshopSubmissionRecord>;
@@ -165,7 +150,6 @@ export interface TypedWorkshopPocketBase extends PocketBase {
collection(idOrName: "reports"): RecordService<WorkshopReportRecord>;
collection(idOrName: "workshop_report_summaries"): RecordService<WorkshopReportSummaryRecord>;
collection(idOrName: "comments"): RecordService<WorkshopCommentRecord>;
collection(idOrName: "assistant_activity"): RecordService<WorkshopAssistantActivityRecord>;
collection(idOrName: string): RecordService;
}
@@ -289,6 +273,7 @@ export class WorkshopBackend {
}
public get currentUser(): WorkshopUser | null {
if (!this.pocketBase.authStore.isValid) return null;
const record = this.pocketBase.authStore.record;
if (!record) return null;
const parsed = workshopUserSchema.safeParse({
@@ -308,7 +293,10 @@ export class WorkshopBackend {
}
public async refreshAuth() {
if (!this.pocketBase.authStore.isValid) return null;
if (!this.pocketBase.authStore.isValid) {
this.pocketBase.authStore.clear();
return null;
}
try {
await this.pocketBase.collection("users").authRefresh();
return this.currentUser;
@@ -352,24 +340,6 @@ export class WorkshopBackend {
}
}
public async listAssistantActivity(limit = 10, signal?: AbortSignal): Promise<WorkshopAssistantActivity[]> {
try {
const result = await this.pocketBase
.collection("assistant_activity")
.getList(1, Math.min(Math.max(limit, 1), 50), {
sort: "-created",
signal: requestSignal(signal),
requestKey: null,
});
return result.items.flatMap((item) => {
const parsed = workshopAssistantActivitySchema.safeParse(item);
return parsed.success ? [parsed.data] : [];
});
} catch (error) {
throw workshopError(error, "Failed to load live Homarr provider activity");
}
}
public async list(options: WorkshopListOptions = {}): Promise<WorkshopPage<WorkshopSubmissionSummary>> {
try {
const page = options.page ?? 1;
+2 -30
View File
@@ -88,7 +88,7 @@ describe("WorkshopBackend", () => {
test("lets PocketBase own the complete GitHub popup flow", async () => {
mocks.authWithOAuth2.mockResolvedValue({
token: "token",
token: validWorkshopToken,
record: {
id: "user-id",
name: "octocat",
@@ -137,6 +137,7 @@ describe("WorkshopBackend", () => {
id: "author-id",
name: "octocat",
} as never);
expect(client.currentUser).toBeNull();
expect(client.authToken).toBeNull();
client.pocketBase.authStore.save(validWorkshopToken, { id: 42 } as never);
@@ -144,35 +145,6 @@ describe("WorkshopBackend", () => {
expect(client.authToken).toBeNull();
});
test("loads only valid anonymous provider activity records", async () => {
mocks.getList.mockResolvedValue({
items: [
{
id: "activity-id",
status: "completed",
model: "homarr/deepseek-v4-flash-latest",
requestUnits: 2,
inputTokens: 12,
outputTokens: 7,
totalTokens: 19,
durationMs: 250,
cost: 0.0001,
created: "2026-08-11 10:00:00.000Z",
updated: "2026-08-11 10:00:00.250Z",
},
{ id: "invalid" },
],
});
const client = new WorkshopBackend("https://workshop.example.com");
await expect(client.listAssistantActivity()).resolves.toHaveLength(1);
expect(mocks.getList).toHaveBeenCalledWith(
1,
10,
expect.objectContaining({ sort: "-created", requestKey: null, signal: expect.any(AbortSignal) }),
);
});
test("sends the exported schema while leaving initial revision ownership to PocketBase", async () => {
const client = new WorkshopBackend("https://workshop.example.com");
client.pocketBase.authStore.save(validWorkshopToken, { id: "author-id" } as never);
-15
View File
@@ -110,21 +110,6 @@ export const workshopAssistantUsageSchema = z.object({
});
export type WorkshopAssistantUsage = z.infer<typeof workshopAssistantUsageSchema>;
export const workshopAssistantActivitySchema = z.object({
id: z.string(),
status: z.enum(["processing", "completed", "failed"]),
model: z.string(),
requestUnits: z.number().int().positive(),
inputTokens: z.number().int().nonnegative().default(0),
outputTokens: z.number().int().nonnegative().default(0),
totalTokens: z.number().int().nonnegative().default(0),
durationMs: z.number().int().nonnegative().default(0),
cost: z.number().nonnegative().default(0),
created: z.string(),
updated: z.string(),
});
export type WorkshopAssistantActivity = z.infer<typeof workshopAssistantActivitySchema>;
const normalizeWorkshopSubmissionRecord = (value: unknown) => {
if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
const record = value as Record<string, unknown>;