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
2 changes: 1 addition & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ depends on the type of result:
| Type | `sortDate` is |
|------|---------------|
| Meeting | The meeting's start, as scheduled by the organization |
| Document | The start of the meeting at which the document was (last) discussed. A document that hangs off no meeting, such as a register entry (ingekomen stukken, raadsvragen), gets the date the source system last modified it |
| Document | The start of the meeting at which the document was (last) discussed. A register document (ingekomen stukken, raadsvragen, toezeggingen) gets the date of its register entry, the day the questions were asked; the entity detail also carries `dateModified`, the day the source system last changed the entry, which for questions is the day the answer was added |
| Motion | The meeting at which the motion was last discussed, else the date the source system gives it |
| Recording | The start of the meeting it records |

Expand Down
19 changes: 19 additions & 0 deletions src/ibabs/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,12 @@ export function normalizeIbabsRegisterDocuments(
const reference = parseAgendaPointReference(pickValue(values, ["Agendapunt"]));
const meeting = reference && meetings ? meetings.find(reference) : undefined;
const sourceIri = `ibabs://${source.ibabsSitename}/listentry/${entry.EntryId}`;
// A register entry has one date, the day it was entered: the day the
// questions were asked, for a raadsvragen entry. The answer is added to the
// same entry weeks later, and iBabs dates documents no further than that.
// The entry's last modification is the only trace of when the answer
// arrived, so every document carries it as date_modified (#294).
const dateModified = normalizeMutationDate(entry.MutationDate);

return detail.Documents.map((document) => ({
id: canonicalDocumentId(source, document.Id),
Expand All @@ -328,6 +334,7 @@ export function normalizeIbabsRegisterDocuments(
identifier_url: `ibabs://${source.ibabsSitename}/document/${document.Id}`,
file_name: document.FileName,
size_in_bytes: document.FileSize,
date_modified: dateModified,
last_discussed_at: meeting?.start_date ?? date,
is_referenced_by: meeting?.id ?? canonicalOrganizationId(source),
organization: canonicalOrganizationId(source),
Expand Down Expand Up @@ -395,3 +402,15 @@ export function normalizeIbabsDocuments(
raw: document,
}));
}

/** iBabs writes MutationDate as a local timestamp without a zone
* (`2026-03-25T12:52:40.897`). Kept to the day, at noon UTC, so the date
* survives any zone a reader is in. */
function normalizeMutationDate(value?: string): string | undefined {
const match = /^(\d{4}-\d{2}-\d{2})/.exec(value ?? "");
if (!match) {
return undefined;
}
const parsed = new Date(`${match[1]}T12:00:00Z`);
return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();
}
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,11 @@ export interface EntityContentResponse {
organization?: string;
date?: string;
sortDate?: string;
/** When the source system last changed this entity, formatted, when it
* says so. For an iBabs register entry: the day its last document, usually
* the answer, was added. */
dateModified?: string;
dateModifiedIso?: string;
markdownText?: string;
downloadUrl?: string;
contentType?: string;
Expand Down
46 changes: 46 additions & 0 deletions tests/registers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,49 @@ Deno.test("iBabs register entries reach the document path without a vote lookup"
"the failed download is reported against the document",
);
});

Deno.test("a register document carries the entry's last modification as date_modified", async () => {
// #294: raadsvragen are dated the day they were asked; the answer arrives
// in the same entry weeks later, and only the entry's MutationDate says when.
const { normalizeIbabsRegisterDocuments } = await import("../src/ibabs/normalize.ts");
const { getSource } = await import("../src/sources/index.ts");
const source = getSource("harderwijk");
const list = { ListId: "L1", ListName: "Schriftelijke vragen en antwoorden" };
const entry = { EntryId: "E1", MutationDate: "2026-03-25T12:52:40.897" };
const detail = {
EntryId: "E1",
Values: { Datum: "Feb 28 2026 12:00AM", Onderwerp: "Digitale archief Vervolg" },
Documents: [
{
Id: "D1",
FileName: "vragen.pdf",
DisplayName: "Schriftelijke vragen",
PublicDownloadURL: "https://x/1",
},
{
Id: "D2",
FileName: "antwoord.pdf",
DisplayName: "Beantwoording",
PublicDownloadURL: "https://x/2",
},
],
};
// deno-lint-ignore no-explicit-any
const docs = normalizeIbabsRegisterDocuments(
source as any,
list as any,
entry as any,
detail as any,
);
assert(docs.length === 2, "both documents of the entry");
for (const doc of docs) {
assert(
doc.last_discussed_at?.startsWith("2026-02-28"),
`the entry date stays the date, got ${doc.last_discussed_at}`,
);
assert(
doc.date_modified === "2026-03-25T12:00:00.000Z",
`the modification is the day the answer came, got ${doc.date_modified}`,
);
}
});
6 changes: 6 additions & 0 deletions web/search_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type SearchHit = {
chapters?: MeetingRecording["chapters"];
speakers?: MeetingRecording["speakers"];
is_referenced_by?: string;
date_modified?: string;
agenda?: MeetingAgendaItem[];
motion_type?: string;
status?: string;
Expand Down Expand Up @@ -1589,6 +1590,11 @@ export async function getEntityContent(
organization: displayOrganization(hit),
date: formatDate(hit.start_date),
sortDate: hit.start_date,
// When the source system last changed the entity, where it says so. For a
// register entry that is the day the answer was added (#294); the date
// above stays the day the entry was made, which is what people search by.
dateModified: hit.payload?.date_modified ? formatDate(hit.payload.date_modified) : undefined,
dateModifiedIso: hit.payload?.date_modified,
// Fall back to the attachment's text and file for a motion; its own
// fields are always empty.
markdownText: markdownText ?? motionAttachment?.markdownText,
Expand Down
5 changes: 5 additions & 0 deletions web/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2389,6 +2389,11 @@
<span class="pill">{detailItem.organization}</span>
<span class="pill pill--soft">{detailItem.entityTypeLabel}</span>
<span class="detail-sheet__date">{detailItem.date}</span>
{#if detailContent?.dateModified && detailContent.dateModifiedIso?.slice(0, 10) !== detailContent.sortDate?.slice(0, 10)}
<span class="detail-sheet__date detail-sheet__date--modified" title="Datum waarop het bronsysteem dit stuk voor het laatst heeft gewijzigd">
bijgewerkt {detailContent.dateModified}
</span>
{/if}
</div>

<div class="detail-sheet__header-actions">
Expand Down
4 changes: 4 additions & 0 deletions web/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1672,6 +1672,10 @@ select {
font-size: 0.95rem;
}

.detail-sheet__date--modified {
font-size: 0.85rem;
}

.button-with-icon {
display: inline-flex;
align-items: center;
Expand Down
Loading