Skip to content

CLARIN-DSpace v9/Restore the CLARIN bitstream-checksum column on the edit-bitstreams table - #1513

Open
milanmajchrak wants to merge 1 commit into
dtq-dev-9-basefrom
ufal/port-x02b-bitstream-checksum-column-9-base
Open

CLARIN-DSpace v9/Restore the CLARIN bitstream-checksum column on the edit-bitstreams table#1513
milanmajchrak wants to merge 1 commit into
dtq-dev-9-basefrom
ufal/port-x02b-bitstream-checksum-column-9-base

Conversation

@milanmajchrak

Copy link
Copy Markdown
Collaborator

What

Restores the CLARIN bitstream-checksum column on Edit item → Bitstreams, which the v9 upgrade dropped by taking both of its payload files wholesale from vanilla 9.3. Sync card X-02b (guard X1 sweep, 2026-09-10).

The table gains a fifth Sync | Checksum column. For every row it shows (a) whether the bitstream is stored in both stores at once (storeNumber === SYNCHRONIZED_STORES_NUMBER, i.e. 77 = local + S3) and (b) on demand, the checksum of the bitstream fetched over _links.checksum, comparing the database checksum against the active store — and, for a synchronized bitstream, against the other store too. The result is a tick or a cross plus a popover with the algorithm and value for DB / active store / sync store.

This is a textbook wiring-dropped: everything around the feature survived the upgrade and only the join was lost. The backend already has all of it on dtq-dev-9-base (BitstreamRest.storeNumber, @LinkRest(name = BitstreamRest.CHECKSUM), BitstreamCheckSumLinkRepository, BitstreamChecksumConverter), the frontend already has the whole data layer (bitstream-checksum-data.service.ts, bitstream-checksum.model.ts, bitstream-checksum.resource.ts — fork-only files that survived and are already v9-adapted), and all nine item.edit.bitstreams.checksum.* / headers.synchronized i18n keys are already in en.json5. What was missing was the field on the model and the component that reads it.

There is no single source commit to cherry-pick: the fork delta is measured against dspace-7.6.5, and both payload files were byte-identical with dspace-9.3 on this base, so the hunks were re-applied by hand from origin/dtq-dev.

Changes

File What
core/shared/bitstream.model.ts SYNCHRONIZED_STORES_NUMBER = 77, @autoserialize storeNumber, the checksum HALLink and the @link(BITSTREAM_CHECKSUM) follow-link
item-bitstreams/item-bitstreams.service.ts fifth ResponsiveColumnSizes(2,2,2,2,2) in getColumnSizes() and the four existing widths rebalanced to (3,3,3,3,3)/(3,3,3,3,3)/(2,2,2,2,2)/(2,2,2,2,2) so they still sum to 12
item-edit-bitstream-bundle.component.ts sanitizedBundleName, showChecksumValues, checkSum$, computedChecksum, loading, compareChecksums(), checksumsAreEqual(), isBitstreamSynchronized(), computeChecksum()
item-edit-bitstream-bundle.component.html the fifth header and cell, the empty fifth cell in the bundle row, and the #checksum / #checksumWarning / #computeChecksumInfo / #checksumPopover templates
item-bitstreams.service.stub.ts, …component.spec.ts five column sizes instead of four, a dedicated BitstreamChecksumDataService spy, and five new tests

src/assets/i18n/* is deliberately untouchedgit diff --numstat origin/dtq-dev-9-base HEAD -- src/assets/i18n/ is empty. The nine keys were already there with zero consumers (finding X-03); this PR gives them one back.

v9 adaptations, and why each one is not a literal copy

  • BitstreamChecksumDataService is injected directly, with no provider registration. The fork registers it in src/app/core/core.module.ts; that file does not exist on this base (v9 uses provide-core.ts) and the service is already @Injectable({ providedIn: 'root' }). Porting the registration would have added a duplicate provider.
  • computeChecksum(entry) and isBitstreamSynchronized(entry) take the table row. The fork reads itemBitstreamsService.getSelectedBitstream()?.bitstream?.bitstream and dereferences _links.checksum straight away. On v9 getSelectedBitstream() returns null when no row is selected (item-bitstreams.service.ts:191), and the button is in every row, not only the selected one — so the fork form throws TypeError: Cannot read properties of undefined (reading '_links') on the first click. checksumsAreEqual(checksum, entry) gained the same row argument for the same reason.
  • computeChecksum() returns early when the row has no checksum link, instead of calling the service with undefined.
  • _links.checksum is optional. The fork declares it required; on this base that breaks the strict object literal in the vanilla thumbnail.component.spec.ts (error TS2741: Property 'checksum' is missing). The link is CLARIN-only and the paired follow-link was already optional, so checksum?: HALLink is both correct and confined to this PR's own file — the alternative was editing an unrelated vanilla spec.
  • [ngTemplateOutletContext]="{ entry: entry }" and [popoverContext]="{ entry: entry }". The fork's #checksum and #checksumPopover declare let-entry="entry" but nothing ever passes a context, so its entry is always undefined.
  • @if / @for control flow (prefer-control-flow is an error here), *ngVar kept, Bootstrap 5 utilities (pl-1ps-1, pl-2ps-2, font-weight-boldfw-bold), and NgbPopover + VarDirective added to the standalone imports[] — without them [ngbPopover] and *ngVar are a silent no-op at runtime.
  • The five column ids and headers= are suffixed with sanitizedBundleName. This base emits duplicate DOM ids (name, description, format, actions) once an item has more than one bundle. The card asks for the scheme to be kept because the new column's headers= needs it.
  • The [queryParams] hunk that FE-28 added to this template is untouched (regression-guarded by an AC grep).

Deliberately not ported from the same fork diff, as out of scope for this card: the dont-break-out spans and the paginationControls id changes (a11y themes owned elsewhere), and getParentHierarchyTitle/getAllParentsRecursive (a different feature entirely).

Known fork limitation kept as-is: checkSum$, computedChecksum and loading are component-level, not per-row, so computing a checksum switches every row of that bundle into the "computed" state. That is the fork's own design; fixing it is a behaviour change beyond this card.

Why

Without the model field the component does not compile (Property 'storeNumber' does not exist on type 'Bitstream'); without the component the model field is dead code. The backend has served storeNumber and /checksum on this base since the v9 upgrade with nothing on the frontend consuming them, and nine translated i18n keys have been sitting unreferenced.

Testing

$ npm run test:headless -- --include='src/app/item-page/edit-item-page/item-bitstreams/**/*.spec.ts' --code-coverage=false
TOTAL: 73 SUCCESS

$ npm run test:headless -- --include='.../item-edit-bitstream-bundle.component.spec.ts' --code-coverage=false
TOTAL: 24 SUCCESS          # 19 on the base; the fork's own spec hunk adds no test at all

$ npm run lint:nobuild -- --quiet
All files pass linting.

$ NODE_OPTIONS=--max-old-space-size=4096 npm run build:prod
EXIT=0
$ grep -cE 'error TS|Module not found|NG[0-9]{4}|^Error:' build.log
8      # all NG8113 *warnings*, 4 unique x 2 builds, all in files this PR does not touch
       # (clarin-ref-citation, orcid-badge-and-tooltip, custom/home-page)

The fork ships zero tests for this feature, and its one spec hunk is six lines of dead DI plumbing that wires the BitstreamChecksumDataService provider to a different stub (useValue: itemBitstreamsService). All five tests here are new, and each was proven load-bearing by breaking the production code and watching the named test go red:

Production code reverted Test that went red
isBitstreamSynchronized() hardcoded to true should report a bitstream stored in both stores as synchronized (+ the synchronized-store test, which depends on it) → 2 FAILED, 22 SUCCESS
compareChecksums() reduced to a value-only comparison should treat checksums with the same value but a different algorithm as unequal1 FAILED, 23 SUCCESS
checksumsAreEqual() drops the synchronized-store comparison should compare the synchronized store as well, but only for a synchronized bitstream1 FAILED, 23 SUCCESS
computeChecksum() reads getSelectedBitstream() (the fork form) should request the checksum of the row it was given, not of the selected row1 FAILED, 23 SUCCESS
computeChecksum() loses the missing-href guard (the fork form) should do nothing when the row has no checksum link1 FAILED, 23 SUCCESS

The template itself is not covered by karma — the spec stubs viewContainerRef.createEmbeddedView, so ng-template #bundleView never renders. Its only real gate is the AOT build:prod above, which also confirms NgbPopover and VarDirective are genuinely used (an unused standalone import would have raised NG8113 for this component, and none did).

Not verified here

AC-X-02b-6 is a [live] criterion: it needs the branch merged, an FE deploy to dev-6:8603 and an admin session there to check the tick against store_number in the database and the /checksum endpoint. The card stays PARTIAL on that one AC.

🤖 Generated with Claude Code

@milanmajchrak
milanmajchrak force-pushed the ufal/port-x02b-bitstream-checksum-column-9-base branch from a71dda3 to 5dcc163 Compare September 10, 2026 15:43
@milanmajchrak

Copy link
Copy Markdown
Collaborator Author

Local gate output (sync card X-02b, §6.3 order) — head 5dcc163ba1, base dtq-dev-9-base 8ac588e84e

$ npm run lint:nobuild -- --quiet
All files pass linting.

$ npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\.model\.ts$' --circular --extensions ts ./
No circular dependency found!
   (npm run check-circ-deps itself fails on Windows: npm hands the script to cmd.exe, which
    splits the `|` inside the exclusion regex -> `'bundle' is not recognized`)

$ NODE_OPTIONS=--max-old-space-size=4096 npm run build:prod
EXIT=0
$ grep -cE 'error TS|Module not found|NG[0-9]{4}|^Error:' build.log
8      # 4 unique NG8113 *warnings* x 2 builds, all pre-existing and in files this PR does not
       # touch: clarin-ref-citation.component.ts:40, orcid-badge-and-tooltip.component.ts:31,
       # themes/custom/home-page.component.ts:30 and :31

$ npm run test:headless -- --include='src/app/item-page/edit-item-page/item-bitstreams/**/*.spec.ts' --code-coverage=false
TOTAL: 73 SUCCESS
$ npm run test:headless -- --include='.../item-edit-bitstream-bundle.component.spec.ts' --code-coverage=false
TOTAL: 24 SUCCESS          # 19 on the base
$ npm run test:headless -- --include='.../item-bitstreams.service.spec.ts' --code-coverage=false
TOTAL: 37 SUCCESS

Static AC greps (port head vs pre-port base)

AC-X-02b-1                                       PORT   BASE
export const SYNCHRONIZED_STORES_NUMBER = 77        1      0
storeNumber: number                                 1      0
checksum?: HALLink                                  1      0
@link(BITSTREAM_CHECKSUM)                           1      0
blob join: port=c5d200483f  dspace-9.3=29a80069db  base=29a80069db   -> DIVERGED
                                                   (base was byte-identical with vanilla)

AC-X-02b-3
max columns[N] in the template          port=4  base=3
new ResponsiveColumnSizes count         port=5  base=4   item-bitstreams.service.ts
                                        port=5  base=4   item-bitstreams.service.stub.ts
                                        port=5  base=4   item-edit-bitstream-bundle.component.spec.ts
pageSize: MAX_PAGE_SIZE (regression guard)  1

AC-X-02b-4   all nine previously orphaned i18n keys now have exactly one consumer (base: 0)
$ git diff --numstat origin/dtq-dev-9-base HEAD -- src/assets/i18n/
(empty)

AC-X-02b-5
NgbPopover + VarDirective in standalone imports[]   port=2  base=0
BS4 utilities (pl-N|pr-N|font-weight-bold|text-left|text-right)   0
ngTemplateOutletContext                             port=1  base=0
[queryParams]="{ itemId: item.uuid ...  (FE-28 regression guard)   1
*ngIf left in the template                          0

Negative controls — every new test proven load-bearing

Production code broken, spec untouched; git diff --stat checked non-empty before each run and
the tree checked clean after each git checkout -- ..

NC-b1  isBitstreamSynchronized() hardcoded to true          -> 2 FAILED, 22 SUCCESS
NC-b2  compareChecksums() reduced to a value-only compare   -> 1 FAILED, 23 SUCCESS
NC-b3  checksumsAreEqual() drops the sync-store comparison  -> 1 FAILED, 23 SUCCESS
NC-b4  computeChecksum() reads getSelectedBitstream()       -> 1 FAILED, 23 SUCCESS
NC-b5  computeChecksum() loses the missing-href guard       -> 1 FAILED, 23 SUCCESS

NC-b4 and NC-b5 revert to the fork's literal code, so they double as proof that the two crash
fixes in this PR are real rather than cosmetic.

Card errors found (for the sync ledger, not fixed here)

  1. AC-X-02b-1's third grep is checksum: HALLink and measures 0 on a correct port. The
    required key breaks the strict object literal in the vanilla thumbnail.component.spec.ts
    (error TS2741), so the port declares checksum?: HALLink. Corrected marker → 1.
  2. The card's step 6 says to wire the spec provider to a bitstreamChecksumService spy "rather
    than to itemBitstreamsService as the fork mistakenly does" — correct, and done; worth noting
    that the fork's spec hunk adds 0 it(), so nothing was lost by not porting it.

Not verified

AC-X-02b-6 is [live] — needs a merge, an FE deploy to dev-6:8603, an admin session and a
store_number query. Card stays PARTIAL on that AC.

…olumn on the edit-bitstreams table

Source: no single commit - the fork delta is measured against dspace-7.6.5, and the
v9 upgrade took both payload files wholesale from vanilla 9.3, so the fork hunks were
never applied (guard X1 sweep 2026-09-10, card X-02b).

- bitstream.model.ts: SYNCHRONIZED_STORES_NUMBER = 77, @autoserialize storeNumber,
  the checksum HALLink and the @link(BITSTREAM_CHECKSUM) follow-link.
- item-bitstreams.service.ts: fifth ResponsiveColumnSizes and the rebalanced widths
  (the MAX_PAGE_SIZE half of the same fork hunk had already landed).
- item-edit-bitstream-bundle.component.{ts,html}: the Sync | Checksum column, the four
  popover templates, and BitstreamChecksumDataService wired in directly (it is
  providedIn:'root' on this base; core.module.ts no longer exists).

v9 adaptations, each deliberate:
- computeChecksum(entry) and isBitstreamSynchronized(entry) take the table row instead of
  itemBitstreamsService.getSelectedBitstream(), which returns null on v9 when no row is
  selected - the fork form throws a TypeError from any unselected row's button.
- computeChecksum() returns early when the row has no checksum link.
- _links.checksum is optional: the fork's required key breaks the strict object literal in
  the vanilla thumbnail.component.spec.ts, and the link is genuinely CLARIN-only.
- @if / @for control flow, Bootstrap 5 utilities (ps-1, ps-2, fw-bold), NgbPopover and
  VarDirective added to the standalone imports[].
- [ngTemplateOutletContext] and [popoverContext] pass the row down; the fork sends none, so
  its let-entry is always undefined.
- The five column ids/headers are suffixed with sanitizedBundleName, as the card requires -
  9-base emits duplicate DOM ids when an item has more than one bundle.

Five new karma tests, each proven load-bearing by a production-code negative control.
en.json5 is untouched: all nine item.edit.bitstreams.checksum.* keys were already there
with no consumer (finding X-03).

Closes card X-02b.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@milanmajchrak
milanmajchrak force-pushed the ufal/port-x02b-bitstream-checksum-column-9-base branch from 5dcc163 to 7d530ae Compare September 10, 2026 16:16
@milanmajchrak

Copy link
Copy Markdown
Collaborator Author

CI triage — the tests (20.x) failure on 5dcc163ba1, fixed in 7d530ae866

Real failure, caused by this PR, and worth recording because it was my addition, not the fork's:

1) Edit Item > Bitstreams tab
     should pass accessibility tests:
   AssertionError: 1 accessibility violation was detected: expected 1 to equal 0

  id: 'aria-prohibited-attr'   impact: 'serious'   nodes: 4
  "Ensure ARIA attributes are not prohibited for an element's role"
  '<i class="fa-times fas" aria-label="Sync | Checksum"></i>'   (x4, one per bitstream row)

A bare <i> has the implicit generic role, which prohibits aria-label. The fork's markup carries
no aria attribute on that icon at all; I had added [attr.aria-label] while porting, thinking it
helped. It does not — axe rejects it, and the cell already gets its meaning from
headers="… synchronized-{{ sanitizedBundleName }}" pointing at the column header.

Fixed by using the same idiom as every other decorative icon in this template:

-  <i [class]="isBitstreamSynchronized(entry) ? 'fas fa-check' : 'fas fa-times'"
-    [attr.aria-label]="'item.edit.bitstreams.headers.synchronized' | translate"></i>
+  <i [class]="isBitstreamSynchronized(entry) ? 'fas fa-check' : 'fas fa-times'"
+    aria-hidden="true"></i>

No remaining aria-label in this template sits on anything but a <table> or a <button>.

Re-run locally after the fix:

$ npm run lint:nobuild -- --quiet
All files pass linting.
$ npm run test:headless -- --include='src/app/item-page/edit-item-page/item-bitstreams/**/*.spec.ts' --code-coverage=false
TOTAL: 73 SUCCESS

Known a11y gap left as-is (fork markup, no card owns it)

The compute-checksum control in the #checksum template is <a class="btn" (click)="computeChecksum(entry)">
with no href — so it is not keyboard-operable and has no accessible name. axe does not flag it (an <a>
without href is not a link role), and turning it into a <button> is a markup change neither this card
nor the source commit asks for. Flagged here for a future a11y card rather than changed silently.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant