Skip to content

Pin migration notes to release 1.14.0 - #1492

Closed
sven1103-agent wants to merge 11 commits into
mainfrom
chore/pin-migration-scripts-to-release-1.14.0
Closed

Pin migration notes to release 1.14.0#1492
sven1103-agent wants to merge 11 commits into
mainfrom
chore/pin-migration-scripts-to-release-1.14.0

Conversation

@sven1103-agent

Copy link
Copy Markdown
Collaborator

Moves all pending migration notes to a dedicated release 1.13.0 -> 1.14.0 section

sven1103-agent and others added 11 commits June 19, 2026 15:30
…akeholder documents (#1480)

* Add init files for agent work

* Add new user stories for the account settings

* feat(): add Associated Datasets UI prototype

Add a pure UI prototype for the 'Connect datasets with research projects'
feature (Stories 01-08), demonstrating search, connect, view, sync, and
remove interactions for open and restricted datasets on InvenioRDM
instances (Zenodo, FDAT).

Files added:
- datamanager-app/.../demo/AssociatedDatasetsDemo.java:
  Development-profile-only Vaadin view at /test-view/associated-datasets
  with a Public/Restricted tab layout, mock data, and inline actions.
- docs/feature-vision-associated-datasets.md: Reformatted feature vision
  document with background, scope, and user stories.
- ui-screenshot(-restricted).png: Visual reference of the prototype
  for PR review.

The demo uses existing project UI patterns (Section, ActionBar, InfoBox,
Tag, TabSheet) and requires no real backend integration.

* docs: add ADR infrastructure, stakeholder documents, and InvenioRDM prototype

- ADR infrastructure: introduce docs/adr/ with MADR template (from
  https://adr.github.io/madr/), README with rules and index, and
  LICENSE. Provides a lightweight, append-only log for recording
  significant architectural decisions.
- Governance: update AGENTS.md §11.1 (Story Lifecycle) to define the
  two decoupled surfaces (stakeholder space / developer space) and the
  PO as handoff actor. Wire ADR references into docs/requirements.md
  and docs/requirements-guide.md.
- Stakeholder documents: add feature brief and resource information
  spec for the InvenioRDM dataset connection feature.
- Prototype: extend AssociatedDatasetsDemoV2 with account settings
  workflow for InvenioRDM credential management.

* docs: note FAIR Signposting mechanism has been replaced with API-based integration

The InvenioRDM FAIR Signposting serialization bug is upstream and will
not be ready in time for a Data Manager feature delivery. The current
integration uses the InvenioRDM OpenAPI directly. Correction notes were
added to the markdown renderings of the stakeholder brief to preserve
the original wording while clearly flagging the change.

* fix: revert accidental removal of production dialog.css styles

The file was modified on the prototype branch with ~868 lines removed.
Those styles are used by production dialogs across the application and
must not be removed. Revert to the origin/development version of the
file.

Prototype-specific CSS additions, if needed, belong in a prototype-only
stylesheet — not in the production dialog.css component stylesheet.

* Update bundle

* Remove development artifacts

* Remove development artifact: ui-screenshot.png

---------

Co-authored-by: Sven Fillinger <sven.fillinger@qbic.uni-tuebingen.de>
…n) (#1481)

Capture architectural decisions for the InvenioRDM dataset connection feature (#1466) as four Architecture Decision Records, drafted after a structured design interview with the team.

---------

Co-authored-by: Sven Fillinger <sven.fillinger@qbic.uni-tuebingen.de>
* feat(FEAT-DATSET-01): connect associated InvenioRDM datasets with projects (draft)

Part of #1467 / #1466. Honors ADRs 0001, 0002, 0003, 0004.

Note: commit.gpgsign=true is set in ~/.gitconfig, but the signing
key ~/.ssh/id_qbic is missing locally. This commit is therefore
unsigned. Re-enable signing after the key is restored.

What works so far:
- New 'DATASETS' link on project navigation (below SUMMARY)
- @route 'projects/:projectId?/datasets' under ProjectMainLayout
- ConnectedResourcesComponent with expandable detail rows
- Empty state when project has no connected datasets
- Sliding connect sidebar (per prototype V2) with:
    - InvenioRDM instance selector (Zenodo, FDAT from config)
    - Search field + Search + Clear
    - Multi-select results grid with lazy-loading + loading indicator
    - Optional experiment association (AC9)
    - Connect Selected footer button
- Domain aggregate: AssociatedDataset + sealed ResourceMetadata hierarchy
  (source-specific metadata in JSON blob; 4 universal SQL columns)
- Application service + DatasetSource port with actingUserId
  (decryption boundary stays in infrastructure per ADR-0002 D1)
- InvenioRDM client calling REST API v12 (bounded retry, 5xx/429
  only; 4xx not retried)
- JPA repository + DB DDL (sql/migrations/create-associated-dataset.sql)
- 3-tier migration docs (NEXT.md + README index)

* fix(datasets): gate sidebar grid fetch on explicit user action

The sidebar was still blocking on open because:
  - loadInstances() auto-selects the first repository on open
  - that auto-selection fires the value-change listener
  - which calls refreshSearchResults() → refreshAll()
  - which makes Vaadin's lazy grid ask for the first page
  - fetchPage then makes a synchronous HTTP call to InvenioRDM
  - the whole chain runs on the server thread, freezing the UI

Introduce a `searchInitiated` flag set ONLY by explicit user actions
(Search button click, Enter in the search field, Clear button click).

- fetchPage() short-circuits to `Stream.empty()` when the flag is
  false — no HTTP call is made for the grid's auto first-page fetch
- The instance-selector value-change listener also gates on the flag
  so the auto-select during open() does not refresh the grid
- `close()` resets the flag, so each new open is instant again
- Scrolling still works lazily once a search has been initiated

AC3 (paginated results when no query) stays satisfied: the very
first search can be with an empty search term.

* Fix grid data provider query contract violation

Add query.getOffset() and query.getLimit() calls before early returns
in ConnectDatasetSidebar.fetchPage() to satisfy Vaadin's contract
requirements.

This resolves the 'getLimit() or getPageSize() method has not been
called' error that occurred when the data provider returned early
before accessing query parameters.

* feat(datasets): Add welcome message overlay for empty grid states

Empty grid states are user-unfriendly and don't guide users on what to do next.

Added a welcome message overlay that displays before the user initiates their first search:
- Search icon with large font size
- Title: 'Search for datasets'
- Subtitle explaining what the search does
- Centered, with proper spacing and secondary text colors

The overlay:
- Uses absolute positioning within the results container
- Covers the empty grid area without affecting layout
- Disappears when user performs any search action:
  - Clicks Search button
  - Presses Enter in search field
  - Clicks Clear button
- Has higher z-index to appear above the grid

This improves UX by:
- Providing clear next steps to users
- Reducing confusion about the empty state
- Maintaining smooth interactions (no layout shifts)
- Keeping the sidebar opening instant (no premature HTTP calls)

Honors FEAT-DATSET-01 UX requirements.

* refactor(datasets): implement async search with progressive UI feedback

Refactored ConnectDatasetSidebar to move HTTP fetch out of the lazy
data provider and into an async background thread. This allows the
loading indicator to be shown immediately via @Push, providing better
user feedback during search operations.

Key changes:
- Removed HTTP call from fetchPage() - now only slices cachedResults
- Made refreshSearchResults() fully async with background Thread
- Added cachedResults List<SearchHit> to store search results
- Added searchInProgress flag to prevent duplicate concurrent searches
- Search controls (button, field, instanceSelector) disabled during search
- Loading indicator shown immediately, hidden when results are pushed back
- Welcome message hidden when search is initiated

This resolves the UX issue where the loading indicator was not visible
because everything happened in a single Vaadin response. Now the loading
state is pushed immediately, then the results are pushed when the HTTP
call completes.

Implements Option A: load all results in one background fetch with
page=0, pageSize=100. Virtual scrolling still works via cachedResults.

* feat(datasets): improve search loading indicator UX

Replaced the basic ProgressBar with a more user-friendly loading overlay:
- Animated hourglass spinner (⌛) with CSS rotation
- Clear 'Searching for datasets...' message
- Helpful hint 'This may take a few seconds'
- Full-width overlay with centered content
- Higher z-index (2) to ensure visibility

This addresses the UX issue where the loading indicator was not visible
due to being rendered in the same response as the data. The async search
implementation from the previous commit ensures the loading state is
pushed immediately via @Push, making this indicator now effective.

The visual feedback now clearly communicates to users that:
1. A search is in progress
2. The operation may take some time
3. The system is actively working on their request

* fix(datasets): capture user identity before async thread to fix security context issue

The async search implementation was failing because SecurityContextHolder uses
ThreadLocal storage, which is not propagated to background threads. When we moved
the HTTP call to a background thread, the security context became unavailable.

Solution: Capture the current user ID in the main thread BEFORE launching the
background thread, then pass the captured value to the background thread. This
ensures the security context is accessible when we need to identify the user
for the dataset search operation.

* fix(datasets): use security-context-aware task executor for async search

Replace manual Thread creation with CompletableFuture.supplyAsync using
the DelegatingSecurityContextAsyncTaskExecutor from Spring's AsyncConfig.

This ensures the security context is properly propagated to the async
thread, fixing the 'Search failed: Cannot invoke
Authentication.getPrincipal()' error that occurred when searching for
datasets.

The taskExecutor bean is already configured in AsyncConfig to wrap the
ThreadPoolTaskExecutor with DelegatingSecurityContextAsyncTaskExecutor,
which automatically propagates the SecurityContext to async threads.

Changes:
- Inject Executor (taskExecutor) into ConnectDatasetSidebar constructor
- Use CompletableFuture.supplyAsync() with taskExecutor instead of
  new Thread()
- Add proper error handling with exceptionally() callback
- Update AssociatedDatasetsMain to inject and pass the taskExecutor

This follows the Vaadin idiomatic approach for async operations with
security context propagation.

* fix: Use relevance-based sorting for search queries in InvenioRDM client

When searching for datasets (e.g., 'Data Manager QBiC'), the results were
returned sorted by newest first instead of by relevance. This caused
inconsistency with Zenodo's UI where the most relevant result appeared first.

The issue was that buildSearchUrl() always appended '&sort=newest', which
forced the API to sort by creation date even when a search query was provided.

Fix: Only use '&sort=newest' when there's no search query (browsing mode).
When searching, rely on the API's default relevance-based sorting to match
Zenodo's behavior.

This ensures users get the same result ordering as in Zenodo's web UI.

* fix: Ensure welcome message shows on sidebar open, not loading indicator

Reset UI state in open() method to ensure the sidebar always starts
with the welcome message visible, not the loading indicator.

Changes:
- Explicitly reset searchInitiated = false
- Hide loading indicator
- Show welcome message
- Refresh grid data provider
- Enable all controls

This ensures users see the 'Search for Datasets' prompt when opening
the sidebar, with the 'Searching...' overlay only appearing when they
actually initiate a search.

* fix(datasets): Apply overlay class name to experiment selector dropdown

The experiment selector dropdown was being clipped by the sidebar's z-index,
making it impossible to see the full experiment list. This issue was already
fixed for the dataset repository dropdown, so we apply the same solution:
setOverlayClassName("connect-dataset-sidebar-overlay")

The CSS class is defined in the theme and ensures the dropdown overlay
renders above the sidebar panel.

* feat(FEAT-DATSET-01): resolve UUIDs to human-readable display names

Introduce an API-contract DTO to replace the raw domain entity leaking
into the UI layer and fix several related UX issues.

UI domain-entity layer violation fix:
- New ConnectedDatasetView DTO in the associated_dataset application
  package, carrying resolved display names and flat source-specific
  fields so the view never touches domain model classes.
- AssociatedDatasetService.listConnectedDatasetViews() replaces the old
  listConnectedDatasets(). It collects distinct user IDs and experiment
  IDs from the batch, resolves each exactly once via
  UserInformationService and ExperimentInformationService, then maps
  aggregate to DTO. O(U + E) lookups regardless of row count.
- ConnectedResourcesComponent now consumes ConnectedDatasetView and has
  no imports from the domain model package (AccessLevel remains via
  enum).

UX improvements:
- Connected By and Linked Experiment detail rows now show the users
  full name and the experiments display name instead of raw UUIDs.
- Linked Experiment name is a clickable anchor (target=blank) that
  opens the experiment view in a new browser tab via
  AppRoutes.ProjectRoutes.EXPERIMENT.
- Sidebar panel width is now responsive:
    width: min(55 percent, 720px); min-width: 460px; max-width: 100vw
  Title text is clamped to 2 lines with a CSS ellipsis and full title
  exposed as a native HTML title attribute for hover tooltip.
- Connected Resources view now occupies the full available content width
  (new .main.project.datasets grid override collapses the two-column
  project grid into a single 1fr column).

InvenioRDM version resolution fix (single-version records):
- Zenodo does not populate metadata.version for records with only one
  version. The authoritative source is metadata.relations.version[]
  which is always present but 0-based. version() and recordVersion()
  now fall back to relations.version[0].index + 1, producing "v1" for
  the first (and possibly only) version.
- Removed the bogus Versions DTO and its JsonProperty("versions")
  mappings on Hit/RecordResponse: links.versions is a URL string in
  the InvenioRDM response and never useful to map.

Co-authored-by: sven1103-agent <sven.broja@qbic.uni-tuebingen.de>

* feat(FEAT-DATSET-01): wire toast notifications and add collaborator email directive

Toast notification factory wiring
- Inject MessageSourceNotificationFactory into ConnectDatasetSidebar
  via AssociatedDatasetsMain (passed through constructor).
- Replace raw "new Notification(...)" calls with factory-produced
  Toasts (BOTTOM_START positioning, i18n, correct theme alignment).
- Add four toast keys to toast-notifications.properties:
    dataset.connected.success (success, count parameter)
    dataset.connected.failure (error, count parameter)
    dataset.experiments.failed (error, static)
    dataset.search.failed (error, static)
- Show the sidebar is now fully integrated with the apps notification
  system instead of rendering ad-hoc BOTTOM_END Vaadin notifications.

Email notification for collaborators (Task 6)
- New directive InformProjectCollaboratorsAboutDatasetConnection
  (package: life.qbic.projectmanagement.application.policy.directive)
  that subscribes to AssociatedDatasetConnectedEvent.
- On connect, every collaborator on the project is looked up via
  ProjectAccessService.listCollaborators() and emailed, with the actor
  excluded from the recipient list (no self-notification).
- Emails are dispatched via JobRunr background jobs so the domain
  handler does not block on SMTP.
- New Messages.datasetConnectedToProject(...) template.
- New AssociatedDatasetConnectedPolicy wires the directive via
  DomainEventDispatcher, registered as a @bean in AppConfig.java.

AC8 is now fully satisfied.

* feat(FEAT-DATSET-01): make dataset connect non-blocking via reactor + Option B UX

Async service (reactor)
- Introduce AsyncAssociatedDatasetService API and
  AsyncAssociatedDatasetServiceImpl implementing non-blocking single and
  batch connect endpoints on top of the blocking
  AssociatedDatasetService via Mono.fromCallable +
  Schedulers.boundedElastic().
- connectDatasets(...) fans out with a parallelism cap of 3, matching
  the anonymous per-IP rate limit of public InvenioRDM instances (e.g.
  Zenodo).
- Per-request 30s timeout; on timeout or network failure the Mono
  emits a ConnectDatasetResponse carrying ConnectDatasetError so the
  reactive stream never terminates in error and the UI can tally
  successes vs failures cleanly.
- Wire the async service as a Spring @bean in AppConfig.java.

Option B UX for dataset connect
- The ConnectDatasetSidebar now stays open during a batch connect and
  shows a spinner overlay on the results grid ("Connecting
  datasets..."), leaving the close button reachable.
- Per-row state: row card opacity drops to 0.5 while connecting; a
  green check-mark appears on success and a red cross on failure —
  rendered immediately as each Mono emits onNext.
- Atomic success/failure counters are updated off the UI thread and
  flushed back via UiHandle.onUiAndPush for safe Vaadin access.
- After all responses have arrived (onComplete), a 600ms settling
  delay runs before the overlay hides, the grid refreshes (status
  map cleared), the sidebar closes, the success/failure toasts fire,
  and ConnectedResourcesComponent is refreshed via the
  DatasetsConnectedEvent — guaranteeing the list reflects the new
  state before the user sees it.

* refactor(FEAT-DATSET-01): move async connect into AssociatedDatasetService + fix SecurityContext propagation

- Delete redundant AsyncAssociatedDatasetService and its implementation.
  They duplicated the API contract for no abstraction benefit.
- Expose connectDatasetAsync(ConnectDatasetRequest): Mono<...> and
  connectDatasets(List<ConnectDatasetRequest>): Flux<...> directly on
  AssociatedDatasetService, keeping the sync connectDataset method
  intact (matches the precedent where AsyncProjectService owns its
  interface but AssociatedDatasetService just needs one extra
  transport method).
- Fix SecurityContext propagation bug that caused per-record
  AccessDenied on the worker thread. The new methods use
  .contextWrite(ReactiveSecurityContextUtils.reactiveSecurity(securityContext))
  - the exact pattern used by AsyncProjectServiceImpl - so Spring
  Security reactive AOP correctly sets the ThreadLocal before
  @PreAuthorize evaluates on connectDataset.
- Update ConnectDatasetSidebar, AssociatedDatasetsMain, and
  AppConfig to consume AssociatedDatasetService directly.
- Inner records ConnectDatasetRequest and ConnectDatasetResponse
  live on AssociatedDatasetService now.

* feat(FEAT-DATSET-01): persist SUCCESS indicators until user resets sidebar

Strategy B: connected rows now stay visually marked until the user
either closes the sidebar or starts a new search.

Per-row state changes:
- SUCCESS rows get a success-coloured background tint (var(--lumo-success-color-10pct))
  and a leading "Connected ✓" Tag badge (SUCCESS colour) so they are
  distinguishable at a glance even before reading the toast banner.
- ERROR rows keep the red cross icon in the top row.
- PENDING rows remain at 0.5 opacity during connection.

Auto-deselect on success:
- When a connect succeeds the corresponding search result is
  auto-deselected from the multi-select grid so it cannot be re-
  connected by a subsequent "Connect Selected" click.
- The footer selection counter ticks down live, giving the user
  concrete progress feedback (5 → 4 → 3 → 2 left).

Lifecycle of the status map:
- REMOVED `rowConnectionStatuses.clear()` from the Flux `onComplete`
  (it was the root cause of the brief flash — the clear ran before
  the grid refresh, so icons vanished ~600ms before the toast fired).
- ADDED the clear to `close()` alongside `resultsGrid.deselectAll()`
  so state is properly discarded when the sidebar is dismissed.
- ADDED the clear to `refreshSearchResults()` (new search) so stale
  connection state from a previous connect does not bleed into a
  fresh InvenioRDM result set.

Removed the explicit `close()` call from `onComplete` — the sidebar now
stays open long enough for the user to confirm the connected rows, and
closes naturally when the toast auto-dismisses (~5s) or on user click.

* fix(FEAT-DATSET-01): replace CompletableFuture.delayedExecutor with Mono.delay in onComplete

CompletableFuture.delayedExecutor(...) uses the common fork-join pool,
which can saturate under concurrent Vaadin push load and silently drop
runnables. Symptoms in production: spinner overlay stayed forever, no
toast fired, sidebar never closed.

Replace with Mono.delay(...), which uses Reactors boundedElastic
scheduler already configured and exercised by the rest of the
connectDatasets pipeline. This is the same mechanism the codebase
already uses in AsyncProjectServiceImpl.

* fix(FEAT-DATSET-01): stop relying on Flux.onComplete for post-connection UI work

The deprecated 3-arg subscribe(onNext, onError, onComplete) has a
known reactor-core 3.x defect: onComplete is silently skipped when
subscribeOn(boundedElastic) is combined with onErrorResume, which is
exactly our pipeline. Symptoms: spinner overlay "Connecting datasets..."
stays forever, no toast, sidebar never closes.

Also stop relying on Mono.delay-scheduler close. Under concurrent
push load that scheduled task was dropped silently too.

New approach: count completed responses inside onNext via a shared
AtomicInteger; when the counter reaches total, trigger onBatchFinished
inline on the same worker thread. onBatchFinished posts UI reset + toast
via uiHandle.onUiAndPush and schedules close via Mono.delay with explicit
error handler. If any individual onNext throws, the catch increments the
counter too (so a partial pipeline stall still reaches the finish
line) and the last one calls onBatchFinished with all-failure
semantics rather than leaving the UI permanently frozen.

* fix(FEAT-DATSET-01): simplify connect flow - remove per-row tracking and timer-based close

User feedback: "sidebar remains open and then suddenly closes magically;
reports failures although the record is connected successfully".

Root cause analysis:
- Per-row SUCCESS badges and status map added complexity without value.
- Mono.delay(5s) for auto-close felt unpredictable and could silently
  drop under push load.
- The "reports failures" issue may be a side-effect of the previous
  buggy onComplete-path where the timer and completion callback could
  fire out-of-order.

Changes:
- Removed `rowConnectionStatuses` map and `ConnectionStatus` enum.
- Removed per-row SUCCESS badge and tinting from `buildSearchResultCard`.
- Removed automatic selection change per response.
- Replaced timer-based close with immediate `close()` call in
  `onBatchFinished`.
- Simplified counter logic: track successes and failures inline in
  `subscribe onNext`; when `completedCount == total`, close and toast.
- Kept the spinner overlay while connecting (grid hidden), so user
  sees continuous "Connecting datasets..." feedback.
- Added explicit try/catch around individual response handlers so the
  batch always terminates even if a handler throws.

* fix(FEAT-DATSET-01): fix false-failure toast and simplify connect UX

Two bugs fixed:

1. Wrong failure count in toast: `connectDataset` step 5 (forward
   cached domain events to the global dispatcher) was not protected by
   a try/ catch. If event dispatch threw (e.g., subscriber issue), the
   exception propagated up through Mono.fromCallable, was caught by the
   onErrorResume, and mapped to CONNECT_FAILED — even though the dataset
   had already been persisted in step 4. The user saw their record in
   the Connected Resources list but also got a "could not be connected"
   toast. Fix: wrap the event dispatch in try/catch so a dispatch
   failure is logged but does not fail the connect itself.

2. Sidebar closed unpredictably: previous code used
   `CompletableFuture.delayedExecutor` / `Mono.delay` to schedule the
   close. Under concurrent Vaadin pushes these tasks could be silently
   dropped, leaving the sidebar permanently open, OR fire immediately
   after a busy push burst, closing the sidebar in a way that felt
   "magical" to the user. Fix: remove the timer; close the sidebar
   synchronously inside onBatchFinished immediately after the batch
   finishes. User always has the explicit × close affordance as
   fallback.

Along the way removed the per-row SUCCESS badge logic and the
rowConnectionStatuses tracking map — these added complexity without
observable UX value (the sidebar now closes immediately, so per-row
state never gets visible for more than a frame).

* feat(FEAT-DATSET-01): improve connected resource card scannability

- Promote resource type from gray detail row to a CONTRAST badge in the
  card header row so users can scan resource types at a glance.
- Remove meaningless dash placeholders for missing remote properties
  (version, creator, community, linked experiment, connected by/on);
  fields are now rendered only when the provider supplies data.
- Fuse 'Connected by' and 'Connected on' into a single italic
  attribution line ('connected on <date> by <name>') to reflect that
  they are semantically the same provenance event.
- Refactor addDetailCell() to be a no-op on null values so future
  callers cannot accidentally reintroduce dash placeholders.

* feat(FEAT-DATSET-01): order connected datasets by connected-on descending

Connected resources were previously returned in undefined database
order (no ORDER BY in the JPQL query). This was non-deterministic and
could shift after index rebuilds or vacuuming.

Default sort is now connected_on DESC — newest connections first —
which matches the mental model of 'what was added to my project most
recently'. The Sort is applied at the JPA infrastructure layer so the
domain repository interface stays Spring-independent; switching the
sort key later is a one-line change in the impl.

* feat(FEAT-DATSET-01): improve search result card interaction

- Make row-click toggle selection: clicking anywhere on a card row
  (not just the checkbox) now selects/deselects the hit. The checkbox
  continues to work independently. Cursor pointer on cards gives
  explicit affordance that the row is interactive.
- Replace plain-text "PID: x.yz/..." with a real clickable
  Anchor pointing at the DOI resolver URL. The record opens in a new
  tab (target=_blank) so users can verify the dataset before connecting
  without losing their current search context.
- Drop the redundant "PID:" label since the URL itself is unambiguous.

* fix(FEAT-DATSET-01): align provider-tag colors between sidebar and connected list

Search result cards in the sidebar rendered every provider with
PRIMARY (blue), but the connected-resources list differentiates
Zenodo (PRIMARY) from other providers (TEAL) so the two views now
share the same visual language. Switching a card from search to
connected-list no longer changes its perceived visual weight.

* refactor: extract ResourceProviderTag factory for centralized provider styling

- Replace inline provider tag construction (with Zenodo=PRIMARY /
  other=TEAL logic) in ConnectedResourcesComponent and
  ConnectDatasetSidebar with a single ResourceProviderTag factory.
- Provider tags now use TagColor.NEUTRAL instead of color-coding,
  reducing visual noise and eliminating the implicit hierarchy
  between providers.
- Add Javadoc explaining the design rationale.

* fix(FEAT-DATSET-01): align badge order between search results and connected list

Both views now render badges in the same order: provider first, then
access status. Previously the sidebar showed access then provider,
while the connected-resources list showed provider then access — a
minor but unnecessary inconsistency when moving between the two.

* Finalise user story

* refactor(datasets): harden AssociatedDatasetService — typed exceptions, YAGNI cleanup, Instant timestamps, pure-CSS four-dots spinner

The service layer now throws typed, user-friendly exceptions instead of
re-exposing infrastructure-level ApplicationExceptions with URLs and
status codes leaking to the UI. The reactive pipeline catches and logs
every escaped error, so connect failures are always traceable.

## Exception contract (new)
* AssociatedDatasetServiceException (sealed base)
  - DatasetSourceNotFoundException        – unknown repository ID
  - DatasetSourceUnavailableException     – network / HTTP / parse failures
* Each carries a user-friendly message; no infrastructure details leak.
* connectDatasetAsync: onErrorResume now logs the cause instead of
  silently swallowing — fixes the 'toast shown, nothing in the log' bug.
* Duplicate-check (isActiveConnectionPresent) wrapped in try/catch so
  persistence errors surface through the typed error, not through the
  reactive catch-all.
* Stale 'experimentId must not be null (use Optional.empty())'
  requireNonNull from the Optional→@nullable refactor removed.
* UiExceptionHandler surfaces AssociatedDatasetServiceException
  messages directly as the dialog body (title + translated content).
* toast-notifications.properties: dataset.search.failed.message.text
  now uses {0} so the user sees the service-specific message.

## YAGNI: embargoUntil removed from InvenioRdmResourceMetadata
FEAT-DATSET-01 covers public datasets only; embargo tracking is scoped
to FEAT-DATSET-14. The record field + the matching
'embargoUntil == null' check in deriveAccessLevel() are redundant —
EMBARGOED status already fails the PUBLIC-only check.
* Record field removed; deriveAccessLevel() simplified + documented.
* InvenioRdmDatasetSource.embargoUntil() helper removed.
* InvenioRdmClient.Embargo + its JSON binding removed.
* @JsonIgnoreProperties(ignoreUnknown = true) on the record so that
  rows persisted by older app versions (with 'embargoUntil' in the
  resource_metadata JSON) still deserialize correctly — fixes the
  'Error applying AttributeConverter' bug on reopening the page.
* ResourceMetadataConverter now prints a payload preview on failure.
* Regression spec InvenioRdmResourceMetadataJsonSpec covers the
  legacy-payload deserialization path + unknown future fields.

## Instant instead of LocalDateTime for connection timestamps
connectedOn and lastSyncedAt were LocalDateTime — a timezone-naive type
that produces incomparable records in multi-JVM deployments (Berlin JVM
vs Kolkata JVM write different wall-clock values for the same instant),
drifts silently on redeploy to a different host, and is ambiguous at
DST fall-back. Replaced with Instant; DB columns changed from
DATETIME(3) → TIMESTAMP(3); hibernate.jdbc.time_zone=UTC added to the
data-management EM config so the full stack is UTC (Java → JDBC →
MariaDB, all consistent). The ConnectedResourcesComponent view
extracts LocalDate via LocalDate.ofInstant(offset=UTC) for display.

## Application DTOs no longer leak the domain AccessLevel enum
ConnectedDatasetView and SearchHit exposed AccessLevel to the UI layer,
forcing the view to import a domain type to read one field. Changed to
'boolean isPublic'; translation from AccessLevel happens at the service
boundary (AssociatedDatasetService + InvenioRdmDatasetSource).
DataSetTagFactory now accepts boolean and has zero domain imports, so
it is a pure UI-layer utility.

## DataSetTagFactory — centralize all dataset tag styling
Inline tag creation for provider / dataset type / access badges in
ConnectedResourcesComponent and ConnectDatasetSidebar now routes
through DataSetTagFactory. Access type badge (Public/Restricted) is a
new TagType with semantic SUCCESS/WARNING color.

## CSS: inline styles → reusable classes in ConnectDatasetSidebar
~60 inline getStyle().set(...) calls extracted to:
* New connect-dataset-sidebar.css with scoped .cds-* classes
* Reusable utilities added to all.css: .clamp-1/2/3-line (multi-line
  -webkit-line-clamp), .overlay-center-fill (position:absolute + inset:0
  + centered flex layout for loading/welcome overlays).
* Only runtime display:none/flex/block toggles stay inline.

## CSS spinner — Temani Afif Spinner II (t_afif/pen/yLMXBRL)
Replaces the clock-emoji innerHTML spinner with a pure-CSS four-dots
orbit spinner. Both rings have explicit, named color tokens
(--cds-spinner-outer, --cds-spinner-inner defaulting to
--lumo-primary-color) so the two visible dot colors are discoverable
in the source; inner dots derive an accent via hue-rotate(45deg) to
stay harmonious with any Lumo primary choice. The outer/inner rings
rotate at different rates for the characteristic orbit effect.

## Test updates
* InvenioRdmClientParsingSpec: removed embargo-parsing tests (embargo
  infrastructure is gone) and the rec.access.embargo.active assertion
  (Embargo class removed from the client). JSON fixtures kept as-is —
  Jackson's @JsonIgnoreProperties ignores the key.
* New InvenioRdmResourceMetadataJsonSpec — covers legacy-payload
  deserialization (embargoUntil still in JSON) and unknown future fields
  are silently ignored.

* feat(invenio): replace ApplicationException with typed InvenioRdmException hierarchy

Add a structured exception hierarchy to the InvenioRdmClient API:

- InvenioRdmException (abstract base) — shared URL accessor for all
  client failures
- InvenioRdmPermanentException — 4xx errors not retried per ADR-0002 §9
  (401, 403, 404, etc.), carries statusCode
- InvenioRdmTransientException — retry exhaustion for 5xx/429/network,
  carries statusCode, attempts, and lastError
- InvenioRdmResponseParsingException — JSON deserialization failure,
  carries targetType
- InvenioRdmInterruptedException — thread interrupted during retry sleep

Update method signatures with explicit @throws declarations and
Javadoc. Update InvenioRdmDatasetSource callers to catch base exception
type and check for 404 via instanceof pattern matching instead of
string-based status code detection.

* Latest refactor changes from review

* Fix OIDC issue

* Address change requests

* Add change requests

* Remove unused imports

* Remove unused imports

---------

Co-authored-by: Sven Fillinger <sven.fillinger@qbic.uni-tuebingen.de>
Co-authored-by: sven1103-agent <sven.broja@qbic.uni-tuebingen.de>
…1486)

* feat: FEAT-DATSET-03 — remove connected dataset

Implement the 'Remove a connected open, published dataset' story
(#1469) of the FEAT-DATASET-CONNECTION feature (#1466).

Changes by task:

Task 0 — UI foundation: AlertDialog factory
  * New centralized, intent-driven dialog factory (DANGER/WARNING/ERROR/INFO)
    built on AppDialog.small(), IconFactory, and DialogFooter helpers
  * AlertDialog.danger() shortcut for destructive-action pattern
  * Fluent builder API for full control
  * Unit tests (12 cases)

Task 1 — Domain: emit AssociatedDatasetRemovedEvent on remove()
  * AssociatedDataset.remove(removedByUserId) transitions state to REMOVED
    and dispatches an AssociatedDatasetRemovedEvent
  * Actor is the user who performed the removal (may differ from
    original connector)
  * New domain-level spec: AssociatedDatasetRemoveSpec (4 tests)

Task 2 — Application: AssociatedDatasetService.removeDataset()
  * Find → remove → save → forward-events pipeline
  * Returns Result<AssociatedDatasetId, RemoveDatasetError>
  * Pre-existing AssociatedDatasetServiceRemoveSpec (6 tests) now green

Task 3 — Policy & notification directive
  * AssociatedDatasetRemovedPolicy subscribes to removal events
  * InformProjectCollaboratorsAboutDatasetRemoval directive emails
    every project collaborator except the actor (JobRunr background job)

Task 4 — Email template
  * Messages.datasetRemovedFromProject() template added

Task 5 — Toast notifications
  * dataset.removed.success / .failure / .already / .notfound
  * dataset.removing.in-progress (pending-task toast)

Task 6 — UI: remove button + async flow
  * Per-card Remove button (VaadinIcon.TRASH, WRITE-only, right-aligned)
  * setWriteAllowed(boolean) propagates project ACL from the view
  * AlertDialog.danger() confirmation with plain-text message
  * Blocking removeDataset() wrapped in CompletableFuture on
    ForkJoinPool.commonPool; UI thread returns to idle immediately
  * Pending-task toast shown during removal; replaced by success/error
    toast on completion via UiHandle.onUiAndPush()
  * Non-critical errors (NOT_FOUND, ALREADY_REMOVED) refresh silently

Task 7 — Wiring
  * AppConfig registers the AssociatedDatasetRemovedPolicy bean

Total: 308+ tests pass (0 failures, 0 errors).

Closes #1469

* refactor: use reactive removeDatasetAsync instead of raw CompletableFuture

Mirror the connectDatasetAsync pattern for removal: the new
AssociatedDatasetService.removeDatasetAsync(String, String) returns
Mono<Result<...>> and wraps the blocking call with boundedElastic,
contextWrite, timeout, and onErrorResume — exactly like connectDatasetAsync.

ConnectedDatasetsMain no longer rolls its own CompletableFuture + ForkJoinPool.
It now subscribes to the Mono and reacts via onNext / onError callbacks,
hopping back to the UI thread through the existing UiHandle.

No functional change — only the concurrency mechanism changes:
  * service API: add removeDatasetAsync()
  * view: delete CompletableFuture/ForkJoinPool plumbing, use Mono pattern

* Address change requests

* Remove time-out param
- NGS tab now produces <date>_<project>_ngs_measurement_dataset_locations.txt
- PxP tab now produces <date>_<project>_proteomics_measurement_dataset_locations.txt

Fixes #1444

Co-authored-by: Tobias Koch <tobias.koch@uni-tuebingen.de>
…ctories (#1489)

* refactor: migrate all NotificationDialog to AlertDialog and remove factories

Replace all NotificationDialog subclasses with AlertDialog patterns:

- Deleted 10 NotificationDialog subclasses (dead-simple factories with zero reusable logic):
  AccessTokenDeletionConfirmationNotification, BatchDeletionConfirmationNotification,
  MeasurementDeletionConfirmationNotification, QCItemDeletionConfirmationNotification,
  PurchaseItemDeletionConfirmationNotification, ProjectUserRemovalConfirmationNotification,
  ExistingGroupsPreventVariableEdit, ExistingSamplesPreventVariableEdit,
  ExistingSamplesPreventSampleOriginEdit, ExistingSamplesPreventGroupEdit

- Deleted CancelConfirmationDialogFactory — callers now use AlertDialog.alert().warning() directly
- Deprecated MessageSourceNotificationFactory.dialog() — single call-site inlined as AlertDialog
- Converted NotificationDialog.java to deprecated stub throwing UnsupportedOperationException
- Migrated UiExceptionHandler to AlertDialog
- Updated front-end-components.md with AlertDialog pattern documentation

GitHub Issue: #1487

* chore: completely remove NotificationDialog class and deprecated dialog() method

- Delete NotificationDialog.java stub entirely
- Delete deprecated MessageSourceNotificationFactory.dialog() method and its
  helper methods (parseTitle, parseConfirmText, DEFAULT_CONFIRM_TEXT)
- Remove unused AlertDialog import from MessageSourceNotificationFactory

* style: make AlertDialog warning buttons red and shorten button labels

- WARNING intent now gets red danger button (same as DANGER)
- Shortened button labels: 'Discard' instead of 'Discard changes',
  'Cancel' instead of 'Continue editing'
- Updated AppDialog.createConfirmDialog() for consistency

* Apply changes to AppDialog confirmation

* feat(alert-dialog): replace single-word button labels with action-oriented descriptions

UX research indicates that AlertDialog confirm/cancel buttons with single-word
labels (Confirm, Discard, Cancel, Okay) create unnecessary cognitive load because
users must infer the actual outcome from context.

This commit improves transparency by making buttons explicitly state their effect:

Destructive actions (was: 'Confirm' / 'Cancel'):
- 'Remove' / 'Remove user'  →  'Keep user'
- 'Disconnect' / 'Disconnect dataset'  →  'Keep connection'
- 'Delete' / 'Delete offer'  →  'Keep offer'
- 'Delete file'  →  'Keep file'
- 'Delete batch'  →  'Keep batch'
- 'Delete measurements'  →  'Keep measurements'
- 'Delete token'  →  'Keep token'

Discard-changes confirmation (was: 'Discard' / 'Cancel'):
- 'Discard changes'  →  'Keep editing'
  → applied to 6 call sites including internal AppDialog unsaved-changes alert

Error acknowledgment (was: 'Okay'):
- 'Got it'  →  applied to 4 error/informational dialogs

Every button pair now follows the pattern: both buttons state their outcome.

Files changed:
- AlertDialog: danger() factory gains cancelLabel parameter
- AppDialog: internal createConfirmDialog() uses new labels
- 11 call sites updated to use new API
- AlertDialogTest: updated for new signature (14 tests, all passing)

* Address change requests
…ported and shown entities based on their persistent identifier (#1482)

* Introduce Sorting of Samples, Measurements and Raw Data via an Ascending sort of the SampleId and MeasurementIds within the UI grids and the downloaded files (Excel Templates and URL Link List)

* Fix URL Filtering not using the sorted list

* Outsource Naturalorder comparator into seperate class

* Move NaturalOrder Comparison in Helper class and add tests

* Make sorting specific for measurement and samples to avoid unnecessary complicated general method. Adjust testing accordingly

* Remove unused imports and set static constant as suggested by sonarcode

* Address Sonarcloud, Adjust naming schema and make constant static as well

* Remove unnecessary code comparator method

* Add documentation for the worksheet entity ordering by id
…ng (#1490)

* feat(haproxy): add custom 503 maintenance landing page (#1464)

* feat(haproxy): add custom 503 maintenance landing page

Add a branded HAProxy error page for 503 responses with:
- Inline SVG illustration (server rack scene with floating papers,
  smoke, and animated sparks/LEDs)
- Contact information and provider details
- Self-contained HTML+CSS (no external dependencies)

Fix HAProxy errorfile format requirements:
- Use HTTP/1.1 status line (required by HAProxy 2.7+)
- Include Content-Length header matching exact body size
- Use CRLF line endings throughout (RFC 7230)
- Keep body under HAProxy's 16KB error file limit

* Add file size constraint to docs

---------

Co-authored-by: Sven Fillinger <sven.fillinger@qbic.uni-tuebingen.de>

* feat(FEAT-DATSET-09): show connected-dataset summary in project listing

Story #1475 (FEAT-DATSET-09) — researchers can now see from their
project collection view whether each project has connected datasets,
how many, their access status (Open / Restricted), and when the most
recent dataset was connected. Clicking the footer navigates directly
to the project's ConnectedDatasetsMain view.

Changes:
- sql: extend project_overview view with 4 aggregate columns sourced
  from associated_dataset (COUNT, SUM PUBLIC, SUM RESTRICTED, MAX
  connected_on). Migration script is idempotent and self-contained
  — no cross-view dependency on project_measurements.
- project-management: +4 fields on ProjectOverview entity
- UI: card-body and footer rendered as sibling RouterLinks inside a
  project-card-wrapper Div so each click target drives exactly one
  navigation. Footer uses theme classes only (no inline styles).
- theme: .project-card-wrapper owns shadow/border-radius/margin;
  .project-overview-item provides card-body padding; .project-dataset-
  footer provides footer padding + hover affordance. Chevron turns
  primary-text-colour on hover.
- requirements: DATA-R-04/05/06 allocated (new IDs; existing 01-03
  for immunopeptidomics preserved).
- demo ProjectListingDatasetsDemo.java kept as development-profile
  prototype sandbox, javadoc marked superseded.

Refs: #1475, #1466 (FEAT-DATASET-CONNECTION)

* Address change requests

* Fix formatter usage

---------

Co-authored-by: sven1103-agent <261423644+sven1103-agent@users.noreply.github.com>
#1484)

* feat(toast): redesign toast notifications with dark mode design system

Redesign the toast notification UI to align with the data-manager style
guide and improve visual design, accessibility, and consistency.

Functional changes:
- Extract levelIcon as a separate field so subsequent withContent calls
  cannot accidentally destroy the status icon (fix for missing icons).
- Use official Vaadin icons (vaadin:check, vaadin:close, vaadin:info)
  rendered at 14×14px in white inside 28×28 colored circles, instead of
  the smaller Lumo iconset (no Lumo info icon exists).
- Rewrite toast CSS to use design tokens from the style guide instead of
  hardcoded hex colors; add 14 new CSS variables
  (--color-primary, --color-error, --color-success, --color-shade-*,
  --toast-background-color, --toast-text-color, etc.) in variables.css.
- Increase link color from #1676f3 to #66A8FF to meet WCAG AA contrast
  requirements against the dark navy background.
- Fix routing/info toast link visibility by adding CSS overrides for
  RouterLink/Button elements rendered inside the toast.
- Use flexbox-based routing container instead of CSS Grid, simplifying
  the DOM while preserving ellipsis truncation for long messages via
  child combinator selectors.
- Remove max-width for the toast overlay to let content determine width.
- Fix pending task titles/subtexts rendering literal HTML markup by using
  Html wrappers instead of Div for title/subtext in withProgressBar().
- Wrap HTML content in <span> instead of <div style='display:contents'>
  to eliminate trailing block-level whitespace in toast messages.
- Reimplement pendingTaskToast to build progress layout directly instead
  of delegating to the text-toast path.
- Add actionToast factory method for error toasts with a 'Try Again'
  button.

* Address change requests

* Address change requests

* Exclude local .ssh agent folder from tracking

---------

Co-authored-by: sven1103-agent <261423644+sven1103-agent@users.noreply.github.com>
@github-actions github-actions Bot added the chore label Jul 30, 2026
@sven1103 sven1103 closed this Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants