feat(update): server push-команда на массовое обновление UI и формат версий

This commit is contained in:
AidarKC
2026-04-23 16:19:00 +03:00
parent 630ba30c27
commit f213e9aa43
10 changed files with 252 additions and 14 deletions
@@ -30,6 +30,7 @@ public final class DebugApiConfigurator {
addServlet(context, new DebugClientsServlet(tokenProvider), "/debug/ws/clients");
addServlet(context, new DebugConnectServlet(tokenProvider), "/debug/ws/connect");
addServlet(context, new DebugLogsServlet(tokenProvider), "/debug/ws/logs");
addServlet(context, new DebugUiReloadAllServlet(tokenProvider), "/debug/ws/ui-reload-all");
log.info("✅ Debug API включены настройкой {}=true", CFG_DEBUG_API_ENABLED);
}
@@ -51,4 +51,10 @@ public final class DebugTokenProvider {
String actual = authorizationHeader.substring("Bearer ".length()).trim();
return token.equals(actual);
}
public boolean matchesRawToken(String rawToken) {
if (!isEnabled()) return false;
if (rawToken == null || rawToken.isBlank()) return false;
return token.equals(rawToken.trim());
}
}
@@ -0,0 +1,133 @@
package server.debug;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
import server.logic.ws_protocol.JSON.ConnectionContext;
import server.logic.ws_protocol.JSON.push.WsEventSender;
import java.io.IOException;
import java.util.Set;
/**
* POST /debug/ws/ui-reload-all
*
* Заголовки:
* - X-Debug-Token: <token> (рекомендуется)
* - или Authorization: Bearer <token>
*
* Тело (опционально):
* {
* "reason": "deploy_ui",
* "reloadAfterMs": 800
* }
*/
public class DebugUiReloadAllServlet extends HttpServlet {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final DebugTokenProvider tokenProvider;
public DebugUiReloadAllServlet(DebugTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
if (!tokenProvider.isEnabled()) {
writeError(resp, 503, "DEBUG_DISABLED", "Debug API отключен: .debug-token не найден или пуст");
return;
}
String headerToken = req.getHeader("X-Debug-Token");
String bearerHeader = req.getHeader("Authorization");
boolean authorized = tokenProvider.matchesRawToken(headerToken) || tokenProvider.matchesBearerHeader(bearerHeader);
if (!authorized) {
writeError(resp, 401, "UNAUTHORIZED", "Неверный debug token");
return;
}
JsonNode body = null;
try {
if (req.getContentLengthLong() != 0) {
body = MAPPER.readTree(req.getInputStream());
}
} catch (Exception e) {
writeError(resp, 400, "BAD_JSON", "Тело запроса должно быть JSON");
return;
}
String reason = safe(text(body, "reason"));
if (reason.isBlank()) reason = "manual_debug_api";
int reloadAfterMs = clampInt(body == null ? 700 : body.path("reloadAfterMs").asInt(700), 100, 15_000);
long issuedAtMs = System.currentTimeMillis();
Set<ConnectionContext> all = ActiveConnectionsRegistry.getInstance().getAllConnectionsSnapshot();
int totalConnections = 0;
int sent = 0;
for (ConnectionContext ctx : all) {
if (ctx == null || ctx.getWsSession() == null || !ctx.getWsSession().isOpen()) continue;
totalConnections += 1;
ObjectNode payload = MAPPER.createObjectNode();
payload.put("reason", reason);
payload.put("issuedAtMs", issuedAtMs);
payload.put("reloadAfterMs", reloadAfterMs);
String sessionId = safe(ctx.getSessionId());
String eventId = "evt-ui-reload-" + issuedAtMs + "-" + (sessionId.isBlank() ? totalConnections : sessionId);
if (WsEventSender.sendEvent(ctx, "ForceUiReload", eventId, payload)) {
sent += 1;
}
}
ObjectNode payload = MAPPER.createObjectNode();
payload.put("accepted", sent > 0);
payload.put("reason", reason);
payload.put("reloadAfterMs", reloadAfterMs);
payload.put("issuedAtMs", issuedAtMs);
payload.put("totalConnections", totalConnections);
payload.put("sentCount", sent);
payload.put("skippedCount", Math.max(0, totalConnections - sent));
writeOk(resp, payload);
}
private static int clampInt(int value, int min, int max) {
return Math.min(max, Math.max(min, value));
}
private static String text(JsonNode node, String field) {
if (node == null) return "";
return String.valueOf(node.path(field).asText(""));
}
private static String safe(String value) {
return value == null ? "" : value.trim();
}
private static void writeOk(HttpServletResponse resp, ObjectNode payload) throws IOException {
ObjectNode root = MAPPER.createObjectNode();
root.put("ok", true);
root.set("payload", payload == null ? MAPPER.createObjectNode() : payload);
writeJson(resp, 200, root);
}
private static void writeError(HttpServletResponse resp, int status, String code, String message) throws IOException {
ObjectNode root = MAPPER.createObjectNode();
root.put("ok", false);
root.put("code", code);
root.put("message", message);
writeJson(resp, status, root);
}
private static void writeJson(HttpServletResponse resp, int status, ObjectNode root) throws IOException {
resp.setStatus(status);
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json; charset=UTF-8");
resp.getWriter().write(MAPPER.writeValueAsString(root));
}
}