Compare commits
42 Commits
v0.7.1
...
9ad7651c44
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ad7651c44 | |||
| 389392fd1d | |||
| aaebeaa547 | |||
| 03ee3855f5 | |||
| 94a39cf3b4 | |||
| efe6f6c2b0 | |||
| 73a9d15786 | |||
| dfe05cf2d2 | |||
| fcba907438 | |||
| 5739602702 | |||
| addf78f01d | |||
| 5e04e84ee4 | |||
| 8d5c2e2b7f | |||
| 788d2c12f2 | |||
| b25a9746cf | |||
| 41fda9aeee | |||
| 550078268c | |||
| 0582690dca | |||
| 88278bd1dd | |||
| d24d6459a0 | |||
| 4b866e5212 | |||
| 6c6bd20f0d | |||
| 2764228abf | |||
| f95d69c915 | |||
| 70351e9d9a | |||
| ff4905126d | |||
| 0e5b5a7de4 | |||
| c8c032336b | |||
| dda27e55fc | |||
| 83ac67471e | |||
| e3c8232e38 | |||
| a4df9fc759 | |||
| f1989c1d77 | |||
| 8efdf5d0e0 | |||
| 96bc5de942 | |||
| 84ccdd53ad | |||
| 29978058ee | |||
| e510f64336 | |||
| f189f67aaf | |||
| 8efa148739 | |||
| 8f4dd3e9d6 | |||
| bf38b6695f |
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
<groupId>com.loremind</groupId>
|
<groupId>com.loremind</groupId>
|
||||||
<artifactId>loremind-core</artifactId>
|
<artifactId>loremind-core</artifactId>
|
||||||
<version>0.7.1</version>
|
<version>0.7.0</version>
|
||||||
<name>LoreMind Core</name>
|
<name>LoreMind Core</name>
|
||||||
<description>Backend Core - Architecture Hexagonale</description>
|
<description>Backend Core - Architecture Hexagonale</description>
|
||||||
|
|
||||||
|
|||||||
@@ -32,13 +32,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* image suivie ({@code update-check.images}). On stocke ces digests comme
|
* image suivie ({@code update-check.images}). On stocke ces digests comme
|
||||||
* "baseline" (= ce que le conteneur en cours d'execution est cense faire
|
* "baseline" (= ce que le conteneur en cours d'execution est cense faire
|
||||||
* tourner, puisque le `docker compose pull` precede toujours `up -d`).
|
* tourner, puisque le `docker compose pull` precede toujours `up -d`).
|
||||||
* - Si l'init echoue (reseau Docker pas encore pret, registry transitoirement
|
|
||||||
* indisponible), un thread daemon de retry avec backoff complete les
|
|
||||||
* baselines manquantes en arriere-plan.
|
|
||||||
* - {@link #check()} re-interroge le registry et compare. Si un digest a
|
* - {@link #check()} re-interroge le registry et compare. Si un digest a
|
||||||
* change, une mise a jour est disponible. Si la baseline manque (echec
|
* change, une mise a jour est disponible.
|
||||||
* de tous les retries), retourne {@link ImageStatusKind#UNKNOWN} pour
|
|
||||||
* cette image — JAMAIS d'alignement silencieux (eviterait des MAJ ratees).
|
|
||||||
* - {@link #apply()} POST sur /v1/update de Watchtower (qui doit etre lance
|
* - {@link #apply()} POST sur /v1/update de Watchtower (qui doit etre lance
|
||||||
* avec WATCHTOWER_HTTP_API_UPDATE=true et le meme token).
|
* avec WATCHTOWER_HTTP_API_UPDATE=true et le meme token).
|
||||||
*
|
*
|
||||||
@@ -89,9 +84,6 @@ public class UpdateCheckService {
|
|||||||
this.watchtowerToken = watchtowerToken;
|
this.watchtowerToken = watchtowerToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Backoff progressif (ms) pour retry de baseline en cas d'echec initial. */
|
|
||||||
private static final long[] BASELINE_RETRY_BACKOFFS_MS = {2_000, 5_000, 15_000, 30_000, 60_000};
|
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
void initBaseline() {
|
void initBaseline() {
|
||||||
if (!isEnabled()) {
|
if (!isEnabled()) {
|
||||||
@@ -99,19 +91,7 @@ public class UpdateCheckService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info("Update check enabled - registry={} images={} tag={}", registry, images, tag);
|
log.info("Update check enabled - registry={} images={} tag={}", registry, images, tag);
|
||||||
boolean complete = tryBaselineMissing();
|
|
||||||
if (!complete) {
|
|
||||||
startBaselineRetryThread();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tente de poser la baseline pour les images qui ne l'ont pas encore.
|
|
||||||
* @return true si TOUTES les images ont leur baseline apres cet essai.
|
|
||||||
*/
|
|
||||||
private boolean tryBaselineMissing() {
|
|
||||||
for (String image : images) {
|
for (String image : images) {
|
||||||
if (baselineDigests.containsKey(image)) continue;
|
|
||||||
try {
|
try {
|
||||||
String digest = fetchRemoteDigest(image);
|
String digest = fetchRemoteDigest(image);
|
||||||
if (digest != null) {
|
if (digest != null) {
|
||||||
@@ -122,33 +102,6 @@ public class UpdateCheckService {
|
|||||||
log.warn("Cannot baseline digest for {}: {}", image, e.getMessage());
|
log.warn("Cannot baseline digest for {}: {}", image, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return baselineDigests.size() == images.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lance un thread daemon qui retente de poser les baselines manquantes
|
|
||||||
* avec backoff. Le thread s'arrete des que toutes les baselines sont
|
|
||||||
* posees, ou apres epuisement des backoffs (et alors {@link #check()}
|
|
||||||
* retournera UNKNOWN pour ces images jusqu'au prochain redemarrage).
|
|
||||||
*/
|
|
||||||
private void startBaselineRetryThread() {
|
|
||||||
Thread t = new Thread(() -> {
|
|
||||||
for (long backoff : BASELINE_RETRY_BACKOFFS_MS) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(backoff);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (tryBaselineMissing()) {
|
|
||||||
log.info("Baseline complete after retry");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.warn("Baseline incomplete after all retries; check() will return UNKNOWN for missing images");
|
|
||||||
}, "update-baseline-retry");
|
|
||||||
t.setDaemon(true);
|
|
||||||
t.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -157,11 +110,10 @@ public class UpdateCheckService {
|
|||||||
|
|
||||||
public UpdateStatus check() {
|
public UpdateStatus check() {
|
||||||
if (!isEnabled()) {
|
if (!isEnabled()) {
|
||||||
return new UpdateStatus(false, false, false, List.of(), Instant.now());
|
return new UpdateStatus(false, false, List.of(), Instant.now());
|
||||||
}
|
}
|
||||||
List<ImageStatus> statuses = new ArrayList<>();
|
List<ImageStatus> statuses = new ArrayList<>();
|
||||||
boolean anyUpdate = false;
|
boolean anyUpdate = false;
|
||||||
boolean anyUnknown = false;
|
|
||||||
for (String image : images) {
|
for (String image : images) {
|
||||||
String baseline = baselineDigests.get(image);
|
String baseline = baselineDigests.get(image);
|
||||||
String remote = null;
|
String remote = null;
|
||||||
@@ -170,21 +122,17 @@ public class UpdateCheckService {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Check failed for {}: {}", image, e.getMessage());
|
log.warn("Check failed for {}: {}", image, e.getMessage());
|
||||||
}
|
}
|
||||||
// PAS d'alignement lazy si baseline absente : ce serait un faux negatif
|
// Si on n'a pas de baseline (echec au boot), on l'aligne maintenant
|
||||||
// silencieux. On reporte UNKNOWN pour que l'UI le signale.
|
// pour eviter un faux positif "MAJ dispo".
|
||||||
ImageStatusKind kind;
|
if (baseline == null && remote != null) {
|
||||||
if (baseline == null || remote == null) {
|
baselineDigests.put(image, remote);
|
||||||
kind = ImageStatusKind.UNKNOWN;
|
baseline = remote;
|
||||||
anyUnknown = true;
|
|
||||||
} else if (baseline.equals(remote)) {
|
|
||||||
kind = ImageStatusKind.UP_TO_DATE;
|
|
||||||
} else {
|
|
||||||
kind = ImageStatusKind.UPDATE_AVAILABLE;
|
|
||||||
anyUpdate = true;
|
|
||||||
}
|
}
|
||||||
statuses.add(new ImageStatus(image, baseline, remote, kind));
|
boolean updateAvailable = baseline != null && remote != null && !baseline.equals(remote);
|
||||||
|
if (updateAvailable) anyUpdate = true;
|
||||||
|
statuses.add(new ImageStatus(image, baseline, remote, updateAvailable));
|
||||||
}
|
}
|
||||||
return new UpdateStatus(true, anyUpdate, anyUnknown, statuses, Instant.now());
|
return new UpdateStatus(true, anyUpdate, statuses, Instant.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void apply() {
|
public void apply() {
|
||||||
@@ -330,38 +278,15 @@ public class UpdateCheckService {
|
|||||||
// Records de retour (sortis sous forme JSON par Jackson)
|
// Records de retour (sortis sous forme JSON par Jackson)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
|
||||||
* Etat tri-state d'une image vis-a-vis du registry.
|
|
||||||
* <ul>
|
|
||||||
* <li>{@link #UP_TO_DATE} : digest local == digest remote.</li>
|
|
||||||
* <li>{@link #UPDATE_AVAILABLE} : digests differents, MAJ disponible.</li>
|
|
||||||
* <li>{@link #UNKNOWN} : impossible de comparer (baseline ou remote manquant).
|
|
||||||
* L'UI doit afficher un avertissement plutot que de declarer "a jour".</li>
|
|
||||||
* </ul>
|
|
||||||
*/
|
|
||||||
public enum ImageStatusKind { UP_TO_DATE, UPDATE_AVAILABLE, UNKNOWN }
|
|
||||||
|
|
||||||
public record UpdateStatus(
|
public record UpdateStatus(
|
||||||
boolean enabled,
|
boolean enabled,
|
||||||
boolean updateAvailable,
|
boolean updateAvailable,
|
||||||
boolean anyUnknown,
|
|
||||||
List<ImageStatus> images,
|
List<ImageStatus> images,
|
||||||
Instant checkedAt) {}
|
Instant checkedAt) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Le champ {@code updateAvailable} est conserve pour la compatibilite
|
|
||||||
* avec les anciens clients ; il est strictement derive de {@code status}
|
|
||||||
* dans le constructeur compact.
|
|
||||||
*/
|
|
||||||
public record ImageStatus(
|
public record ImageStatus(
|
||||||
String image,
|
String image,
|
||||||
String localDigest,
|
String localDigest,
|
||||||
String remoteDigest,
|
String remoteDigest,
|
||||||
ImageStatusKind status,
|
boolean updateAvailable) {}
|
||||||
boolean updateAvailable) {
|
|
||||||
|
|
||||||
public ImageStatus(String image, String localDigest, String remoteDigest, ImageStatusKind status) {
|
|
||||||
this(image, localDigest, remoteDigest, status, status == ImageStatusKind.UPDATE_AVAILABLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
package com.loremind.infrastructure.updates;
|
|
||||||
|
|
||||||
import com.loremind.infrastructure.updates.UpdateCheckService.ImageStatus;
|
|
||||||
import com.loremind.infrastructure.updates.UpdateCheckService.ImageStatusKind;
|
|
||||||
import com.loremind.infrastructure.updates.UpdateCheckService.UpdateStatus;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test unitaire pour UpdateCheckService.
|
|
||||||
*
|
|
||||||
* Couvre les invariants critiques de la detection de MAJ :
|
|
||||||
* - feature desactivee si token absent
|
|
||||||
* - status UP_TO_DATE quand baseline == remote
|
|
||||||
* - status UPDATE_AVAILABLE quand baseline != remote
|
|
||||||
* - status UNKNOWN quand baseline manque (PAS d'alignement lazy — invariant
|
|
||||||
* central, regression historique)
|
|
||||||
* - status UNKNOWN quand remote impossible a fetcher
|
|
||||||
* - drapeaux top-level updateAvailable / anyUnknown coherents
|
|
||||||
* - back-compat : champ updateAvailable sur ImageStatus = (status == UPDATE_AVAILABLE)
|
|
||||||
*/
|
|
||||||
public class UpdateCheckServiceTest {
|
|
||||||
|
|
||||||
private static UpdateCheckService newService(String token) {
|
|
||||||
return new UpdateCheckService(
|
|
||||||
new RestTemplateBuilder(),
|
|
||||||
"ghcr.io",
|
|
||||||
"igmlcreation/loremind-core,igmlcreation/loremind-brain",
|
|
||||||
"latest",
|
|
||||||
"http://watchtower:8080",
|
|
||||||
token
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injecte un RestTemplate moque dans le service deja construit, et pose
|
|
||||||
* directement les baselines pour eviter les vrais appels HTTP.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static void setBaselines(UpdateCheckService svc, Map<String, String> baselines) {
|
|
||||||
((Map<String, String>) ReflectionTestUtils.getField(svc, "baselineDigests")).putAll(baselines);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RestTemplate stubHttp(UpdateCheckService svc) {
|
|
||||||
RestTemplate http = mock(RestTemplate.class);
|
|
||||||
ReflectionTestUtils.setField(svc, "http", http);
|
|
||||||
return http;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void stubRemoteDigest(RestTemplate http, String image, String digest) {
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
if (digest != null) headers.add("Docker-Content-Digest", digest);
|
|
||||||
ResponseEntity<Void> resp = new ResponseEntity<>(headers, org.springframework.http.HttpStatus.OK);
|
|
||||||
when(http.exchange(eq("https://ghcr.io/v2/" + image + "/manifests/latest"),
|
|
||||||
eq(org.springframework.http.HttpMethod.HEAD), any(), eq(Void.class)))
|
|
||||||
.thenReturn(resp);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void stubRemoteFailure(RestTemplate http, String image) {
|
|
||||||
when(http.exchange(eq("https://ghcr.io/v2/" + image + "/manifests/latest"),
|
|
||||||
eq(org.springframework.http.HttpMethod.HEAD), any(), eq(Void.class)))
|
|
||||||
.thenThrow(new RuntimeException("network down"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void disabledWhenTokenMissing() {
|
|
||||||
UpdateCheckService svc = newService("");
|
|
||||||
UpdateStatus status = svc.check();
|
|
||||||
assertFalse(status.enabled());
|
|
||||||
assertFalse(status.updateAvailable());
|
|
||||||
assertFalse(status.anyUnknown());
|
|
||||||
assertTrue(status.images().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void upToDate_whenBaselineEqualsRemote() {
|
|
||||||
UpdateCheckService svc = newService("token");
|
|
||||||
ReflectionTestUtils.setField(svc, "baselineDigests", new ConcurrentHashMap<>());
|
|
||||||
setBaselines(svc, Map.of(
|
|
||||||
"igmlcreation/loremind-core", "sha256:aaa",
|
|
||||||
"igmlcreation/loremind-brain", "sha256:bbb"
|
|
||||||
));
|
|
||||||
RestTemplate http = stubHttp(svc);
|
|
||||||
stubRemoteDigest(http, "igmlcreation/loremind-core", "sha256:aaa");
|
|
||||||
stubRemoteDigest(http, "igmlcreation/loremind-brain", "sha256:bbb");
|
|
||||||
|
|
||||||
UpdateStatus status = svc.check();
|
|
||||||
|
|
||||||
assertTrue(status.enabled());
|
|
||||||
assertFalse(status.updateAvailable());
|
|
||||||
assertFalse(status.anyUnknown());
|
|
||||||
for (ImageStatus img : status.images()) {
|
|
||||||
assertEquals(ImageStatusKind.UP_TO_DATE, img.status());
|
|
||||||
assertFalse(img.updateAvailable(), "back-compat bool");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void updateAvailable_whenRemoteDiffers() {
|
|
||||||
UpdateCheckService svc = newService("token");
|
|
||||||
ReflectionTestUtils.setField(svc, "baselineDigests", new ConcurrentHashMap<>());
|
|
||||||
setBaselines(svc, Map.of(
|
|
||||||
"igmlcreation/loremind-core", "sha256:OLD",
|
|
||||||
"igmlcreation/loremind-brain", "sha256:bbb"
|
|
||||||
));
|
|
||||||
RestTemplate http = stubHttp(svc);
|
|
||||||
stubRemoteDigest(http, "igmlcreation/loremind-core", "sha256:NEW");
|
|
||||||
stubRemoteDigest(http, "igmlcreation/loremind-brain", "sha256:bbb");
|
|
||||||
|
|
||||||
UpdateStatus status = svc.check();
|
|
||||||
|
|
||||||
assertTrue(status.updateAvailable());
|
|
||||||
assertFalse(status.anyUnknown());
|
|
||||||
ImageStatus core = status.images().stream()
|
|
||||||
.filter(i -> i.image().endsWith("core")).findFirst().orElseThrow();
|
|
||||||
assertEquals(ImageStatusKind.UPDATE_AVAILABLE, core.status());
|
|
||||||
assertTrue(core.updateAvailable(), "back-compat bool");
|
|
||||||
ImageStatus brain = status.images().stream()
|
|
||||||
.filter(i -> i.image().endsWith("brain")).findFirst().orElseThrow();
|
|
||||||
assertEquals(ImageStatusKind.UP_TO_DATE, brain.status());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void unknown_whenBaselineMissing_DOES_NOT_lazyAlign() {
|
|
||||||
// INVARIANT CENTRAL : si la baseline est absente (echec init au boot),
|
|
||||||
// on NE DOIT PAS aligner lazy sur le remote courant — sinon une MAJ
|
|
||||||
// pousse APRES le boot serait declaree "a jour" silencieusement.
|
|
||||||
UpdateCheckService svc = newService("token");
|
|
||||||
ReflectionTestUtils.setField(svc, "baselineDigests", new ConcurrentHashMap<>());
|
|
||||||
// baseline DELIBEREMENT vide
|
|
||||||
RestTemplate http = stubHttp(svc);
|
|
||||||
stubRemoteDigest(http, "igmlcreation/loremind-core", "sha256:remote-now");
|
|
||||||
stubRemoteDigest(http, "igmlcreation/loremind-brain", "sha256:remote-now-2");
|
|
||||||
|
|
||||||
UpdateStatus status = svc.check();
|
|
||||||
|
|
||||||
assertTrue(status.enabled());
|
|
||||||
assertFalse(status.updateAvailable());
|
|
||||||
assertTrue(status.anyUnknown());
|
|
||||||
for (ImageStatus img : status.images()) {
|
|
||||||
assertEquals(ImageStatusKind.UNKNOWN, img.status());
|
|
||||||
assertNull(img.localDigest());
|
|
||||||
assertNotNull(img.remoteDigest()); // remote OK, baseline manquante
|
|
||||||
}
|
|
||||||
|
|
||||||
// VERIFICATION CRITIQUE : la baseline ne doit PAS avoir ete posee.
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Map<String, String> baselines = (Map<String, String>) ReflectionTestUtils.getField(svc, "baselineDigests");
|
|
||||||
assertTrue(baselines.isEmpty(),
|
|
||||||
"check() ne doit JAMAIS aligner lazy la baseline sur le remote — "
|
|
||||||
+ "regression de bug historique (faux negatif silencieux).");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void unknown_whenRemoteFetchFails() {
|
|
||||||
UpdateCheckService svc = newService("token");
|
|
||||||
ReflectionTestUtils.setField(svc, "baselineDigests", new ConcurrentHashMap<>());
|
|
||||||
setBaselines(svc, Map.of("igmlcreation/loremind-core", "sha256:aaa",
|
|
||||||
"igmlcreation/loremind-brain", "sha256:bbb"));
|
|
||||||
RestTemplate http = stubHttp(svc);
|
|
||||||
stubRemoteFailure(http, "igmlcreation/loremind-core");
|
|
||||||
stubRemoteDigest(http, "igmlcreation/loremind-brain", "sha256:bbb");
|
|
||||||
|
|
||||||
UpdateStatus status = svc.check();
|
|
||||||
|
|
||||||
assertFalse(status.updateAvailable());
|
|
||||||
assertTrue(status.anyUnknown());
|
|
||||||
ImageStatus core = status.images().stream()
|
|
||||||
.filter(i -> i.image().endsWith("core")).findFirst().orElseThrow();
|
|
||||||
assertEquals(ImageStatusKind.UNKNOWN, core.status());
|
|
||||||
assertNull(core.remoteDigest());
|
|
||||||
assertEquals("sha256:aaa", core.localDigest()); // baseline preservee
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void mixedStatuses_anyUnknownAndAnyUpdateBothTrue() {
|
|
||||||
UpdateCheckService svc = newService("token");
|
|
||||||
ReflectionTestUtils.setField(svc, "baselineDigests", new ConcurrentHashMap<>());
|
|
||||||
setBaselines(svc, Map.of("igmlcreation/loremind-core", "sha256:OLD"));
|
|
||||||
// brain n'a pas de baseline -> UNKNOWN
|
|
||||||
RestTemplate http = stubHttp(svc);
|
|
||||||
stubRemoteDigest(http, "igmlcreation/loremind-core", "sha256:NEW");
|
|
||||||
stubRemoteFailure(http, "igmlcreation/loremind-brain");
|
|
||||||
|
|
||||||
UpdateStatus status = svc.check();
|
|
||||||
|
|
||||||
assertTrue(status.updateAvailable(), "core a une MAJ disponible");
|
|
||||||
assertTrue(status.anyUnknown(), "brain est UNKNOWN");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,8 +7,8 @@
|
|||||||
# Surchargables via variables d'environnement (CI) :
|
# Surchargables via variables d'environnement (CI) :
|
||||||
# SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD
|
# SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD
|
||||||
spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/loremind_test}
|
spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/loremind_test}
|
||||||
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:loremind_test}
|
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:ietm64}
|
||||||
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:loremind_test}
|
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:M&Ipourlavie64}
|
||||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||||
|
|
||||||
# Configuration JPA pour les tests : schema recree a chaque run, pas de logs SQL.
|
# Configuration JPA pour les tests : schema recree a chaque run, pas de logs SQL.
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
Auteur : ietm64
|
Auteur : ietm64
|
||||||
Licence : AGPL-3.0
|
Licence : AGPL-3.0
|
||||||
Projet : LoreMindMJ - assistant pour Maitres de Jeu de JDR
|
Projet : LoreMindMJ - assistant pour Maitres de Jeu de JDR
|
||||||
Version : 0.7.1
|
Version : 0.7.0
|
||||||
|
|
||||||
.LINK
|
.LINK
|
||||||
https://github.com/IGMLcreation/LoreMind
|
https://github.com/IGMLcreation/LoreMind
|
||||||
|
|||||||
2
web/package-lock.json
generated
2
web/package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.7.1",
|
"version": "0.7.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "loremind-web",
|
"name": "loremind-web",
|
||||||
"version": "0.7.1",
|
"version": "0.7.0",
|
||||||
"description": "LoreMind Frontend - Angular",
|
"description": "LoreMind Frontend - Angular",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
|
|||||||
@@ -4,29 +4,17 @@ import { BehaviorSubject, Observable, catchError, of, tap } from 'rxjs';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reflet de UpdateCheckService.UpdateStatus cote backend.
|
* Reflet de UpdateCheckService.UpdateStatus cote backend.
|
||||||
*
|
|
||||||
* Etat tri-state par image : UP_TO_DATE / UPDATE_AVAILABLE / UNKNOWN.
|
|
||||||
* UNKNOWN signale que la comparaison est impossible (baseline absente ou
|
|
||||||
* remote injoignable) — l'UI doit afficher un avertissement plutot que
|
|
||||||
* d'annoncer "a jour" silencieusement.
|
|
||||||
*/
|
*/
|
||||||
export type ImageStatusKind = 'UP_TO_DATE' | 'UPDATE_AVAILABLE' | 'UNKNOWN';
|
|
||||||
|
|
||||||
export interface ImageStatus {
|
export interface ImageStatus {
|
||||||
image: string;
|
image: string;
|
||||||
localDigest: string | null;
|
localDigest: string | null;
|
||||||
remoteDigest: string | null;
|
remoteDigest: string | null;
|
||||||
status: ImageStatusKind;
|
|
||||||
/** Conserve pour back-compat ; equivalent a (status === 'UPDATE_AVAILABLE'). */
|
|
||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateStatus {
|
export interface UpdateStatus {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/** True si au moins une image a status === 'UPDATE_AVAILABLE'. */
|
|
||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
/** True si au moins une image a status === 'UNKNOWN'. */
|
|
||||||
anyUnknown: boolean;
|
|
||||||
images: ImageStatus[];
|
images: ImageStatus[];
|
||||||
checkedAt: string;
|
checkedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,21 +244,16 @@
|
|||||||
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
||||||
<span>Une mise a jour est disponible.</span>
|
<span>Une mise a jour est disponible.</span>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="updateStatus?.anyUnknown && !updateStatus?.updateAvailable" class="alert alert-warn">
|
<div *ngIf="!updateStatus?.updateAvailable" class="hint">
|
||||||
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
|
||||||
<span>Verification impossible pour certaines images — voir details ci-dessous.</span>
|
|
||||||
</div>
|
|
||||||
<div *ngIf="!updateStatus?.updateAvailable && !updateStatus?.anyUnknown" class="hint">
|
|
||||||
Tout est a jour (verifie le {{ updateStatus?.checkedAt | date:'short' }}).
|
Tout est a jour (verifie le {{ updateStatus?.checkedAt | date:'short' }}).
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="update-images" *ngIf="updateStatus?.images?.length">
|
<ul class="update-images" *ngIf="updateStatus?.images?.length">
|
||||||
<li *ngFor="let img of updateStatus?.images">
|
<li *ngFor="let img of updateStatus?.images">
|
||||||
<strong>{{ img.image }}</strong>
|
<strong>{{ img.image }}</strong>
|
||||||
<span *ngIf="img.status === 'UPDATE_AVAILABLE'" class="badge-update">MAJ dispo</span>
|
<span *ngIf="img.updateAvailable" class="badge-update">MAJ dispo</span>
|
||||||
<span *ngIf="img.status === 'UP_TO_DATE'" class="badge-ok">a jour</span>
|
<span *ngIf="!img.updateAvailable && img.remoteDigest" class="badge-ok">a jour</span>
|
||||||
<span *ngIf="img.status === 'UNKNOWN'" class="badge-warn"
|
<span *ngIf="!img.remoteDigest" class="badge-warn">indisponible</span>
|
||||||
title="Impossible de comparer (baseline absente ou registry injoignable)">verification impossible</span>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
@@ -303,7 +303,6 @@
|
|||||||
}
|
}
|
||||||
.alert-error { background: rgba(220, 80, 80, 0.15); color: #ff9b9b; }
|
.alert-error { background: rgba(220, 80, 80, 0.15); color: #ff9b9b; }
|
||||||
.alert-success { background: rgba(80, 200, 120, 0.15); color: #a2e8b6; }
|
.alert-success { background: rgba(80, 200, 120, 0.15); color: #a2e8b6; }
|
||||||
.alert-warn { background: rgba(245, 158, 11, 0.15); color: #fbbf24; }
|
|
||||||
|
|
||||||
/* --- Slider fenetre de contexte -------------------------------------- */
|
/* --- Slider fenetre de contexte -------------------------------------- */
|
||||||
.ctx-value {
|
.ctx-value {
|
||||||
|
|||||||
Reference in New Issue
Block a user