mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
La vérification du rendu du gabarit après une mise à jour attend jusqu'à vingt minutes que le nouveau moteur réponde. Un retour à la version précédente, une autre version installée ou une seconde mise à jour pendant cette attente la laissaient courir : elle comparait alors l'ancien moteur à celui d'une autre bascule, ou écrasait le relevé de la suivante. - Chaque bascule ouvre une génération ; une vérification périmée s'arrête sans rien ranger, et son relevé n'écrase plus le suivant. Retour et « utiliser » effacent le relevé, qui parlerait d'un moteur arrêté. - /api/engine/plan (registre) et /api/engine/rollback (configuration et redémarrage) refusent tout autre verbe que POST — l'interface n'utilise que POST. - Tests : vérification annulée en attente, relevé périmé ignoré, GET refusé. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
529 lines
20 KiB
Go
529 lines
20 KiB
Go
// web_engine_reco.go — « version recommandée » du panneau Moteur, retour à la
|
|
// version précédente, et garde du rendu des gabarits à travers une mise à jour.
|
|
//
|
|
// POST /api/engine/plan → choisit la version à installer (sur clic) et
|
|
// dit si le prompt rendu va changer
|
|
// POST /api/engine/rollback → revient au moteur d'avant la dernière bascule
|
|
//
|
|
// RIEN D'AUTOMATIQUE. Aucune de ces routes n'est appelée au démarrage ni par
|
|
// l'état du panneau : le registre n'est interrogé que sur un clic, et rien ne
|
|
// s'installe sans la confirmation qui suit. L'encart « version recommandée »
|
|
// lui-même se décide sans réseau, sur le build du moteur courant.
|
|
package loki
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// enginePreserveDefaultBuild : premier build où llama-server active
|
|
// preserve_reasoning quand ni --reasoning-preserve ni
|
|
// --no-reasoning-preserve n'est passé (PR #28174).
|
|
enginePreserveDefaultBuild = 10763
|
|
// engineQwen4MTPBuild : premier build qui sait la tête MTP de qwen4exp
|
|
// (PR #29761). Cité dans l'encart, jamais activé par Loki : SPEC reste off
|
|
// tant que l'utilisateur ne le pose pas.
|
|
engineQwen4MTPBuild = 11331
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Choix de la version recommandée
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// engineRecommendedTag : le tag figé le plus récent publié pour la variante,
|
|
// à condition qu'il atteigne engineMinRecommended et qu'il existe vraiment.
|
|
//
|
|
// Le registre n'offre pas de « plus récent au-dessus de N » ; lister ses
|
|
// milliers de tags pour le trouver serait lent et fragile. Le plus récent
|
|
// au-dessus du minimum, c'est le dernier publié — l'annotation du tag mouvant
|
|
// le nomme (engineOCILatest) — et on vérifie que son tag figé répond avant de
|
|
// le proposer : c'est lui qu'on installera, pas le tag mouvant, pour que la
|
|
// version confirmée soit la version installée.
|
|
func engineRecommendedTag(variant string) (int, string, error) {
|
|
build, tag, err := engineOCILatest(variant)
|
|
if err != nil {
|
|
return 0, "", fmt.Errorf("registre injoignable, aucune version choisie : %w", err)
|
|
}
|
|
if build == 0 {
|
|
// Sans numéro, impossible de promettre « au moins b10864 » : on ne
|
|
// propose pas un tag mouvant dont on ne sait pas ce qu'il contient.
|
|
return 0, "", fmt.Errorf("le registre n'annonce pas le build du tag « %s » : impossible de vérifier qu'il atteint b%d", variant, engineMinRecommended)
|
|
}
|
|
if build < engineMinRecommended {
|
|
return 0, "", fmt.Errorf("dernier build publié pour %s : b%d, antérieur à la version recommandée b%d", variant, build, engineMinRecommended)
|
|
}
|
|
if _, _, err := ociResolve(ociEngineRepo, tag); err != nil {
|
|
return 0, "", fmt.Errorf("%s annoncé mais introuvable sur le registre : %w", tag, err)
|
|
}
|
|
return build, tag, nil
|
|
}
|
|
|
|
// engineRecommendation : l'encart du panneau, ou nil. Sans réseau : seulement
|
|
// le build de confiance du moteur courant (engineBuildTrust — jamais pour un
|
|
// compilé ou un fork), comparé au minimum recommandé.
|
|
func engineRecommendation(trusted int, supported bool) map[string]any {
|
|
if trusted <= 0 || trusted >= engineMinRecommended || !supported {
|
|
return nil
|
|
}
|
|
return map[string]any{
|
|
"current": trusted,
|
|
"min": engineMinRecommended,
|
|
"gains": []string{
|
|
fmt.Sprintf("points de reprise des modèles hybrides (Qwen3.5/3.6, Qwen3-Next…) gardés : plus de ~8 k jetons recalculés à chaque reprise (b%d)", engineMinRecommended),
|
|
fmt.Sprintf("MTP rapide (graphes CUDA) à partir de b%d", specAutoMinBuild),
|
|
fmt.Sprintf("MTP de qwen4exp à partir de b%d", engineQwen4MTPBuild),
|
|
},
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Rendu du raisonnement : le défaut qui change à b10763
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// preserveIn : ce qu'il faut savoir pour dire si la bascule change le prompt.
|
|
type preserveIn struct {
|
|
External bool // preset externe : le gabarit n'est pas rendu par ce moteur
|
|
Current int // build de confiance du moteur courant (0 = inconnu)
|
|
Target int // build visé (0 = inconnu)
|
|
Key string // REASONING_PRESERVE de la configuration active
|
|
Overridden string // EXTRA_ARGS ou LLAMA_ARG_* qui règlent déjà la question
|
|
Template string // gabarit chargé (/props)
|
|
TplKnown bool
|
|
Preserves tplTri // la sonde, sur le moteur courant
|
|
}
|
|
|
|
// preserveRisk : le verdict, pour l'avertissement et la case à cocher.
|
|
type preserveRisk struct {
|
|
Risk string `json:"risk"` // "yes", "no" ou "unknown"
|
|
Why string `json:"why"`
|
|
// Suggest : cocher d'office REASONING_PRESERVE=off. Seulement quand le
|
|
// changement est établi : sur un gabarit qui garde de lui-même la réflexion
|
|
// (Qwen3.8), « off » changerait à son tour le rendu.
|
|
Suggest bool `json:"suggest"`
|
|
}
|
|
|
|
// tplPreserveVars : les variables que llama-server pose dans le contexte du
|
|
// gabarit avec preserve_reasoning (caps_apply_preserve_reasoning). Un gabarit
|
|
// qui n'en lit aucune rend la même chose sur les deux moteurs.
|
|
var tplPreserveVars = []string{"preserve_thinking", "clear_thinking", "truncate_history_thinking", "preserve_reasoning"}
|
|
|
|
// reasoningPreserveRisk : la bascule vers le build visé change-t-elle le prompt
|
|
// rendu ? Fonction pure.
|
|
func reasoningPreserveRisk(in preserveIn) preserveRisk {
|
|
no := func(why string) preserveRisk { return preserveRisk{Risk: "no", Why: why} }
|
|
switch {
|
|
case in.External:
|
|
return no("preset externe : son gabarit n'est pas rendu par ce moteur")
|
|
case in.Target > 0 && in.Target < enginePreserveDefaultBuild:
|
|
return no(fmt.Sprintf("b%d n'a pas encore preserve_reasoning par défaut (b%d)", in.Target, enginePreserveDefaultBuild))
|
|
case in.Current >= enginePreserveDefaultBuild:
|
|
return no(fmt.Sprintf("le moteur courant (b%d) a déjà preserve_reasoning par défaut", in.Current))
|
|
case in.Key != "":
|
|
return no("REASONING_PRESERVE=" + in.Key + " est posée : le rendu ne dépend pas du défaut du moteur")
|
|
case in.Overridden != "":
|
|
return no(in.Overridden + " règle déjà preserve_reasoning")
|
|
case !in.TplKnown:
|
|
return preserveRisk{Risk: "unknown", Why: "moteur arrêté ou gabarit illisible : impossible de vérifier. " +
|
|
"À partir de b" + fmt.Sprint(enginePreserveDefaultBuild) + ", un gabarit de type Qwen3.6 rend la réflexion des tours passés, " +
|
|
"vide puisque Loki ne la renvoie pas (sans REASONING_ECHO) ; REASONING_PRESERVE=off garde le rendu actuel. " +
|
|
"À l'inverse, sur un gabarit qui la garde déjà (Qwen3.8), off changerait le rendu."}
|
|
}
|
|
tpl := false
|
|
for _, v := range tplPreserveVars {
|
|
if strings.Contains(in.Template, v) {
|
|
tpl = true
|
|
break
|
|
}
|
|
}
|
|
switch {
|
|
case !tpl:
|
|
return no("le gabarit chargé ne lit pas preserve_thinking / clear_thinking : rendu inchangé")
|
|
case in.Preserves == tplYes:
|
|
return no("le gabarit garde déjà la réflexion des tours passés : rendu inchangé (ne pas poser off)")
|
|
case in.Preserves == tplNo:
|
|
return preserveRisk{Risk: "yes", Suggest: true, Why: fmt.Sprintf("le gabarit chargé retire la réflexion des tours passés ; "+
|
|
"à partir de b%d le moteur la lui fait garder, et sans REASONING_ECHO Loki ne la renvoie pas : chaque tour passé "+
|
|
"serait rendu avec un bloc de réflexion vide. REASONING_PRESERVE=off garde le rendu actuel.", enginePreserveDefaultBuild)}
|
|
}
|
|
return preserveRisk{Risk: "unknown", Why: "le gabarit lit preserve_thinking mais la sonde n'a pas conclu : " +
|
|
"REASONING_PRESERVE=off garde le rendu actuel s'il retire la réflexion des tours passés"}
|
|
}
|
|
|
|
// preserveOverridden : EXTRA_ARGS ou variable d'environnement qui fixent déjà
|
|
// preserve_reasoning (mêmes gardes que reasoningPreserveArgs). Vide = aucune.
|
|
func preserveOverridden(cfg map[string]string) string {
|
|
extra := splitArgs(cfg["EXTRA_ARGS"])
|
|
switch {
|
|
case hasAnyFlag(extra, "--reasoning-preserve", "--no-reasoning-preserve"):
|
|
return "EXTRA_ARGS"
|
|
case strings.Contains(strings.Join(flagValues(extra, "--chat-template-kwargs"), " "), "preserve_reasoning"):
|
|
return "--chat-template-kwargs"
|
|
case strings.TrimSpace(os.Getenv("LLAMA_ARG_REASONING_PRESERVE")) != "":
|
|
return "LLAMA_ARG_REASONING_PRESERVE"
|
|
case strings.Contains(os.Getenv("LLAMA_ARG_CHAT_TEMPLATE_KWARGS"), "preserve_reasoning"):
|
|
return "LLAMA_ARG_CHAT_TEMPLATE_KWARGS"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// enginePreserveInputs : les lectures de reasoningPreserveRisk. Le moteur
|
|
// courant n'est interrogé qu'en lecture (/health, /props, /apply-template) :
|
|
// rien n'y touche au cache de la conversation.
|
|
func enginePreserveInputs(ctx context.Context, target int) preserveIn {
|
|
cfg := ReadConfig()
|
|
in := preserveIn{
|
|
External: isExternalConfig(cfg),
|
|
Current: engineBuildTrusted(currentEngineBin()),
|
|
Target: target,
|
|
Key: strings.TrimSpace(cfg["REASONING_PRESERVE"]),
|
|
Overridden: preserveOverridden(cfg),
|
|
Preserves: tplUnknown,
|
|
}
|
|
if in.External || in.Key != "" || in.Overridden != "" || (target > 0 && target < enginePreserveDefaultBuild) {
|
|
return in // verdict déjà acquis : pas la peine d'interroger le moteur
|
|
}
|
|
p := tplLocalProber()
|
|
if !p.healthy(ctx) {
|
|
return in
|
|
}
|
|
props, err := p.props(ctx)
|
|
if err != nil {
|
|
return in
|
|
}
|
|
in.Template, in.TplKnown = props.ChatTemplate, props.ChatTemplate != ""
|
|
in.Preserves = tplProbeEnsure(ctx, tplLastShape()).PreservesHistory
|
|
return in
|
|
}
|
|
|
|
// engineHelpHas : l'aide du binaire mentionne-t-elle ce drapeau ? Lancé avec
|
|
// ses bibliothèques à côté, comme le test à blanc : un moteur téléchargé n'a
|
|
// pas encore servi, binHelp ne le connaît pas.
|
|
func engineHelpHas(bin, flag string) bool {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
cmd := hideCmd(exec.CommandContext(ctx, bin, "--help"))
|
|
cmd.WaitDelay = 2 * time.Second
|
|
cmd.Env = libraryPathEnv(filepath.Dir(bin))
|
|
out, _ := cmd.CombinedOutput()
|
|
return strings.Contains(string(out), flag)
|
|
}
|
|
|
|
// enginePreserveOff pose REASONING_PRESERVE=off, choisi par l'utilisateur
|
|
// avant la bascule. Dans le preset actif s'il y en a un — sinon la prochaine
|
|
// bascule de preset l'effacerait sans un mot —, dans la configuration sinon.
|
|
// Rien si la clé est déjà posée : c'est un choix antérieur, on ne le contredit
|
|
// pas.
|
|
func enginePreserveOff(newBin string) (string, error) {
|
|
return enginePreserveOffIf(func() bool { return engineHelpHas(newBin, "--no-reasoning-preserve") })
|
|
}
|
|
|
|
// enginePreserveOffIf : le corps d'enginePreserveOff, l'aide du nouveau moteur
|
|
// passée en fonction (les tests n'ont pas de llama-server à lancer).
|
|
func enginePreserveOffIf(knowsFlag func() bool) (string, error) {
|
|
if v := strings.TrimSpace(ReadConfig()["REASONING_PRESERVE"]); v != "" {
|
|
return "REASONING_PRESERVE=" + v + " déjà posée : inchangée", nil
|
|
}
|
|
if !knowsFlag() {
|
|
return "", fmt.Errorf("le nouveau moteur ne connaît pas --no-reasoning-preserve")
|
|
}
|
|
if err := tuneGuard(); err != nil {
|
|
return "", err
|
|
}
|
|
if p, content, err := tuneActivePreset(); err == nil {
|
|
if _, _, err := SavePresetApplying(p.ID, p.Name, presetSetKey(content, "REASONING_PRESERVE", "off")); err != nil {
|
|
return "", err
|
|
}
|
|
return "REASONING_PRESERVE=off posée dans le preset « " + p.Name + " »", nil
|
|
}
|
|
if err := SetConfigKey("REASONING_PRESERVE", "off"); err != nil {
|
|
return "", err
|
|
}
|
|
return "REASONING_PRESERVE=off posée dans la configuration active (aucun preset ne lui correspond)", nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Vérification du rendu après la bascule
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// engineRenderCheck : le dernier relevé avant/après une bascule de moteur.
|
|
type engineRenderCheck struct {
|
|
Status string `json:"status"` // pending, same, changed, unknown
|
|
Tag string `json:"tag"`
|
|
Detail string `json:"detail,omitempty"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
var (
|
|
engineRenderMu sync.Mutex
|
|
engineRenderLast *engineRenderCheck
|
|
// engineRenderGen : la vérification en cours. Une bascule plus récente
|
|
// (autre mise à jour, retour à la version précédente, autre version
|
|
// installée) la périme : une vérification d'avant, encore à attendre son
|
|
// moteur, ne doit ni écraser le relevé suivant ni comparer l'ancien
|
|
// moteur au moteur d'une autre bascule.
|
|
engineRenderGen uint64
|
|
// engineRenderWait : délai laissé au nouveau moteur pour charger le modèle
|
|
// (un gros MoE en mmap peut demander plusieurs minutes). Variable pour les
|
|
// tests.
|
|
engineRenderWait = 20 * time.Minute
|
|
engineRenderPoll = 5 * time.Second
|
|
)
|
|
|
|
// engineRenderBegin ouvre une vérification : celles d'avant sont périmées.
|
|
func engineRenderBegin() uint64 {
|
|
engineRenderMu.Lock()
|
|
defer engineRenderMu.Unlock()
|
|
engineRenderGen++
|
|
return engineRenderGen
|
|
}
|
|
|
|
// engineRenderCancel : une bascule sans vérification (retour à la version
|
|
// précédente, autre version installée) périme la vérification en cours et
|
|
// efface son relevé, qui parlerait d'un moteur qui ne tourne plus.
|
|
func engineRenderCancel() {
|
|
engineRenderMu.Lock()
|
|
defer engineRenderMu.Unlock()
|
|
engineRenderGen++
|
|
engineRenderLast = nil
|
|
}
|
|
|
|
// engineRenderCurrent : la vérification gen est-elle encore la dernière ?
|
|
func engineRenderCurrent(gen uint64) bool {
|
|
engineRenderMu.Lock()
|
|
defer engineRenderMu.Unlock()
|
|
return gen == engineRenderGen
|
|
}
|
|
|
|
// engineRenderSet range le relevé de la vérification gen — rien si une
|
|
// bascule plus récente l'a périmée.
|
|
func engineRenderSet(gen uint64, c engineRenderCheck) {
|
|
c.At = time.Now()
|
|
engineRenderMu.Lock()
|
|
if gen != engineRenderGen {
|
|
engineRenderMu.Unlock()
|
|
return
|
|
}
|
|
engineRenderLast = &c
|
|
engineRenderMu.Unlock()
|
|
if c.Status != "pending" {
|
|
line := "[moteur] rendu du gabarit après " + c.Tag + " : " + c.Status
|
|
if c.Detail != "" {
|
|
line += " — " + c.Detail
|
|
}
|
|
fmt.Fprintln(os.Stderr, line)
|
|
}
|
|
}
|
|
|
|
func engineRenderSnapshot() *engineRenderCheck {
|
|
engineRenderMu.Lock()
|
|
defer engineRenderMu.Unlock()
|
|
if engineRenderLast == nil {
|
|
return nil
|
|
}
|
|
c := *engineRenderLast
|
|
return &c
|
|
}
|
|
|
|
// engineRenderRef : le rendu relevé sur le moteur qui tourne encore, la forme
|
|
// rejouée et le build qu'il annonce (/props).
|
|
type engineRenderRef struct {
|
|
text string
|
|
shape tplShape
|
|
build string
|
|
}
|
|
|
|
// engineRenderBefore relève le rendu sur le moteur qui tourne encore. ok=false
|
|
// : moteur arrêté, preset externe ou rendu impossible — pas de comparaison.
|
|
func engineRenderBefore(ctx context.Context) (engineRenderRef, bool) {
|
|
ref := engineRenderRef{shape: tplLastShape()}
|
|
if externalActive() {
|
|
return ref, false
|
|
}
|
|
p := tplLocalProber()
|
|
if !p.healthy(ctx) {
|
|
return ref, false
|
|
}
|
|
if props, err := p.props(ctx); err == nil {
|
|
ref.build = props.BuildInfo
|
|
}
|
|
fp, err := tplRenderPrint(ctx, p, ref.shape)
|
|
ref.text = fp
|
|
return ref, err == nil
|
|
}
|
|
|
|
// engineRenderAfter attend le nouveau moteur (modèle chargé) et compare. En
|
|
// tâche de fond : le job de mise à jour est fini, la bascule faite ; ceci ne
|
|
// fait que signaler.
|
|
//
|
|
// « Prêt » veut dire /health à 200 ET un build différent de celui relevé
|
|
// avant : un ancien moteur qui aurait survécu à l'arrêt répondrait aussi, et
|
|
// comparer l'ancien à lui-même conclurait à tort « identique ».
|
|
func engineRenderAfter(ref engineRenderRef, tag string) {
|
|
gen := engineRenderBegin()
|
|
engineRenderSet(gen, engineRenderCheck{Status: "pending", Tag: tag})
|
|
ctx, cancel := context.WithTimeout(context.Background(), engineRenderWait)
|
|
defer cancel()
|
|
p := tplLocalProber()
|
|
ready := func() bool {
|
|
if !p.healthy(ctx) {
|
|
return false
|
|
}
|
|
if ref.build == "" {
|
|
return true
|
|
}
|
|
props, err := p.props(ctx)
|
|
return err == nil && props.BuildInfo != ref.build
|
|
}
|
|
for !ready() {
|
|
if !engineRenderCurrent(gen) {
|
|
return // bascule plus récente : plus rien à comparer
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
engineRenderSet(gen, engineRenderCheck{Status: "unknown", Tag: tag, Detail: "le nouveau moteur n'a pas répondu à temps : rendu non vérifié"})
|
|
return
|
|
case <-time.After(engineRenderPoll):
|
|
}
|
|
}
|
|
after, err := tplRenderPrint(ctx, p, ref.shape)
|
|
if err != nil {
|
|
engineRenderSet(gen, engineRenderCheck{Status: "unknown", Tag: tag, Detail: "rendu impossible sur le nouveau moteur : " + err.Error()})
|
|
return
|
|
}
|
|
if same, d := tplRenderDiff(ref.text, after); same {
|
|
engineRenderSet(gen, engineRenderCheck{Status: "same", Tag: tag})
|
|
} else {
|
|
engineRenderSet(gen, engineRenderCheck{Status: "changed", Tag: tag, Detail: d})
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Version précédente
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// enginePrevFile : le moteur d'avant la dernière bascule faite par le panneau.
|
|
// Un fichier à part, pas une clé de configuration : un preset n'a rien à en
|
|
// dire, et il ne doit pas changer l'empreinte de la configuration active.
|
|
func enginePrevFile() string { return filepath.Join(engineDir(), "previous-bin") }
|
|
|
|
// engineRememberPrevious note le moteur qu'on quitte. Best-effort : sans ce
|
|
// fichier, le bouton « revenir » n'apparaît simplement pas.
|
|
func engineRememberPrevious(left, next string) {
|
|
if left == "" || samePath(left, next) {
|
|
return
|
|
}
|
|
_ = os.MkdirAll(engineDir(), 0o755)
|
|
_ = os.WriteFile(enginePrevFile(), []byte(left+"\n"), 0o644)
|
|
}
|
|
|
|
// enginePrevious : le moteur précédent s'il est encore là et n'est pas celui
|
|
// qui tourne. Le ménage d'après mise à jour le garde (engineKeepAfterUpdate) ;
|
|
// une suppression à la main le fait disparaître, et le bouton avec.
|
|
func enginePrevious(cur string) map[string]any {
|
|
b, err := os.ReadFile(enginePrevFile())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
bin := strings.TrimSpace(string(b))
|
|
if bin == "" || !isFile(bin) || samePath(bin, cur) {
|
|
return nil
|
|
}
|
|
out := map[string]any{"bin": bin, "source": engineSource(bin)}
|
|
if tag := engineTagOf(bin); tag != "" {
|
|
out["tag"] = tag
|
|
out["build"] = engineTagBuild(tag)
|
|
} else {
|
|
out["build"] = engineBuildCached(bin)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// handleEnginePlan : sur clic, la version qu'on installerait et ce qu'elle
|
|
// changerait au prompt. {recommended:true} : la plus récente ≥ b10864
|
|
// réellement publiée ; sinon la dernière publiée. Rien n'est téléchargé.
|
|
func handleEnginePlan(w http.ResponseWriter, r *http.Request) {
|
|
// Interroge le registre : POST seulement, comme le clic qui l'appelle.
|
|
if r.Method != http.MethodPost {
|
|
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"ok": false, "error": "POST requis"})
|
|
return
|
|
}
|
|
if !engineOCISupported() {
|
|
sendJSON(w, 200, map[string]any{"ok": false, "error": engineUnsupportedWhy()})
|
|
return
|
|
}
|
|
var req struct {
|
|
Recommended bool `json:"recommended"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&req)
|
|
variant := engineVariant()
|
|
var (
|
|
build int
|
|
tag string
|
|
err error
|
|
)
|
|
if req.Recommended {
|
|
build, tag, err = engineRecommendedTag(variant)
|
|
} else {
|
|
build, tag, err = engineOCILatest(variant)
|
|
}
|
|
if err != nil {
|
|
sendJSON(w, 200, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
cur := currentEngineBin()
|
|
curBuild, _ := engineBuildOf(cur)
|
|
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
|
|
defer cancel()
|
|
sendJSON(w, 200, map[string]any{
|
|
"ok": true,
|
|
"tag": tag,
|
|
"build": build,
|
|
"current": curBuild,
|
|
"uptodate": build > 0 && curBuild == build,
|
|
"preserve": reasoningPreserveRisk(enginePreserveInputs(ctx, build)),
|
|
// Ce qui restera pour revenir en arrière : la version téléchargée qui
|
|
// tourne (gardée par le ménage), ou le moteur de l'image.
|
|
"keeps": engineTagOf(cur),
|
|
})
|
|
}
|
|
|
|
// handleEngineRollback revient au moteur d'avant la dernière bascule. Aucun
|
|
// réseau. Le moteur quitté devient à son tour « le précédent » : deux clics
|
|
// ramènent où l'on était.
|
|
func handleEngineRollback(w http.ResponseWriter, r *http.Request) {
|
|
// Change la configuration et redémarre le moteur : POST seulement.
|
|
if r.Method != http.MethodPost {
|
|
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"ok": false, "error": "POST requis"})
|
|
return
|
|
}
|
|
if tuneDenyHTTP(w) {
|
|
return
|
|
}
|
|
cur := currentEngineBin()
|
|
prev := enginePrevious(cur)
|
|
if prev == nil {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": "aucune version précédente conservée"})
|
|
return
|
|
}
|
|
bin := prev["bin"].(string)
|
|
if err := SetConfigKey("BIN", bin); err != nil {
|
|
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
engineRememberPrevious(cur, bin)
|
|
engineRenderCancel()
|
|
if serviceIsActive() {
|
|
_ = serviceAction("restart")
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true, "bin": bin})
|
|
}
|