mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
Merge the document layout default and the air quality history into release/v1.42.0
This commit is contained in:
commit
57c44006ee
35 files changed
+1884
-76
No files matched your search
@@ -89,9 +89,27 @@ describe("GET /api/documents/inbound/layout", () => {
|
||||
const res = await callGet();
|
||||
expect(res.status).toBe(200);
|
||||
expect((await res.json()).data).toEqual(DEFAULT_DOCUMENTS_LAYOUT);
|
||||
expect(DEFAULT_DOCUMENTS_LAYOUT).toEqual({
|
||||
version: 1,
|
||||
view: "cards",
|
||||
arrangement: "flow",
|
||||
});
|
||||
expect(prisma.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps a saved stacked arrangement although the default is flow", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
documentsLayoutJson: {
|
||||
version: 1,
|
||||
view: "cards",
|
||||
arrangement: "stacked",
|
||||
},
|
||||
} as never);
|
||||
|
||||
const res = await callGet();
|
||||
expect((await res.json()).data.arrangement).toBe("stacked");
|
||||
});
|
||||
|
||||
it("serves the stored choice, repairing an unknown value field by field", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
documentsLayoutJson: { version: 1, view: "list", arrangement: "spiral" },
|
||||
@@ -101,7 +119,7 @@ describe("GET /api/documents/inbound/layout", () => {
|
||||
expect((await res.json()).data).toEqual({
|
||||
version: 1,
|
||||
view: "list",
|
||||
arrangement: "stacked",
|
||||
arrangement: "flow",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,6 +164,23 @@ describe("PUT /api/documents/inbound/layout", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("stores only the chosen field, so the untouched one keeps following the default", async () => {
|
||||
vi.mocked(prisma.user.findUnique).mockResolvedValue({
|
||||
documentsLayoutJson: null,
|
||||
} as never);
|
||||
|
||||
const res = await put({ version: 1, view: "list" });
|
||||
expect((await res.json()).data).toEqual({
|
||||
version: 1,
|
||||
view: "list",
|
||||
arrangement: "flow",
|
||||
});
|
||||
expect(prisma.user.update).toHaveBeenCalledWith({
|
||||
where: { id: "user-1" },
|
||||
data: { documentsLayoutJson: { version: 1, view: "list" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses an unknown value and an unknown key with 422 and writes nothing", async () => {
|
||||
const bad = await put({ version: 1, view: "carousel" });
|
||||
expect(bad.status).toBe(422);
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
DOCUMENTS_LAYOUT_ARRANGEMENTS,
|
||||
DOCUMENTS_LAYOUT_VIEWS,
|
||||
resolveDocumentsLayout,
|
||||
storedDocumentsLayoutChoices,
|
||||
type DocumentsLayout,
|
||||
} from "@/lib/documents/documents-layout";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -35,12 +36,12 @@ const layoutPutSchema = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
async function readLayout(userId: string): Promise<DocumentsLayout> {
|
||||
async function readStoredLayout(userId: string): Promise<unknown> {
|
||||
const row = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { documentsLayoutJson: true },
|
||||
});
|
||||
return resolveDocumentsLayout(row?.documentsLayoutJson);
|
||||
return row?.documentsLayoutJson;
|
||||
}
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
@@ -51,7 +52,7 @@ export const GET = apiHandler(async () => {
|
||||
const gate = await requireModuleEnabled(user.id, "inboundDocuments");
|
||||
if (!gate.enabled) return gate.response;
|
||||
|
||||
const layout = await readLayout(user.id);
|
||||
const layout = resolveDocumentsLayout(await readStoredLayout(user.id));
|
||||
annotate({
|
||||
action: { name: "documents.layout.read" },
|
||||
meta: { view: layout.view, arrangement: layout.arrangement },
|
||||
@@ -78,17 +79,20 @@ export const PUT = apiHandler(async (request: Request) => {
|
||||
}
|
||||
|
||||
// Preserve-when-absent: one stored read fills whichever field the body
|
||||
// left out.
|
||||
const existing = await readLayout(user.id);
|
||||
const next: DocumentsLayout = {
|
||||
version: 1,
|
||||
view: parsed.data.view ?? existing.view,
|
||||
arrangement: parsed.data.arrangement ?? existing.arrangement,
|
||||
// left out. Only fields the user has chosen are written, so a field never
|
||||
// picked keeps following the default (which can change between releases).
|
||||
const choices = {
|
||||
...storedDocumentsLayoutChoices(await readStoredLayout(user.id)),
|
||||
...(parsed.data.view ? { view: parsed.data.view } : {}),
|
||||
...(parsed.data.arrangement
|
||||
? { arrangement: parsed.data.arrangement }
|
||||
: {}),
|
||||
};
|
||||
const next: DocumentsLayout = resolveDocumentsLayout(choices);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { documentsLayoutJson: toJson(next) },
|
||||
data: { documentsLayoutJson: toJson({ version: 1, ...choices }) },
|
||||
});
|
||||
|
||||
annotate({
|
||||
|
||||
@@ -18,6 +18,7 @@ import { homeLocationSchema } from "@/lib/validations/environment";
|
||||
import { roundCoarse } from "@/lib/environment/open-meteo";
|
||||
import { sealLocation } from "@/lib/environment/location-cipher";
|
||||
import { enqueueEnvironmentFetch } from "@/lib/jobs/environment-fetch";
|
||||
import { enqueueAirQualityHistory } from "@/lib/jobs/environment-air-quality-history";
|
||||
|
||||
export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
@@ -67,6 +68,9 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
// Kick a lookback refresh so recent days populate promptly. No-ops cleanly
|
||||
// when no worker is bound; the nightly cron still covers it.
|
||||
await enqueueEnvironmentFetch({ userId: user.id });
|
||||
// And the past: air quality for every earlier day with entries, in the
|
||||
// background and inside the request budget. A no-op while it is off.
|
||||
await enqueueAirQualityHistory(user.id);
|
||||
|
||||
annotate({ action: { name: "environment.home.set" } });
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { prisma } from "@/lib/db";
|
||||
import { environmentPreferencesSchema } from "@/lib/validations/environment";
|
||||
import { isAirQualityOperatorDisabled } from "@/lib/environment/open-meteo-air-quality";
|
||||
import { enqueueEnvironmentFetch } from "@/lib/jobs/environment-fetch";
|
||||
import { enqueueAirQualityHistory } from "@/lib/jobs/environment-air-quality-history";
|
||||
|
||||
export const PATCH = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
@@ -57,6 +58,8 @@ export const PATCH = apiHandler(async (request: NextRequest) => {
|
||||
before?.environmentAirQualityEnabled === false
|
||||
) {
|
||||
await enqueueEnvironmentFetch({ userId: user.id });
|
||||
// Switched on: fill the past as well, every earlier day with entries.
|
||||
await enqueueAirQualityHistory(user.id);
|
||||
}
|
||||
|
||||
annotate({
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
* (the account switch, the operator switch, how many days carry air-quality
|
||||
* values), the newest stored day for the dashboard chips (with its air
|
||||
* quality only while the part is on), and every attribution line in
|
||||
* `attributions`.
|
||||
* `attributions`, and the progress of the air-quality history backfill
|
||||
* (`airQuality.history`).
|
||||
*/
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
@@ -23,6 +24,7 @@ import { isAirQualityOperatorDisabled } from "@/lib/environment/open-meteo-air-q
|
||||
import { environmentAttributionLines } from "@/lib/environment/air-quality-contract";
|
||||
import { readLocation } from "@/lib/environment/location-cipher";
|
||||
import { isAirQualityActive } from "@/lib/environment/service";
|
||||
import { readAirQualityHistoryState } from "@/lib/environment/air-quality-history";
|
||||
import { ENVIRONMENT_FETCH_QUEUE } from "@/lib/jobs/environment-fetch";
|
||||
import { readQueueFailureForUser } from "@/lib/jobs/job-failures";
|
||||
|
||||
@@ -54,6 +56,7 @@ export const GET = apiHandler(async () => {
|
||||
homeSince: true,
|
||||
timezone: true,
|
||||
environmentAirQualityEnabled: true,
|
||||
environmentAqHistoryJson: true,
|
||||
},
|
||||
}),
|
||||
prisma.environmentTravelLocation.findMany({
|
||||
@@ -157,6 +160,19 @@ export const GET = apiHandler(async () => {
|
||||
const accountEnabled = profile?.environmentAirQualityEnabled ?? true;
|
||||
const operatorDisabled = isAirQualityOperatorDisabled();
|
||||
const airActive = isAirQualityActive(accountEnabled);
|
||||
// The history backfill's progress, while the part is on: what the last run
|
||||
// counted, so the card can say how far the past has been filled.
|
||||
const historyState = airActive
|
||||
? readAirQualityHistoryState(profile?.environmentAqHistoryJson)
|
||||
: null;
|
||||
const history = historyState
|
||||
? {
|
||||
total: historyState.total,
|
||||
done: historyState.done,
|
||||
complete: historyState.complete,
|
||||
checkedAt: historyState.checkedAt,
|
||||
}
|
||||
: null;
|
||||
const latestYear = Number(
|
||||
(latestAir?.date ?? latest?.date ?? new Date().toISOString()).slice(0, 4),
|
||||
);
|
||||
@@ -219,6 +235,7 @@ export const GET = apiHandler(async () => {
|
||||
days: airDays,
|
||||
latestDate: latestAir?.date ?? null,
|
||||
domain: latestAir?.aqDomain ?? null,
|
||||
history,
|
||||
},
|
||||
latestDay,
|
||||
// null = the last background runs did not fail (or there is no queue to
|
||||
|
||||
Reference in new issue
Block a user