Advent Calendar – 2025 – Detail Dialog – Part 1

Classification and objectives from a UI perspective

Today’s Advent Calendar Day focuses specifically on the interaction level prepared in the previous parts. While the basic structure of the user interface and the layout were defined at the beginning, and the interactive table view with sorting, filtering and dynamic actions was subsequently established, it is now a matter of making the transition from overview to detailed observation consistent. The user should no longer only see a tabular collection of data points, but should receive a view tailored to the respective object that enables contextual actions.

  • Classification and objectives from a UI perspective
  • OverviewView: Interactive enhancements
  • Detail view as a standalone UI component (DetailsDialog)
  • CreateView: Expiration date in the UI
  • Navigation and package structure
  • Interaction patterns and UX coherence
  • Uniform copying behaviour
  • Context menus and multiple interactions
  • Consistency of feedback
  • HTTPS Requirement for Clipboard
  • The source code for this version can be found on GitHub at https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-03

    Here’s the screenshot of the version we’re implementing now.

    The central goal of this chapter is to improve the user experience without sacrificing the simplicity of what has been done so far. The interface is extended with a detailed view implemented as a standalone dialogue, thereby deliberately creating a clear framework between the overall list and the individual object. This decision follows the principle of the cognitive separation of information spaces: an overview serves to orient and find relevant entries. At the same time, the detailed view creates an isolated, focused working environment.

    From an architectural point of view, this extension is an essential step towards a component-oriented UI in which each functional unit – such as creating, displaying or deleting short links – is represented by its own, clearly defined components. The new dialogue includes all the necessary UI elements, validations, and event flows for displaying and interacting with a single ShortUrlMapping object. This decoupling not only enables better testability and reusability but also reduces the cognitive overhead in the primary grid, which previously had to record all interactions directly.

    The dialogue serves as an interactive interface between the visual representation and the underlying data model. It reflects the current object, provides copy and navigation actions, and provides visual feedback on the entry’s status – for example, via a colour-coded expiration date. This modular structure allows the user to check details, perform actions, or remove erroneous entries without losing the application’s context.

    OverviewView: Interactive enhancements

    With the third expansion stage of the user interface, the previous list view is not only expanded, but also functionally upgraded. The OverviewView forms the foundation of the daily work with the created short links and is supplemented in this step by several mechanisms that make the behaviour of the application more natural and efficient.

    A central aspect concerns the reactivity of the search fields. In previous versions, search queries were triggered immediately after each input, which was technically correct but inefficient in practice, using the ValueChangeMode.LAZY, in combination with a 400-millisecond delay, the system does not react until the user has completed their input. This delay serves as a natural “pause for thought” and prevents unnecessary updates to the grid. In addition, the enter-key flow has been activated so that a targeted search confirmation can be performed via the keyboard – a typical usage pattern in the professional environment.

    codePart.setValueChangeMode(ValueChangeMode.LAZY);codePart.setValueChangeTimeout(400);codePart.addValueChangeListener(e -> refresh());urlPart.setValueChangeMode(ValueChangeMode.LAZY);urlPart.setValueChangeTimeout(400);urlPart.addValueChangeListener(e -> refresh());

    A second improvement concerns the variety of interactions in the grid. Instead of acting exclusively via buttons, the user can now open a data record by double-clicking or pressing Enter. This form of interaction is not only faster, but also meets the expectations you are used to from desktop applications. To further simplify operation, a context menu has been added that automatically offers the appropriate actions when right-clicking: View details, open target URL, copy shortcode, or delete the entry.

    grid.addItemDoubleClickListener(ev -> openDetailsDialog(ev.getItem()));grid.addItemClickListener(ev -> {  if (ev.getClickCount() == 2) openDetailsDialog(ev.getItem());});GridContextMenu<ShortUrlMapping> menu = new GridContextMenu<>(grid);menu.addItem("Show details", e -> e.getItem().ifPresent(this::openDetailsDialog));menu.addItem("Open URL", e -> e.getItem().ifPresent(m -> UI.getCurrent().getPage().open(m.originalUrl(), "_blank")));menu.addItem("Copy shortcode", e -> e.getItem().ifPresent(m -> UI.getCurrent().getPage().executeJs("navigator.clipboard.writeText($0)", m.shortCode())));menu.addItem("Delete...", e -> e.getItem().ifPresent(m -> confirmDelete(m.shortCode())));

    In addition to functionality, the grid column layout has been revised. The shortcode is now displayed in a monospace font, which makes it easier to quickly grasp and compare visually. In addition, a small copy symbol allows copying the entire short link to the clipboard with a mouse click. JavaScript is used to call up the browser’s native clipboard service.

    grid.addComponentColumn(m -> {  var code = new Span(m.shortCode());  code.getStyle().set("font-family", "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace");  var copy = new Button(new Icon(VaadinIcon.COPY));  copy.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE);  copy.getElement().setProperty("title", "Copy ShortUrl");  copy.addClickListener(_ -> {    UI.getCurrent().getPage().executeJs("navigator.clipboard.writeText($0)", SHORTCODE_BASE_URL + m.shortCode());    Notification.show("Shortcode copied");  });  var wrap = new HorizontalLayout(code, copy);  wrap.setSpacing(true);  wrap.setPadding(false);  return wrap;}).setHeader("Shortcode").setAutoWidth(true).setFrozen(true).setResizable(true).setFlexGrow(0);

    In the column with the original URL, the layout has been changed to an elliptical display : Long URLs are truncated, but remain fully visible via the tooltip. This detail improves readability without losing information.

    grid.addComponentColumn(m -> {  var a = new Anchor(m.originalUrl(), m.originalUrl());  a.setTarget("_blank");  a.getStyle()      .set("white-space", "nowrap")      .set("overflow", "hidden")      .set("text-overflow", "ellipsis")      .set("display", "inline-block")      .set("max-width", "100%");  a.getElement().setProperty("title", m.originalUrl());  return a;}).setHeader("URL").setFlexGrow(1).setResizable(true);

    Particularly noteworthy is the new Expires column, which colour-coded status indicators have visually supplemented. These are based on Lumo badges and show the remaining time until a link expires: green for active, yellow for expiring soon, and red for expired.

    grid.addComponentColumn(m -> {  var pill = new Span(m.expiresAt()      .map(ts -> {        var days = Duration.between(Instant.now(), ts).toDays();        if (days < 0) return "Expired";        if (days == 0) return "Today";        return "in " + days + " days";      }).orElse("No expiry"));  pill.getElement().getThemeList().add("badge pill small");  m.expiresAt().ifPresent(ts -> {    long d = Duration.between(Instant.now(), ts).toDays();    if (d < 0) pill.getElement().getThemeList().add("error");    else if (d <= 3) pill.getElement().getThemeList().add("warning");    else pill.getElement().getThemeList().add("success");  });  return pill;}).setHeader("Expires").setAutoWidth(true).setResizable(true).setFlexGrow(0);

    Finally, the display’s ergonomics have been improved. The grid now works with reduced vertical spacing (compact mode) while retaining its readability thanks to alternating line colours. The line height has been adjusted so that as many entries as possible remain visible even on smaller monitors without making the UI look overloaded.

    With these changes, the OverviewView is transformed from a purely management view into a central control tool that provides both a quick overview and deep interaction. This lays the foundation for integrating the detail dialogue – it complements this view with an object-related perspective, while the OverviewView continues to serve as the starting point for navigation.

    Detail view as a standalone UI component (DetailsDialog)

    Now that the overview list has been expanded with interactive functions, the next logical step is to introduce a standalone UI component focused on the display and management of individual datasets. The goal is to create a modular and reusable view that operates independently of the main view but communicates with it via clearly defined events.

    The dialogue is based on Vaadin’s Dialogue class and opens for each selected ShortUrlMapping object. In doing so, it reads out all relevant properties of the passed object – shortcode, target URL, creation time and, optionally, the expiration date. These values are presented in a clearly structured format, supplemented by action buttons to open, copy and delete the entry.

    The following excerpt shows the basic structure of the dialogue:

    public class DetailsDialog extends Dialog implements HasLogger {  public static final ZoneId ZONE = ZoneId.systemDefault();  private static final DateTimeFormatter DATE_TIME_FMT =      DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm").withZone(ZONE);  private final String shortCode;  private final String originalUrl;  private final Instant createdAt;  private final Optional<Instant> expiresAt;  private final TextField tfShort = new TextField("Shortcode");  private final TextField tfUrl = new TextField("Original URL");  private final TextField tfCreated = new TextField("Created on");  private final TextField tfExpires = new TextField("Expires");  private final Span statusPill = new Span();  private final Button openBtn = new Button("Open", new Icon(VaadinIcon.EXTERNAL_LINK));  private final Button copyShortBtn = new Button("Copy ShortURL", new Icon(VaadinIcon.COPY));  private final Button copyUrlBtn = new Button("Copy URL", new Icon(VaadinIcon.COPY));  private final Button deleteBtn = new Button("Delete...", new Icon(VaadinIcon.TRASH));  private final Button closeBtn = new Button("Close");  public DetailsDialog(ShortUrlMapping mapping) {    Objects.requireNonNull(mapping, "mapping");    this.shortCode = mapping.shortCode();    this.originalUrl = mapping.originalUrl();    this.createdAt = mapping.createdAt();    this.expiresAt = mapping.expiresAt();    setHeaderTitle("Details: " + shortCode);    setModal(true);    setDraggable(true);    setResizable(true);    setWidth("720px");    openBtn.addThemeVariants(ButtonVariant.LUMO_PRIMARY);    deleteBtn.addThemeVariants(ButtonVariant.LUMO_ERROR, ButtonVariant.LUMO_TERTIARY);    var headerActions = new HorizontalLayout(openBtn, copyShortBtn, copyUrlBtn, deleteBtn);    getHeader().add(headerActions);    configureFields();    var form = new FormLayout();    form.add(tfShort, tfUrl, tfCreated, tfExpires, statusPill);    form.setColspan(tfUrl, 2);    add(form);    closeBtn.addClickListener(e -> close());    getFooter().add(closeBtn);    wireActions();  }

    It is already clear here that the dialogue has a high degree of independence. It initialises its contents directly from the passed data object and uses a FormLayout to structure the fields. The input fields are read-only by default because the dialogue is used primarily for display.

    The visual feedback on the expiration status is provided by a so-called status pill, which indicates in colour and text whether a short link is still active, about to expire or has already expired. This is done via a small helper method that provides a status description including a colour scheme:

    private status computeStatusText() {  return expiresAt.map(ts -> {    long d = Duration.between(Instant.now(), ts).toDays();    if (d < 0) return new Status("Expired", "error");    if (d == 0) return new Status("Expires today", "warning");    if (d <= 3) return new Status("Expires in " + d + " days", "warning");    return new Status("Valid(" + d + " days left)", "success");  }).orElse(new Status("No expiry", "contrast"));}

    This status calculation complements the visual feedback from the summary table and provides a consistent representation across the entire application.

    The real added value of the DetailsDialog lies in its event-orientation. Instead of the calling view (OverviewView) controlling all actions itself, the actions are defined as Vaadin events. This allows the dialogue to send signals such as OpenEvent, CopyShortcodeEvent, CopyUrlEvent or DeleteEvent to its environment without knowing what they mean:

    public static class DeleteEvent extends ComponentEvent<DetailsDialog> {  public final String shortCode;  public DeleteEvent(DetailsDialog src, String sc) {    super(src);    this.shortCode = sc;  }}

    In the OverviewView , these events are received and processed:

    var dlg = new DetailsDialog(item);dlg.addDeleteListener(ev -> confirmDelete(ev.shortCode));dlg.addOpenListener(ev -> logger().info("Open URL {}", ev.originalUrl));dlg.addCopyShortListener(ev -> logger().info("Copied shortcode {}", ev.shortCode));dlg.addCopyUrlListener(ev -> logger().info("Copied URL {}", ev.url));dlg.open();

    This creates an apparent decoupling between the presentation and application logic. Dialogue handles representation and interaction; the surrounding view determines how to respond to events.

    In summary, the DetailsDialog establishes an architectural principle that goes beyond the specific use case. The combination of a modular UI component, declarative events and a clearly defined data model not only makes the application more flexible, but also more maintainable in the long term. The user benefits from a coherent, clearly structured, detailed view, which makes working with individual entries much more convenient and transparent.

    CreateView: Expiration date in the UI

    With this step, the creation of new short links receives an important semantic addition: the optional expiration date. The aim is to precisely define the intended service life at the time of creation and to transport this information end-to-end – from the UI to the client to the server and into the persistence.

    From a UI point of view, the existing CreateView is extended with DatePicker and TimePicker, flanked by a checkbox labelled “No expiry”. This combination allows both the explicit determination of an end date and the conscious decision to set an unlimited validity. A small but crucial UX rule: the time will remain disabled until a date is selected, and both fields will be disabled if “No expiry” is enabled.

    Fields and Basic Configuration

    private final TextField urlField = new TextField("Target URL");private final TextField aliasField = new TextField("Alias (optional)");private final Button shortenButton = new Button("Shorten");private final DatePicker expiresDate = new DatePicker("Expires (date)");private final TimePicker expiresTime = new TimePicker("Expires (time)");private final Checkbox noExpiry = new Checkbox("No expiry");private final FormLayout form = new FormLayout();public CreateView() { setSpacing(true); setPadding(true); urlField.setWidthFull(); aliasField.setWidth("300px"); shortenButton.addThemeVariants(ButtonVariant.LUMO_PRIMARY); form.add(urlField, aliasField); configureExpiryFields(); form.setResponsiveSteps( new FormLayout.ResponsiveStep("0", 1), new FormLayout.ResponsiveStep("600px", 2) ); form.setColspan(urlField, 2); var actions = new HorizontalLayout(shortenButton); actions.setAlignItems(Alignment.END); Binder for validations Binder<ShortenRequest> binder = new Binder<>(ShortenRequest.class); ShortenRequest request = new ShortenRequest(); binder.forField(urlField) .asRequired("URL must not be empty") .withValidator(url -> url.startsWith("http://") || url.startsWith("https://"), "Only HTTP(S) URLs allowed") .bind(ShortenRequest::getUrl, ShortenRequest::setUrl); binder.forField(aliasField) .withValidator(a -> a == null || a.isBlank() || a.length() <= AliasPolicy.MAX, "Alias is too long (max " + AliasPolicy.MAX + ")") .withValidator(a -> a == null || a.isBlank() || a.matches(REGEX_ALLOWED), "Only [A-Za-z0-9_-] allowed") .bind(ShortenRequest::getShortURL, ShortenRequest::setShortURL); shortenButton.addClickListener(_ -> { var validated = binder.validate(); if (validated.hasErrors()) return; if (!validateExpiryInFuture()) return; if (binder.writeBeanIfValid(request)) { computeExpiresAt().ifPresent(request::setExpiresAt); var code = createShortCode(request, computeExpiresAt()); code.ifPresentOrElse(c -> { Notification.show("Short link created: " + c); clearForm(binder); }, () -> Notification.show("Alias already assigned or error saving", 3000, Notification.Position.MIDDLE)); } }); add(new H2("Create new short link"), form, actions);}

    Encapsulating process logic in small auxiliary methods improves readability and testability. The calculation uses the local time zone and provides an instant that can be processed unchanged on the server side.

    private static final ZoneId ZONE = ZoneId.systemDefault();private void configureExpiryFields() { expiresDate.setClearButtonVisible(true); expiresDate.setPlaceholder("dd.MM.yyyy"); expiresTime.setStep(Duration.ofMinutes(1)); expiresTime.setPlaceholder("HH:mm"); Activate time only when a date is set expiresTime.setEnabled(false); expiresDate.addValueChangeListener(ev -> { boolean hasDate = ev.getValue() != null; expiresTime.setEnabled(hasDate && !noExpiry.getValue()); }); noExpiry.addValueChangeListener(ev -> { boolean disabled = ev.getValue(); expiresDate.setEnabled(!disabled); expiresTime.setEnabled(!disabled && expiresDate.getValue() != null); }); form.add(noExpiry, expiresDate, expiresTime);}private Optional<Instant> computeExpiresAt() { if (Boolean.TRUE.equals(noExpiry.getValue())) return Optional.empty(); LocalDate d = expiresDate.getValue(); LocalTime t = expiresTime.getValue(); if (d == null || t == null) return Optional.empty(); return Optional.of(ZonedDateTime.of(d, t, ZONE).toInstant());}private boolean validateExpiryInFuture() { var exp = computeExpiresAt(); if (exp.isPresent() && exp.get().isBefore(Instant.now())) { Notification.show("Expiry must be in the future"); return false; } return true;}private void clearForm(Binder<ShortenRequest> binder) { urlField.clear(); aliasField.clear(); noExpiry.clear(); expiresDate.clear(); expiresTime.clear(); binder.setBean(new ShortenRequest()); urlField.setInvalid(false); aliasField.setInvalid(false);}

    It is important to pass it on transparently to the client. Instead of storing the time in the UI, it is transmitted to the server in the request. To do this, the CreateView calls the client with the extended signature. The client validates only if an alias is set and passes the data to the server unchanged.

    private Optional<String> createShortCode(ShortenRequest req, Optional<Instant> expiresAt) {  logger().info("createShortCode with ShortenRequest '{}'", req);  try {    var customMapping = urlShortenerClient.createCustomMapping(req.getShortURL(), req.getUrl(), expiresAt.orElse(null));    return Optional.ofNullable(customMapping.shortCode());  } catch (IllegalArgumentException | IOException e) {    logger().error("Error saving", e);    return Optional.empty();  }}

    On the client side, the payload is serialised as a ShortenRequest and sent to the /shorten endpoint. The response is not limited to the shortcode; it is also parsed as a full ShortUrlMapping. As a result, the UI immediately knows the server-side confirmed state – including the expiresAt.

    public ShortUrlMapping createCustomMapping(String alias, String url, Instant expiredAt) throws IOException { logger().info("Create custom mapping alias='{}' url='{}' expiredAt='{}'", alias, url, expiredAt); if (alias != null && !alias.isBlank()) { var validate = AliasPolicy.validate(alias); if (!validate.valid()) { var reason = validate.reason(); throw new IllegalArgumentException(reason.defaultMessage); } } URL shortenUrl = serverBaseAdmin.resolve(PATH_ADMIN_SHORTEN).toURL(); HttpURLConnection connection = (HttpURLConnection) shortenUrl.openConnection(); connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setRequestProperty(CONTENT_TYPE, JSON_CONTENT_TYPE); var shortenRequest = new ShortenRequest(url, alias, expiredAt); String body = shortenRequest.toJson(); try (OutputStream os = connection.getOutputStream()) { os.write(body.getBytes(UTF_8)); } int status = connection.getResponseCode(); if (status == 200 || status == 201) { try (InputStream is = connection.getInputStream()) { String jsonResponse = new String(is.readAllBytes(), UTF_8); ShortUrlMapping shortUrlMapping = fromJson(jsonResponse, ShortUrlMapping.class); return shortUrlMapping; } } if (status == 409) { throw new IllegalArgumentException("Alias already in use"); } throw new IOException("Unexpected status: " + status);}

    On the server side,  the ShortenHandler receives the extended request, validates the required fields, and then assigns the store to create it. The response contains the complete mapping object that the client and UI can use immediately.

    final String body = readBody(ex.getRequestBody());ShortenRequest req = fromJson(body, ShortenRequest.class);if (isNullOrBlank(req.getUrl())) { writeJson(ex, BAD_REQUEST, "Missing 'url'"); return;}final Result<ShortUrlMapping> urlMappingResult = store.createMapping(req.getShortURL(), req.getUrl(), req.getExpiresAt());urlMappingResult .ifPresentOrElse(success -> logger().info("mapping created success {}", success.toString()), failed -> logger().info("mapping created failed - {}", failed));urlMappingResult .ifSuccess(mapping -> { final Headers h = ex.getResponseHeaders(); h.add("Location", "/r/" + mapping.shortCode()); writeJson(ex, fromCode(201), toJson(mapping)); }) .ifFailure(errorJson -> { try { var parsed = JsonUtils.parseJson(errorJson); var errorCode = Integer.parseInt(parsed.get("code")); var message = parsed.get("message"); writeJson(ex, fromCode(errorCode), message); } catch (Exception e) { writeJson(ex, CONFLICT, errorJson); } });

    In summary, a consistent, end-to-end process is created: The user optionally defines an expiration date when making them, the UI validates basic rules, the client transfers the semantics losslessly, and the server persists them reliably. The OverviewView and the DetailsDialog can then display and interpret this information immediately. This adds a central property to the domain without complicating the existing operating flow.

    Navigation and package structure

    The previous user interface has become much more complex in terms of content due to the detailed dialogue and the extended form functions. To reflect this development, the package structure in the UI module was clearly reorganised. The goal was not only a logical grouping according to responsibilities, but also a long-term basis for extended navigation concepts and modular extensions.

    Previously, the OverviewView was still in the general package com.svenruppert.urlshortener.ui.vaadin.views. With the introduction of detailed dialogue and the growing importance of this area in terms of content, it was included in a separate subpackage, “views.overview“, and moved there. This decision not only creates room for additional components (e.g. context menus, helper dialogues or filters), but also follows the principle of functional coherence: All classes that together form the overview function are now centrally bundled.

    In the code, this step is reflected in the customisation of the import within the MainLayout :

    old:

    import com.svenruppert.urlshortener.ui.vaadin.views.OverviewView;

    new:

    import com.svenruppert.urlshortener.ui.vaadin.views.overview.OverviewView;

    This small but significant step marks the transition from a purely page-based structure to a component-oriented structure. The MainLayout continues to serve as the central navigation element of the application; the individual views are now more decoupled and can be further developed independently. This creates a clear separation between layout logic (central navigation, menus, visual frameworks) and functional logic (display, interaction, data flow).

    The route relationships are deliberately kept simple. The OverviewView is still registered under the path /overview and uses the MainLayout as its parent layout element:

    @PageTitle Overview@Route(value = OverviewView.PATH, layout = MainLayout.class)public class OverviewView extends VerticalLayout implements HasLogger {    public static final String PATH = "overview";    // ...}

    This configuration keeps navigation consistent with the other parts of the application, such as CreateView, AboutView,  or YoutubeView. New views can be easily added without affecting the central navigation mechanism. This ensures maintainability and scalability – two central requirements for an application that grows gradually within the Advent calendar framework.

    The MainLayout itself is largely unchanged, but has been updated during the package migration to reflect new imports and menu entries. The referencing of the OverviewView is particularly important, as it represents the entry point of user interaction:

    SideNavItem overview = new SideNavItem("Overview", OverviewView.class, VaadinIcon.LIST.create());SideNavItem create = new SideNavItem("Create", CreateView.class, VaadinIcon.PLUS.create());SideNavItem about = new SideNavItem("About", AboutView.class, VaadinIcon.INFO_CIRCLE.create());SideNavItem youtube = new SideNavItem("Youtube", YoutubeView.class, VaadinIcon.YOUTUBE.create());SideNav nav = new SideNav(overview, create, about, youtube);addToDrawer(nav);

    This clean separation between routing, structure and presentation lays the foundation for the further development of the user interface. Future features – such as detailed filters, user preferences or administrative functions – can be easily integrated into their own subpackages and namespaces without affecting core navigation. This is the step towards a sustainable UI architecture that specifically supports both growing complexity and extensibility.

    Interaction patterns and UX coherence

    As the application’s feature density increases, the importance of a consistent user experience grows. While the first days of the Advent calendar laid the technical foundation, so far, the focus has been on functionality. In this chapter, the focus shifts to the coherence of interaction patterns, i.e. how users interact with the application in a consistent, predictable rhythm.

    Central to this is the goal of establishing uniform behaviour for recurring actions. Whether in the overview, in the detail dialogue or in forms – copying, opening or deleting should always feel the same. The application thus conveys reliability, which is particularly crucial for technical users of web-based tools.

    Uniform copying behaviour

    A good example is copying URLs and short links. The exact mechanisms are used in both the grid and the detail dialogue: a button with the VaadinIcon.COPY symbol: an asynchronous clipboard action via JavaScript and a discreet confirmation message. This avoids users having to learn different forms of interaction in various views.

    copy.addClickListener(_ -> {  UI.getCurrent().getPage().executeJs("navigator.clipboard.writeText($0)", SHORTCODE_BASE_URL + m.shortCode());  Notification.show("Shortcode copied");});

    Such a detail may seem inconspicuous, but it has a significant impact on the perceived professionalism of the application. The feedback system via notifications plays a central role here: The user receives an immediate, unobtrusive signal about the success of his action – a kind of visual receipt that creates trust.

    Context menus and multiple interactions

    Another element of UX coherence is the context menu in the summary table. It allows access to the same actions, which can also be accessed via buttons or double-clicks. This redundant but deliberate multiple interaction follows the principle of user freedom: Users can choose whether to act via direct icons, the keyboard or the context menu.

    GridContextMenu<ShortUrlMapping> menu = new GridContextMenu<>(grid);menu.addItem("Show details", e -> e.getItem().ifPresent(this::openDetailsDialog));menu.addItem("Open URL", e -> e.getItem().ifPresent(m -> UI.getCurrent().getPage().open(m.originalUrl(), "_blank")));menu.addItem("Copy shortcode", e -> e.getItem().ifPresent(m -> UI.getCurrent().getPage().executeJs("navigator.clipboard.writeText($0)", m.shortCode())));menu.addItem("Delete...", e -> e.getItem().ifPresent(m -> confirmDelete(m.shortCode())));

    The decision to use context menus is not only aesthetic but also ergonomic: it reduces the grid’s optical density without sacrificing functionality. Actions only appear when they are needed – an approach that is essential in complex management interfaces.

    Consistency of feedback

    A uniform pattern is also followed for error messages and validations. The system does not abruptly abort user actions; instead, it clearly communicates why an input was not accepted. Example: The expiration date cannot be in the past. This rule is conveyed both visually and by message.

    if (exp.isPresent() && exp.get().isBefore(Instant.now())) {  Notification.show("Expiry must be in the future");  return false;}

    Accurate feedback prevents the user from perceiving the system as unpredictable. Every validation, every hint, and every success message follows the same communicative style—short, clear, and polite.

    HTTPS Requirement for Clipboard

    A technical but essential detail is that the Clipboard API only works in modern browsers within secure contexts (HTTPS or localhost). Therefore, the copy feature is deliberately designed to fail elegantly when clipboard access is unavailable. The application does not crash, but responds silently – an aspect that underlines the robustness and professionalism of the user experience.

    The mechanisms described in this chapter – consistent feedback, redundant operating options and secure fallbacks – together contribute to a coherent user experience. They form the basis of trust and predictability, two characteristics that are essential as software becomes more complex. Overall, the result is an interface that can be operated intuitively, even as its technical depth in the background continues to increase.

    Cheers Sven

    #Advent2025 #EclipseStore #Java #Vaadin

    La quarta giornata del DhammapaDOS vi porta un gioco che ha oltre 40 anni di vita, un arcade con i fiocchi in grafica rigorosamente CGA, dove impersoniamo uno chef che deve comporre dei panini lottando contro salsicce, uova e altri alimenti con le gambe a suon di lanci di pepe.

    Sto parlando di BurgerTime, firmato Data East, un classico videoludico che onestamente giocherei per ore senza stancarmi.

    Se non lo conoscete venite a guardarlo sul mio canale!

    https://odysee.com/@GiardinoVideoludico:f/DhammapaDOS2025_4:9

    #advent2025

    2/2

    [Il DhammapaDOS 2025] #4 - BurgerTime

    Odysee

    Illuminatə, namaskar!

    Ecco l'elenco della quarta giornata dell'avvento videoludico 2025:

    Kenobisboch: The Tribe
    Retromagazine World TV: Super Mario Bros (Intellivision)
    Luca Merler: Gran Turismo
    Ross Wildstar: Chuck Rock 2 - Son of Chuck
    Ilarik76: Video Hustler
    Il Paul di Gomma: Leggatana
    Austindorf: Golden Fantasia
    Vanack Sabbadium: BurgerTime
    Mr. Micro: Kaboom!
    Vintage People: Bloody Bastards
    RetroCoach: One Must Fall 2097

    Foglio di calcolo aggiornato a ieri!

    #advent2025

    1/2

    Advent Calendar – 2025 – Persistence – Part 02

    Today, we will finally integrate the StoreIndicator into the UI.

  • Vaadin integration: live status of the store
  • Implementation of the StoreIndicator
  • Refactoring inside – The MappingCreator as a central logic.
  • EclipseStore – The Persistent Foundation
  • Additional improvements in the core
  • Before & After – Impact on the developer experience
  • The source code for this version can be found on GitHub at https://github.com/svenruppert/url-shortener/tree/feature/advent-2025-day-02

    Here’s the screenshot of the version we’re implementing now.

    Vaadin integration: live status of the store

    Now that communication between the client and server has been established via the AdminClient, the focus is on integrating with the Vaadin interface. The goal is to make the current system state visible to the user without requiring manual updates. The mechanism is based on cyclic querying, event control, and a reactive UI concept, all fully implemented in Vaadin.

    A central component of this integration is the StoreIndicator component. It serves as a visual interface between the user and the system state. Its design combines UI representation, data retrieval, and state management. The structure is implemented within the Vaadin component model, with the indicator implemented as an independent class, “StoreIndicator”. This class extends HorizontalLayout and adds several sub-elements to the layout—an icon, a text label, and optional status information. Their behaviour is closely linked to Vaadin’s lifecycle control.

    The central entry point is the onAttach method, which is executed whenever the component is added to the UI tree. At this moment, the polling mechanism starts, calling the AdminClient every 10 seconds to get the current status from the server. Communication remains fully controlled on the server side, while the UI is automatically synchronised.

    @Overrideprotected void onAttach(AttachEvent attachEvent) {    refreshOnce();    UI ui = attachEvent.getUI();    ui.setPollInterval(10000);    ui.addPollListener(e -> refreshOnce());}

    Analogous to the onAttach method, onDetach is also an essential part of the StoreIndicator’s lifecycle. This method is executed when the component is removed from the UI tree, such as when switching between views or closing the session. Here, the polling mechanism is cleanly terminated, and resources such as event subscriptions or listeners are released. This prevents background processes from continuing to send data to a defunct UI component. The code illustrates this process:

    @Overrideprotected void onDetach(DetachEvent detachEvent) {    super.onDetach(detachEvent);    if (subscription != null) {        try {            subscription.close();        } catch (Exception ignored) {        }        subscription = null;    }}

    This implementation ensures that no polling tasks or event listeners remain active after the component is removed. This is especially crucial in server-side frameworks like Vaadin to avoid leaks and unnecessary thread activity. The StoreIndicator thus acts like a well-behaved UI citizen, completing its tasks properly when leaving the scene.

    Each polling cycle triggers the refreshOnce method, which uses the AdminClient to determine the current memory state. This checks whether the server returns the mode “EclipseStore” or “InMemory”. The colour scheme is adjusted accordingly, and the symbol is highlighted. The design is based on Vaadin’s Lumo theme system, which enables clean integration with the application’s corporate design via CSS variables.

    Another central element is the internal event system, implemented via the StoreEvents and StoreConnectionChanged classes. It allows for loose coupling between components, so that any change to the memory state can be published globally and consumed by other UI elements. The StoreIndicator uses this system to propagate state changes, while the OverviewView, for example,  acts as a subscriber and automatically updates itself when the memory state changes.

    StoreEvents.publish(new StoreConnectionChanged(newMode, info.mappings()));

    This architecture avoids direct dependencies between UI components. Instead of explicit references or state propagation via view constructors, an event model is used that reduces complexity while increasing the application’s responsiveness. The result is a highly modular, reactive UI structure that reflects data changes on the backend.

    The way in which Vaadin is used here as a reactive framework deserves special attention. While classic web frameworks propagate state changes via HTTP requests and complete page reloads, Vaadin uses server-side push to deliver targeted UI updates. In combination with the polling model of the StoreIndicator, a hybrid solution is created: On the one hand, the architecture remains comprehensible and straightforward, and on the other hand, it gives the impression of a live application that reacts to changes in real time.

    This integration of event model, UI lifecycle, and data transfer is an example of modern component-oriented design in the Java ecosystem. It shows that reactive behaviour patterns can also be implemented without external reactive frameworks such as Vert.x or Reactor.

    Implementation of the StoreIndicator

    The indicator’s graphical structure is already fully defined in the constructor. It extends HorizontalLayout and uses Vaadin components such as Icon and Span to create a compact, semantically transparent status display. The structure follows the typical Vaadin composition logic: The graphic elements are created, styled and then inserted into the layout via the add method. Particular attention is paid to legibility and visual consistency:

    public StoreIndicator(AdminClient adminClient) {  this.adminClient = adminClient;  setAlignItems(FlexComponent.Alignment.CENTER);  setSpacing(true);  setPadding(false);  dbIcon.setSize("16px");  dbIcon.getStyle().set("color", "var(--lumo-secondary-text-color)");  badge.getStyle()      .set("font-size", "12px")      .set("font-weight", "600")      .set("padding", "0.2rem 0.5rem")      .set("border-radius", "0.4rem")      .set("background-color", "var(--lumo-contrast-10pct)")      .set("color", "var(--lumo-body-text-color)");  details.getStyle()      .set("font-size", "12px")      .set("opacity", "0.8");  add(dbIcon, badge, details);}

    The actual logic of the status update is contained in the refreshOnce method. It calls the AdminClient to determine the current server state. This returns a StoreInfo object containing the mode and the number of saved mappings. The mode dynamically adjusts the indicator’s colour scheme– green for persistent, blue for volatile, and red for erroneous states. This colour coding is based on Vaadin’s Lumo theming and provides immediate visual feedback.

    public void refreshOnce() {  getUI().ifPresent(ui -> ui.access(() -> {    try {      StoreInfo info = adminClient.getStoreInfo();      boolean persistent = "EclipseStore".equalsIgnoreCase(info.mode());      badge.setText(persistent ? "EclipseStore": "InMemory");      if (persistent) {        badge.getStyle()            .set("background-color", "var(--lumo-success-color-10pct)")            .set("color", "var(--lumo-success-text-color)");        dbIcon.getStyle().set("color", "var(--lumo-success-color)");      } else {        badge.getStyle()            .set("background-color", "var(--lumo-primary-color-10pct)")            .set("color", "var(--lumo-primary-text-color)");        dbIcon.getStyle().set("color", "var(--lumo-primary-color)");      }      details.setText("· " + info.mappings() + " items");      getElement().setAttribute("title", persistent ? "Persistent via EclipseStore" : "Volatile (InMemory)");      var newMode = persistent ? StoreMode.ECLIPSE_STORE : StoreMode.IN_MEMORY;      if (newMode != lastMode) {        lastMode = newMode;        StoreEvents.publish(new StoreConnectionChanged(newMode, info.mappings()));      }    } catch (Exception e) {      badge.setText("Unavailable");      badge.getStyle()          .set("background-color", "var(--lumo-error-color-10pct)")          .set("color", "var(--lumo-error-text-color)");      dbIcon.getStyle().set("color", "var(--lumo-error-color)");      details.setText("");      getElement().setAttribute("title", "StoreInfo endpoint unavailable");      if (lastMode != StoreMode.UNAVAILABLE) {        lastMode = StoreMode.UNAVAILABLE;        StoreEvents.publish(new StoreConnectionChanged(StoreMode.UNAVAILABLE, 0));      }    }  }));}

    This method is an example of reactive UI design without external frameworks. The combination of ui.access() and PollListener allows periodic updates without blocking the main UI thread model. Error cases are elegantly intercepted via the catch block and propagated via the event bus (StoreEvents.publish), so that other components can also react to failures.

    Refactoring inside – The MappingCreator as a central logic.

    With the introduction of EclipseStore and the parallel continued existence of the InMemory store, the need arose to standardise the generation logic for short URLs. Earlier versions of the application included this logic multiple times – both in the InMemory store and in the later EclipseStore implementation. This redundancy not only led to maintenance costs but also made uniform error handling more difficult. The solution to this problem was to introduce a dedicated component, the MappingCreator, which acts as a central element between validation, alias policy, and persistence mediation.

    The MappingCreator covers the entire process chain from the alias check to the generation of a new short code to the transfer to persistent storage. This component follows the principle of functional composition and is designed to operate independently of the specific storage technology. This means that both the InMemory and EclipseStore approaches can use the exact generation mechanism without duplicating the logic.

    The class is fully implemented in Core Java and does not require any external libraries. It defines a functional interface, ExistsByCode, that checks whether a specific shortcode already exists, and another interface, PutMapping, that stores a newly created mapping. This makes the Creator a generic tool that can take advantage of any implementation of UrlMappingStore . The following code snippet illustrates the structure:

    public final class MappingCreator implements HasLogger {  private final ShortCodeGenerator generator;  private final ExistsByCode exists;  private final PutMapping store;  private final clock clock;  private final Function<ErrorInfo, String> errorMapper;  public MappingCreator(ShortCodeGenerator generator,                        ExistsByCode exists,                        PutMapping store,                        Clock clock,                        Function<ErrorInfo, String> errorMapper) {    this.generator = Objects.requireNonNull(generator);    this.exists = Objects.requireNonNull(exists);    this.store = Objects.requireNonNull(store);    this.clock = Objects.requireNonNullElse(clock, Clock.systemUTC());    this.errorMapper = Objects.requireNonNull(errorMapper);  }

    The complete creation process is then displayed in the create method. The process follows a precisely defined scheme: First, it is checked whether the user has entered an alias. If so, it is checked and normalised via the AliasPolicy’s validate method. If the alias is invalid, the creator creates an error object with HTTP and application code and returns it as a failure result. If no alias is available, the creator automatically generates a unique shortcode with the ShortCodeGenerator, checks for collisions, and repeats the process if necessary.

    public Result<ShortUrlMapping> create(String alias, String url) {  logger().info("createMapping - alias='{}' / url='{}'", alias, url);  final String shortCode;  if (!isNullOrBlank(alias)) {    var aliasCheck = AliasPolicy.validate(alias);    if (aliasCheck.failed()) {      var reason = aliasCheck.reason();      var reasonCode = switch (reason) {        case NULL_OR_BLANK -> "ALIAS_EMPTY";        case TOO_SHORT -> "ALIAS_TOO_SHORT";        case TOO_LONG -> "ALIAS_TOO_LONG";        case INVALID_CHARS -> "ALIAS_INVALID_CHARS";        case RESERVED -> "ALIAS_RESERVED";      };      var errorJson = errorMapper.apply(new ErrorInfo("400", reason.defaultMessage, reasonCode));      return Result.failure(errorJson);    }    var normalized = normalize(alias);    if (exists.test(normalized)) {      var errorJson = errorMapper.apply(new ErrorInfo("409", "normalizedAlias already in use", "ALIAS_CONFLICT"));      return Result.failure(errorJson);    }    shortCode = normalized;  } else {    String gen = normalize(generator.nextCode());    while (exists.test(gen)) {      gen = normalize(generator.nextCode());    }    shortCode = gen;  }  var mapping = new ShortUrlMapping(shortCode, url, Instant.now(clock), Optional.empty());  store.accept(mapping);  return Result.success(mapping);}

    This method shows the functional structure of the component as an example. The creator does not create side effects outside its intended interfaces. It communicates exclusively via the function objects exists and store. Error handling is fully integrated into the result tree, allowing a clean separation between success and failure paths. In this way, logic remains deterministic, testable, and reproducible.

    With the introduction of MappingCreator, the codebase has been significantly streamlined. Both the InMemoryUrlMappingStore and the EclipseStoreUrlMappingStore now use the same generation logic, which substantially improves consistency and extensibility. The Creator is thus not only a refactoring result, but also a strategic architectural element that combines functional abstraction, type safety and test-driven development.

    EclipseStore – The Persistent Foundation

    After the internal generation logic has been unified with the MappingCreator, the implementation of the EclipseStoreUrlMappingStore now serves as the basis for permanent storage of the generated short URLs. This class replaces volatile storage with a fully persistent object model that maintains the application’s state across reboots. While the InMemory store only kept all mappings in a ConcurrentHashMap, in the EclipseStore they are stored within a serializable object, the so-called DataRoot. This structure serves as an anchor for all stored data elements, ensuring the entire object graph remains consistent at all times.

    The central idea of the EclipseStore approach is to implement storage logic not via relational mappings but via object-oriented persistence. This preserves the application’s model in its entirety, without requiring object or database table conversions. The following example shows the definition of the DataRoot:

    public class DataRoot implements Serializable {  @Serial  private static final long serialVersionUID = 1L;  private final Map<String, ShortUrlMapping> mappings = new ConcurrentHashMap<>();  public Map<String, ShortUrlMapping> mappings() {    return mappings;  }}

    In the EclipseStoreUrlMappingStore, when the server starts, it checks whether a root structure already exists. If not, it is recreated and registered as the root node of the memory instance. The following snippet shows the relevant section from the constructor:

    var storagePath = Paths.get(storageDir);Files.createDirectories(storagePath);this.storage = EmbeddedStorage.start(storagePath);DataRoot r = (DataRoot) storage.root();if (r == null) {  storage.setRoot(new DataRoot());  storage.storeRoot();}

    This initialisation ensures that the application has the same persisted state both during a cold boot and after a shutdown. All access to the mappings then takes place directly via the root object. When a new mapping is created, the store calls the storeMappingAndPersist method  , which stores the object in the internal map and synchronises the memory.

    private void storeMappingAndPersist(ShortUrlMapping m) {  var dataRoot = (DataRoot) storage.root();  var mappings = dataRoot.mappings();  mappings.put(m.shortCode(), m);  storage.store(mappings);}

    This immediate write process saves changes immediately. The EclipseStore automatically takes over transaction management at the object level and ensures atomic storage without requiring additional commit logic. This reduces the risk of errors and increases the traceability of the system’s status.

    In addition, the EclipseStoreUrlMappingStore implements all the methods of the UrlMappingStore interface, including find, delete, count,  and existsByCode. These methods operate directly on the object graph and use helper classes, such as UrlMappingFilterHelper, to efficiently execute queries and perform sorting. The key difference to the InMemory store is that changes to the data structure take effect immediately in persistent memory and are therefore available again when the application restarts.

    The implementation of the EclipseStoreUrlMappingStore makes it clear that persistence was not designed as an afterthought, but as an integral part of the architecture. The entire data flow – from alias generation to MappingCreator to storage in DataRoot – follows a consistent design. This not only achieves persistence, but also anchors it conceptually: the memory is not an external component, but part of the living object model of the application.

    Additional improvements in the core

    Parallel to the introduction of the EclipseStore and the standardisation of the mapping logic, the central helper classes in the core module have also been further developed to ensure more consistent data processing. The focus was not on expanding the range of functions, but on making internal processes more precise. In particular, JsonUtils, UrlMappingFilterHelper, and the internal validation logic of the AliasPolicy have been explicitly revised to improve integration between the REST layer, the UI, and memory.

    The JsonUtils class has been fully refactored to improve the security and robustness of data object serialisation and deserialisation. Instead of resorting to external parsers or frameworks, the class implements a well-defined strategy for simple objects and for nested structures. The focus is on stability, readability and deterministic processing. A central component is the toJsonListingPaped method, which is used for tabular representation in the administration interface. It generates a JSON array from a list of mapping objects, including metadata such as the total number and paging information.

    public static String toJsonListingPaged(List<ShortUrlMapping> list, int total) {  var sb = new StringBuilder();  sb.append('{');  sb.append("\"total\":").append(total).append(',');  sb.append("\"items\":[");  for (int i = 0; i < list.size(); i++) {    sb.append(toJson(list.get(i)));    if (i < list.size() - 1) sb.append(',');  }  sb.append("]}");  return sb.toString();}

    This method exemplifies the class’s philosophy: complete control over serialisation and field order. This guarantees that the generated JSON structures correspond precisely to the expected format – an important point when the backend and UI are developed independently of each other. The use of StringBuilder is not a stylistic device, but a deliberate design to minimise garbage objects under high request load.

    Another central component is the UrlMappingFilterHelper, which handles dynamic processing of filter and sorting parameters for REST endpoints. This class abstracts the transformation of the user-entered filters into internal predicate objects, which are then applied to the stored mappings at runtime. The implementation supports flexible queries without requiring specialised query languages or parsers. The following excerpt shows the method that evaluates a URL mapping based on several parameters:

    public static boolean matches(ShortUrlMapping m, UrlMappingListRequest req) {  if (req.codePart() != null && !m.shortCode().contains(req.codePart())) return false;  if (req.urlPart() != null && !m.originalUrl().contains(req.urlPart())) return false;  if (req.from() != null && m.createdAt().isBefore(req.from())) return false;  if (req.to() != null && m.createdAt().isAfter(req.to())) return false;  return true;}

    This compact logic ensures that filter requests are processed directly at the object level. The system does not have to generate intermediate representations or convert data. The result is efficient, storage-level filtering that scales to large datasets. In combination with the store’s advanced counting methods, it provides a high-performance foundation for complex search and administrative functions.

    This area is rounded off by the extension of the AliasPolicy, in particular by the introduction of the helper method failed(). This is used for precise error evaluation and simplifies the control logic in components such as the MappingCreator. Instead of laboriously checking validation results, the developer can directly ask if a validation has failed, and then take fine-grained action based on the return value. This not only improves the readability of the codebase but also the overall system’s error resistance.

    The adjustments above make it clear that there is a well-thought-out concept behind the supposed auxiliary classes. They are not marginal components, but supporting pillars of data consistency and system stability. Through targeted optimisations and a clear methodological framework, a core module is created that serves as the basis for the project’s further development, both functionally and architecturally.

    Before & After – Impact on the developer experience

    With the integration of EclipseStore and the unification of generation logic via MappingCreator, the developer experience in this project has fundamentally changed. What began as a loose interplay of individual components has developed into a precisely orchestrated system that convinces both in its architecture and maintainability. This is particularly evident in the reduction of redundancies, improved testability and increased transparency in the code flow.

    In the past, central operations – especially the creation and verification of new mappings – were scattered across multiple classes. The InMemory store contained its own validation and checking routines, while later extensions had to reimplement the same logic for persistent storage. With the MappingCreator, this process has now been transformed into a clearly defined, reusable unit. This not only eliminates duplicate code but also the risk of different implementations delivering divergent results. The following excerpt shows an example of how the creation of a mapping was previously realised within the store:

    public ShortUrlMapping create(String alias, String url) {  String shortCode = alias != null ? alias : generator.nextCode();  if (map.containsKey(shortCode)) {    throw new IllegalStateException("Alias already exists: " + shortCode);  }  var mapping = new ShortUrlMapping(shortCode, url, Instant.now(), Optional.empty());  map.put(shortCode, mapping);  return mapping;}

    This logic was functionally correct, but inflexible. Neither validation nor error objects nor alias rules were built in, and there was no uniform handling of failures or conflicts. With the introduction of MappingCreators and the centralised error structure, this process has been redesigned. The result is a deterministic, controlled, and traceable generation of short URLs, in which every step – from input to persistence – occurs via clearly defined interfaces.

    The second significant difference concerns how the memory is addressed. Previously, the entire application was designed for volatile data retention, which meant that each reboot resulted in a complete loss of the stored mappings. With the EclipseStore, this state has been fundamentally changed. The following code shows how the Store initialises its persistent database when the application starts:

    var storagePath = Paths.get(storageDir);Files.createDirectories(storagePath);this.storage = EmbeddedStorage.start(storagePath);DataRoot r = (DataRoot) storage.root();if (r == null) {  storage.setRoot(new DataRoot());  storage.storeRoot();}

    These few lines mark the crucial difference between temporary and permanent system states. The DataRoot’s data structure is loaded at startup, changes are immediately synchronised, and the state is maintained across reboots. From a developer’s point of view, this means that unit tests, integration tests and runtime behaviour can now consistently access the same database. The difference between development and production disappears because both use the same data flow and persistence logic.

    This standardisation also directly affects the user interface. The Vaadin components, such as OverviewView and StoreIndicator, now obtain their data via clearly defined interfaces that are decoupled from the persistence layer. State changes of the store or new mappings trigger events that are consumed by the UI components, without explicit dependencies. This creates a consistent, reactive system that works coherently from the data source to the surface.

    This development has fundamentally changed how developers interact with code. Bugs are easier to reproduce, tests are more stable, and extensions can be modular. The code is no longer a collection of independent methods, but a structured system in which each component has its place and responsibility.

    Cheers Sven

    #Advent2025 #EclipseStore #Java #Vaadin

    Hallöchen Mastos heute zum #throwbackthursday kleiner Funclip zur Weihnachtszeit.
    #advent2025 #weihnachten

    🚨 Dream vacation or eternal nightmare?

    Your salvation is NEARER than you think. Time to shine as light in this dark world! ✨ Who's ready for eternal joy?

    Watch now: https://zurl.co/5hCwO

    #Advent2025 #JesusIsComing #ChurchYear #ChristianHope #SecondComing #PastorJustinWixon #MaranaAZ #LCMS #BibleTruth #FaithJourney #EternalLife

    AF Advent Day 4 🥂✨

    (I started these and can't seem to stop now 😂)

    #AFadvent #WiseBartender #Advent2025 #SoberNotBoring

    L' #adventofcode d'avui era facilet així que he aprofitat el temps per aprendre com van els crates i modules de #Rust 🦀 ⚙️

    M'ha costat però he unificat totes les solucions, de manera que cada dia només he d'implementar la funció `solve_problem` enlloc de fer un main nou i tota la pesca.

    https://adventofcode.com/2025/day/4

    #rustlang #advent2025 #codeberg

    Day 4 - Advent of Code 2025

    Good morning! Here are today’s #Advent2025 readings if you’re following along:

    Psalm 72:1-7, 18-19; Isaiah 4:2-6; Acts 1:12-17, 21-26