Register the Yngling one-design keelboat in the BoatClassMasterdata enum
with its LOA (6.35 m), beam (1.73 m), monohull hull type and downwind-sail
flag (symmetric spinnaker). Wire the class to a 140x140 transparent PNG
derived from the International Yngling Association class insignia via the
GWT BoatClassImageResources ClientBundle and the BoatClassImageResolver
display-name map, and add it to the KeelBoatWithGennaker vector graphics in
BoatClassVectorGraphicsResolver so it renders as a keelboat on the race map
rather than falling back to the default spinnaker-dinghy shape.
Also document the addition in the "What's New" (SailingAnalyticsNotes.html)
and Admin Console release notes (release_notes_admin.html) under September
2026.
Assisted-By: Claude Opus 4.8 (claude-opus-5-5)
Switch the wind-track section count on both MDI wire-format ends from
writeObject(Integer)/readObject to writeInt/readInt. The count is now a
bare 4-byte inline int carrying no class descriptor and occupying no
handle-table entry; the null sentinel remains the authority on section
end, the count only drives progress. Javadoc/comments on both ends
updated accordingly.
Introduce shared named constants for the MDI progress schedule
(PROGRESS_TRANSFER_STARTED, PROGRESS_TRANSFER_COMPLETED,
PROGRESS_GRAPH_APPLIED, PROGRESS_WIND_TRACKS_DONE, PROGRESS_COMPLETE)
and derive the two 0.2 band widths from the milestones
(PROGRESS_WIND_TRACKS_BAND_WIDTH, PROGRESS_SENSOR_FIXES_BAND_WIDTH) so
the bands can never drift from the milestones. Replaces the redundant
0.03/0.3/0.6/0.8/1.0/0.2 float literals scattered across the import
progress call sites with a single source of truth.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
Introduces two convenience methods on TimePoint that step by one resolution
unit rather than a hard-coded millisecond:
TimePoint plusResolution() == plus(getResolution())
TimePoint minusResolution() == minus(getResolution())
They are implemented once in AbstractTimePoint and saturate at EndOfTime /
BeginningOfTime just like plus/minus do. Expressing "the next/previous
representable instant" through getResolution() keeps these sites correct
should a finer-resolution TimePoint ever be introduced, instead of assuming
one-millisecond granularity.
All plus(1)/minus(1) call sites whose receiver is a TimePoint and whose intent
is a single resolution tick were converted to plusResolution()/minusResolution()
across production and tests. Deliberately left unchanged:
- plus(1)/minus(1) on Duration receivers (not TimePoints)
- TimePointTest overflow/saturation tests that exercise plus(long)/minus(long)
with the literal 1 on purpose
Assisted-By: Claude Opus 4.8 (claude-opus-5)
Introduce TimePoint.getResolution() returning the smallest positive Duration by which two
distinct instants of an implementation can differ, implemented in AbstractTimePoint as
Duration.ONE_MILLISECOND (all current impls are millisecond-backed). TopLevelMasterData's
inclusive-to-exclusive conversion for device mapping ranges now uses
toInclusive.plus(toInclusive.getResolution()) instead of a hard-coded +1ms, so a
single-instant PING mapping stays a non-empty range and the code remains correct should a
finer-resolution TimePoint ever be introduced.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
RegattaLogDeviceMappingEvent.getToInclusive() is an inclusive end, but
TimeRange.to() is exclusive. TopLevelMasterData.addRangeIfMappingEvent
built TimeRange.create(from, toInclusive); for a single-instant PING
mapping (from == toInclusive, e.g. a pinged mark) this is an empty range,
which MultiTimeRange silently discards. The device is then never streamed
and its single fix never reaches the importing instance.
Convert the inclusive end to an exclusive one by adding one TimePoint
resolution unit (currently 1ms), so [t, t] becomes the non-empty range
[t, t+1ms), and load the corresponding fixes with toIsInclusive == false
in MasterDataResource.writeRaceLogTrackingFixes. Update the related
Javadoc on both sides accordingly.
Adds MasterDataImportTest.testMasterDataImportPingedMarkSingleInstantFix,
which reproduces the drop (single-instant inclusive mapping with one
stored fix) and now passes.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
While streaming a single device's race-log tracking GPS fixes, reset() was
called only once per device. After the bug6227 per-device MultiTimeRange
merge, each device's range can span an entire season, so the
ObjectOutputStream$HandleTable accumulated a whole season of fixes for a
device before being cleared, causing the exporting server to be OOM-killed
(a Memory Analyzer report attributed ~616 MB / 73% of the heap to a single
HandleTable rooted at writeRaceLogTrackingFixes).
Keep the existing per-device reset() and additionally reset() after every
FIXES_PER_HANDLE_TABLE_RESET (1000) fixes, bounding the handle table to at
most 1000 retained fixes at any time. Class descriptors are re-emitted after
each reset() but amortize cheaply and gzip-compress well; the null-sentinel
framing is unaffected by TC_RESET position.
Assisted-By: Claude Opus (claude-opus-5)
The AdminConsole client reported import success before the sensor-fix import
had even started. The client's completion signal is a non-null
DataImportProgress result, but ImportMasterDataOperation.internalApplyTo set
the result and overall progress 1.0 at the end of the object-graph apply() --
which returns to MasterDataImporter.importFromStream, where the wind-track and
sensor-fix imports still run afterward. So "Done"/1.0/result fired prematurely.
Move ownership of terminal completion to MasterDataImporter.importFromStream,
the true end of the whole import (object graph + wind + fixes):
- ImportMasterDataOperation.internalApplyTo no longer sets the result, overall
1.0, or logs "Done"; it ends at overall 0.6 and returns creationCount.
- MasterDataImporter publishes the single terminal completion after the wind
band (0.6->0.8) and sensor-fix band (0.8->1.0):
createOrUpdateDataImportProgressWithReplication(id, 1.0, IMPORT_SENSOR_FIXES,
1.0, creationCount). The result is set only at true 1.0, with replication,
so replica-polling clients observe completion correctly.
- Plumb a nullable MasterDataImportObjectCreationCount through a new 5-arg
createOrUpdateDataImportProgressWithReplication overload and the
CreateOrUpdateDataImportProgress operation, which sets the result on replicas
via progress.setResult(result) when non-null.
- createOrUpdateDataImportProgressWithoutReplication now uses getDataImportLock()
instead of the raw field so subclass overrides (the test mock) are honored;
a no-op in production where the getter returns the field.
serialVersionUIDs left unchanged: adding a nullable field is serialization-
compatible and master/replicas upgrade together.
Verified: MasterDataImportTest 13/13 green; both projects compile clean.
Assisted-By: Claude Opus (claude-opus-5)
Follow-up to fd6739d9a6 (stream GPS fixes as top-level objects), addressing
four defects on the master data import path:
- Merge DeviceMappingDescriptor objects per DeviceIdentifier into a single
MultiTimeRange so a device mapped in several regattas (or via several
open-ended mapping events) has its fixes streamed once over coalesced,
non-overlapping sub-ranges rather than once per mapping. Obsoletes and
removes the nested DeviceMappingDescriptor class; the field becomes
Map<DeviceIdentifier, MultiTimeRange> raceLogTrackingDeviceRanges. The
inclusive mapping end is carried through and streamed with
toIsInclusive=true, at worst streaming a harmless superset (store dedups).
- Apply the top-level-streaming + per-object reset() design to
WindTrackMasterData: strip the wind tracks out of the serialized graph,
stream one WindTrackMasterData at a time on export (held only transiently),
and read+store them incrementally on import (master only). Bounds retained
memory to a single wind track at a time instead of the whole season.
- Remove the premature "Done importing master data into ..." log and the
overall 1.0 from ImportMasterDataOperation.internalApplyTo; that fired
before the sensor-fix import ran. The single terminal "Done" and 1.0 now
live in MasterDataImporter.importFromStream, the true end of the import.
- Make progress rise monotonically and reserve the two phases that usually
dominate the import: the object graph now ends at 0.6, the wind-track
import occupies 0.6 -> 0.8 (20%), and the sensor-fix import occupies
0.8 -> 1.0 (20%), instead of reaching 1.0 in the operation and then
dropping back in the importer.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
Master data import/export (MDI) previously materialized every mapped
device's GPS fixes in memory before serialization: the fixes lived inside
the single TopLevelMasterData object graph written with one writeObject, so
ObjectOutputStream/ObjectInputStream had to retain a handle for every fix to
resolve potential back-references. For events with many fixes this dominated
heap on both the exporting and importing server.
Change the wire format so fixes are streamed as top-level stream objects
after the (now fix-free) TopLevelMasterData:
- TopLevelMasterData no longer carries the fixes themselves. It records
lightweight, fully Serializable DeviceFixMappingDescriptor entries
(device, from, toInclusive) plus a transient SensorFixStore used only on
the export side. Default serialization now does the right thing; the
stripped replica copy carries no descriptors.
- MasterDataResource.writeRaceLogTrackingFixes streams each device section
straight from the SensorFixStore: device header, each fix as a top-level
object, a null fix terminator, then ObjectOutputStream.reset() once per
device so no fix handle is retained across devices while the fix class
descriptor stays shared within a device. A trailing null device ends the
section. Framing uses null sentinels, never fix counts (a concurrent write
could invalidate a count).
- MasterDataImporter.importRaceLogTrackingGPSFixes reads that section
incrementally and stores fixes in batches of BATCH_SIZE_FOR_IMPORTING_FIXES
directly into the master's SensorFixStore, holding at most one batch at a
time. This stream-based import only ever runs on the primary/master:
such requests must always be routed there, and a replica receives only the
stripped ImportMasterDataOperation (no fixes), obtaining tracking data by
the master loading tracked races whose transactions are replicated. A
non-null master descriptor here is therefore a broken routing state and is
reported by throwing IllegalStateException rather than silently draining.
IMPORT_SENSOR_FIXES sub-progress is advanced once per device section,
driven by the descriptor count, restoring the per-device granularity that
existed before.
- ImportMasterDataOperation no longer imports fixes; that logic moved to
MasterDataImporter, which has the target store in scope.
Exporter and importer are upgraded together; mixed-version transfer is not
required.
Verified: MasterDataImportTest green (full class 13/13 repeatedly;
testMasterDataImportWithoutHttpStack repeatedly), covering the 4999/5000/5001
batch-boundary fix counts and the DoubleVectorFix round trip.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
Drop the leftover TODO and commented-out NO_REGATTA / BMW_CUP_RACE_8
constants and the surrounding blank lines; no behavioral change.
Assisted-By: Claude (claude-4.8-opus)
TestStartStopTrackingCycleForRaceLog failed intermittently with
NoSuchElementException on ACTION_START_TRACKING: at click time the race
rows still showed the un-denoted action set, i.e. the leaderboard
denotation had not applied client-side yet.
Root cause: the leaderboard-level denoteForRaceLogTracking RPC in
SmartphoneTrackingEventManagementPanel used a plain AsyncCallback, so it
was invisible to the PENDING_AJAX_CALLS counter that only
MarkedAsyncCallback touches. waitForAjaxRequests() therefore returned
before the denote->refresh chain settled, and the test clicked against
the stale DOM. This was exposed by the Selenium 3.141 -> 4.13 upgrade,
not by a bug6305 functional change.
Two fixes:
1. Production (root): wrap the denote RPC callback in MarkedAsyncCallback,
matching the sibling RPCs in the same panel, so the pending-call
counter stays non-zero until the whole denote->refresh chain settles.
Makes waitForAjaxRequests() honest for this RPC for any test.
2. Test robustness: startTrackingForRace now takes the race name and
waits state-based for the ACTION_START_TRACKING image to appear
(re-fetching the row each poll to survive GWT re-renders) before
clicking, via a new DataEntryPO.hasActionImage helper, instead of
assuming row presence implies the action is available.
Verified: test passes on two independent local runs (98.9s and 275.5s),
both denote->start->stop->undenote cycles green.
Assisted-By: Claude (claude-4.8-opus)
The Fareast 28R boat class (added in e2a2c4d) was never assigned a vector
graphics in BoatClassVectorGraphicsResolver, so it fell back to the default
spinnaker-dinghy shape on the race map. Add it to the KeelBoatWithGennaker
group so this 9.07 m monohull renders as a keelboat, matching its actual
type.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
Register three Swiss one-design keelboat classes in the BoatClassMasterdata
enum with their LOA, beam, hull type and downwind-sail flag. Wire each class
to a 140x140 transparent PNG class-association logo via the GWT
BoatClassImageResources ClientBundle and the BoatClassImageResolver
display-name map, and assign all three to the KeelBoatWithGennaker vector
graphics in BoatClassVectorGraphicsResolver so they render as keelboats on
the race map rather than falling back to the default spinnaker-dinghy shape.
Also document the addition in the "What's New" (SailingAnalyticsNotes.html)
and Admin Console release notes (release_notes_admin.html) under September
2026.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
The user-creation rate limit and animated ACL dialogs caused flaky
failures in TestNegativeAcls:
- The per-IP user-creation lock (server side) previously was only worked
around by a client-side Thread.sleep for the full locking duration.
Instead, when a UserManagementPanelPO has an AdminConsolePage, it now
clears the server-side lock through the "User Creation Abuse" blocklist
UI before creating a user. The legacy sleep-based throttle is retained
as a fallback when no AdminConsolePage is available.
- IpBlocklistPanelPO.unblockIP now polls a freshly refreshed table until
the unblocked row is gone, tying completion to a server round-trip. The
unlock and data-load GWT-RPCs in IPBlocklistTableWrapper are now wrapped
in MarkedAsyncCallback so waitForAjaxRequests() can observe them instead
of being a silent no-op.
- AclActionInputPO.addAction waits for the suggest box to be displayed and
enabled before typing, fixing an ElementNotInteractableException when the
denied-actions input is still settling after adding a user group.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
The negative-ACL Selenium tests intermittently failed with
StaleElementReferenceException (masked as a generic RuntimeException) and
a NullPointerException when reading a freshly-created role/permission row
that had not yet rendered.
Root causes and fixes:
- Several user-management RPCs were fired through a plain AsyncCallback, so
they were invisible to the pending-Ajax counter and waitForAjaxRequests()
returned before their deferred CellTable repaint ran. Wrap them in
MarkedAsyncCallback: createRoleDefinition, getRolesAndPermissionsForUser,
getUserList, addPermissionForUser, removePermissionFromUser and
removeRoleFromUser.
- Pair those marks with waitForAjaxRequests() barriers on the test side after
add/delete clicks (WildcardPermissionPanelPO, UserRoleDefinitionPanelPO).
The ...AndExpectPermissionError() variants deliberately click directly and
wait for the alert instead, since the RPC fails rather than completing.
- Add RoleDefinitionsPanelPO.findRoleWhenPresent(): wait for Ajax, then poll
until the row is present before returning it, and use it from
TestNegativeAcls so a not-yet-rendered row no longer NPEs.
- Stop CellTablePO/GenericCellTablePO from masking staleness: narrow
createEntry's catch to the exceptions the reflective construction can
actually throw, unwrap InvocationTargetException(StaleElementReferenceException)
and rethrow it, and retry getEntries() a bounded number of times so a
redraw-during-read is tolerated rather than surfaced as an opaque failure.
- PageObject.waitUntil now ignores StaleElementReferenceException.
Assisted-By: Claude Opus 4.8 (Claude Code)
Correct the English source message for the stop-auto-renewal
confirmation dialog. Other locales are produced by the downstream
translation process from the English source.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
The ConfirmationDialog message is shown in a GWT Label whose default
white-space handling collapses newlines (and runs of whitespace) into
single spaces, so multi-line messages appeared on a single line. Add
white-space: pre-line to the .infoText style so intentional line
separators in the message are rendered as line breaks while long lines
still wrap.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
SimulatorMap uses the same GoogleMapsLoader as RaceMap, so a rejected
Google Maps key left its map blank with no diagnostic in the same way.
Register the same gm_authFailure listener here, reporting through the
existing errorReporter with the shared errorGoogleMapsAuthenticationFailed
message. Mirrors the RaceMap change.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
When the Google Maps JavaScript API rejects the request (invalid or
missing key, disallowed referrer, API not enabled, or a quota / billing
problem), the map area stayed blank with no controls and no diagnostic.
The RaceBoard's map controls, including the "moreOptionsButton", are
created only inside the loader's onLoad callback, so a rejected key left
that button absent -- which surfaced downstream only as a generic 60s
Selenium timeout (NoSuchElementException: moreOptionsButton) with an
otherwise fully rendered page.
GoogleMapsLoader now installs a window.gm_authFailure hook that the Maps
API invokes exactly on such an authentication failure, and RaceMap
registers a listener that reports it through the existing errorReporter
with a dedicated, translatable message. This is event-driven rather than
timeout-based, so it never misfires on a slow but valid load (relevant
for slow connections and low-end devices).
Assisted-By: Claude Opus 4.8 (claude-opus-5)
The GitHub-hosted runner image rolled out on ~2026-09-14 ships
cmdline-tools 12.0, whose sdkmanager no longer resolves the obsolete
'tools' (legacy Android SDK Tools) package. The android-actions/setup-android
step inherits the action default packages 'tools platform-tools' and thus
failed with "Failed to find package 'tools'" (exit code 1) before the build
script even ran. The same request in buildAndUpdateProduct.sh would fail
the same way once reached.
Override the action's packages input to 'platform-tools' and drop the
trailing 'tools' argument from the sdkmanager call in
buildAndUpdateProduct.sh. build-tools, platform-tools and the platform
are still installed by the script. The cmdline-tools-version pin is left
unchanged.
Assisted-By: Claude (claude-4.8-opus)
A replica set whose server predates bug6288 does not expose
GET /sailingserver/api/v1/livecontent; the master answers HTTP 400 with a
text/plain body. SailingServerImpl.getLiveContent then deserialized a null
JSON body and threw an NPE that surfaced in the UI as an unreadable error.
A secondary bug aborted the whole batch on the first such server.
Detect the non-success response cleanly and model the outcome as a
first-class per-replica-set UNDETERMINED state, treated fail-safe
(block-unless-forced):
- New checked exception LiveContentCheckUnsupportedException in
landscape.common (the only bundle both server.gateway and landscape see).
- SailingServerImpl.getLiveContent inspects the response status and throws
it instead of deserializing a null body.
- LandscapeServiceImpl.checkForLiveContent catches per replica set, records
the name as undetermined, and continues the loop (fixes batch-abort);
checkForLiveContentUnlessForced now also blocks on undetermined sets.
- LiveContentCheckResult carries a parallel undeterminedReplicaSetNames
list (new 3-arg constructor; 2-arg preserved; serialVersionUID
regenerated). JSON and GWT wire formats extended in lockstep; JSON
deserializer tolerates the field's absence for backward compatibility.
- UI shows a single combined dialog with a live-content section and a
'could not verify (server not upgraded)' section; forcing proceeds past
both. English and German messages added.
Also fixes a missing result.add(processJson) in SailingLandscapeResource
that left the returned JSON array empty.
Assisted-By: Claude (claude-4.8-opus)
Describe the new per-operation "force" and per-replica-set
"forceReplicaSet" parameters in the affected webservices/api docs
(movetoarchiveserver, removeapplicationreplicaset, movemastertootherinstance,
upgradeapplicationreplicaset, stopreplicatingandremovemasterfromtargetgroups,
moveallapplicationprocessesawayfrom), explaining the live-content pre-check and
the resulting HTTP 409 when live content is present, and how force bypasses it.
Add docs for endpoints that had none and link them from index.html:
livecontentcheck, deployreplicatoexistinghost,
gethostseligibleforreplicasetprocessdeployment,
changeautoscalingreplicasinstancetype. The livecontentcheck doc explains its
relationship to the single-server GET /v1/livecontent counterpart.
Audit index.html for link completeness: all landscape-gateway endpoints are now
linked except createmfasessioncredentials (covered by createsessioncredentials.html);
no dangling links or orphaned docs remain.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
Add an up-front live-content pre-flight to the five landscape operations
(move-master, move-away, remove, archive, upgrade) in
LandscapeManagementPanel. Before mutating anything, each operation now
checks its whole selection for live content in a single round-trip and
warns the user once, up front, rather than surfacing a warning mid-walk
(possibly minutes in, unattended, after client timeouts).
Force is scoped strictly to the replica sets that the pre-flight flagged
and the user confirmed: per-set operations force a set only when
confirmedConflictingReplicaSetNames.contains(set.getName()), and the
batch move-away passes only the confirmed conflicting names. Every other
set runs force=false and keeps its reactive back-end guard as a fallback
for surprise races that begin after the pre-flight check.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
The confirm-and-force retry in LandscapeManagementPanel needs each
issueRequest lambda to reference itself, which previously forced a
one-element Consumer[] array holder plus @SuppressWarnings("unchecked").
Introduce com.sap.sse.common.util.Holder<T> (mirroring IntHolder/
DoubleHolder, GWT/Android/OSGi-safe) and use it at all five sites,
removing the array idiom and the unchecked-cast suppressions.
No behavioral change: each AsyncCallback is still constructed inside the
lambda body, so a fresh callback is created per (initial and forced)
invocation.
Also carry over the CLAUDE.md note about keeping auto-formatting scoped
to the actual change.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
Each branch that handles a refused landscape operation now explains that
the refusal is a live-content conflict (one or more affected replica sets
are currently serving live content) and that the branch offers the user
the chance to proceed and force the operation anyway.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
The live-content warning shown by LandscapeManagementPanel was a dead end for
five operations (upgrade, move-master-to-other-instance, move-all-away,
archive, remove): on a live-content conflict the panel only displayed an
informational alert and left the operation aborted, with no way to override it
even though the backend already accepts a "force" flag.
Wire the confirm-and-force path (already used by "stop replicating") into all
five: on a conflict, show the warning via a ConfirmationDialog whose callback,
when the user chooses to proceed, re-issues the same request with force enabled
(boolean force=true for upgrade/move-master/archive/remove; the conflicting
replica-set names as forceMasterReplicaSetNames for move-all-away). For the
batched upgrade, the busy-countdown is re-incremented before the forced retry
so the busy indicator stays balanced.
With every operation now offering a "proceed anyway" path, the no-callback
showLiveContentWarning overload has no remaining callers and is removed.
Assisted-By: Claude Opus 4.8 (claude-opus-5)
The result of moveAllApplicationProcessesAwayFrom was wrapped in a
MoveAllApplicationProcessesResultDTO that carried a single String, the ID of
the new host. That wrapper forced a hand-written GWT-RPC
MoveAllApplicationProcessesResultDTO_CustomFieldSerializer to exist purely to
serialize that one string, and the client never even read getNewHostId(). The
service now returns LiveContentAwareOperationResult<String> directly; GWT
serializes String natively. The DTO class, its custom field serializer and the
corresponding Export-Package entry are removed, and the new-host-ID result is
documented on the Async interface's callback parameter.
Assisted-By: Claude Opus 4.8 (claude-opus-5)