api specs

This commit is contained in:
2026-04-29 00:45:48 +04:00
parent 27143319e3
commit 8cda54548f
67 changed files with 5052 additions and 93 deletions

View File

@@ -69,7 +69,7 @@ func (a *App) loadCurrentScore(stage string, playerID int) (int, error) {
}
func (a *App) buildAdviceResponse(stage string, playerID int, raw scoreAdviceModelResponse) ScoreAdviceResponse {
advised := clampInt(raw.AdvisedScore, 0, 9999)
advised := clampInt(raw.AdvisedScore, 0, 100)
reason := strings.TrimSpace(raw.Reason)
if reason == "" {
reason = "AI estimated the score from visible impacts."

145
backend/api_docs.go Normal file
View File

@@ -0,0 +1,145 @@
package main
import (
_ "embed"
"fmt"
"net/http"
"github.com/labstack/echo/v4"
)
//go:embed openapi/openapi.json
var openAPISpecJSON string
func (a *App) handleOpenAPISpec(c echo.Context) error {
return c.Blob(http.StatusOK, "application/json; charset=utf-8", []byte(openAPISpecJSON))
}
func (a *App) handleOpenAPIDocs(c echo.Context) error {
html := fmt.Sprintf(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Shooting Event API Docs</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
<style>
body {
margin: 0;
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f7f9fc;
}
.docs-top {
background: #ffffff;
border-bottom: 1px solid #dde5f3;
padding: 14px 16px;
}
.docs-title {
margin: 0 0 8px;
font-size: 18px;
color: #1b1f40;
}
.docs-help {
margin: 0;
font-size: 14px;
color: #4c5773;
}
.auth-panel {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.auth-panel input {
height: 36px;
border: 1px solid #cdd7eb;
border-radius: 8px;
padding: 0 10px;
font-size: 14px;
}
.auth-panel button {
height: 36px;
border: 1px solid #20284f;
background: #20284f;
color: white;
border-radius: 8px;
padding: 0 12px;
cursor: pointer;
}
.auth-status {
font-size: 13px;
color: #38446b;
}
#swagger-ui {
max-width: 1300px;
margin: 0 auto;
}
</style>
</head>
<body>
<section class="docs-top">
<h1 class="docs-title">Shooting Event OpenAPI Docs</h1>
<p class="docs-help">
Use <strong>Try it out</strong> for live testing. For admin endpoints: login with the form below,
then your token is auto-attached to Swagger Authorize.
</p>
<form id="auth-form" class="auth-panel">
<input id="admin-user" type="text" value="datwyler" autocomplete="username" placeholder="Admin username" />
<input id="admin-pass" type="password" value="datwyler" autocomplete="current-password" placeholder="Admin password" />
<button type="submit">Login & Authorize</button>
<span id="auth-status" class="auth-status"></span>
</form>
</section>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script>
const ui = SwaggerUIBundle({
url: %q,
dom_id: '#swagger-ui',
deepLinking: true,
displayRequestDuration: true,
persistAuthorization: true,
docExpansion: 'list',
tryItOutEnabled: true,
});
async function loginAndAuthorize(event) {
event.preventDefault();
const status = document.getElementById('auth-status');
status.textContent = 'Logging in...';
const username = document.getElementById('admin-user').value || '';
const password = document.getElementById('admin-pass').value || '';
try {
const response = await fetch('/api/admin/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload.message || 'Login failed');
}
if (!payload.token) {
throw new Error('Login succeeded but token was not returned');
}
ui.preauthorizeApiKey('bearerAuth', payload.token);
status.textContent = 'Authorized until ' + (payload.expiresAt || 'token expiry');
} catch (error) {
status.textContent = 'Auth failed: ' + error.message;
}
}
document.getElementById('auth-form').addEventListener('submit', loginAndAuthorize);
</script>
</body>
</html>`, "/api/openapi.json")
return c.HTML(http.StatusOK, html)
}

View File

@@ -198,6 +198,6 @@ func scoreAdvicePrompt(stage string, currentScore int) string {
return fmt.Sprintf(`Target scoring assistant.
Stage: %s. Current score: %d.
Return STRICT JSON only:
{"advisedScore":<int 0..9999>,"reason":"<max 18 words>"}
{"advisedScore":<int 0..100>,"reason":"<max 18 words>"}
Do not add markdown or extra fields.`, stage, currentScore)
}

View File

@@ -310,8 +310,8 @@ func (a *App) handleUpdateScore(c echo.Context) error {
if err := c.Bind(&req); err != nil {
return writeError(c, http.StatusBadRequest, "invalid request body")
}
if req.Score < 0 || req.Score > 9999 {
return writeError(c, http.StatusBadRequest, "score must be between 0 and 9999")
if req.Score < 0 || req.Score > 100 {
return writeError(c, http.StatusBadRequest, "score must be between 0 and 100")
}
res, err := a.db.Exec(`

1408
backend/openapi/openapi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,10 @@ import (
func registerAPIRoutes(e *echo.Echo, app *App) {
api := e.Group("/api")
api.GET("/docs", app.handleOpenAPIDocs)
api.GET("/docs/", app.handleOpenAPIDocs)
api.GET("/openapi.json", app.handleOpenAPISpec)
api.GET("/health", app.handleHealth)
api.GET("/state", app.handleGetState)
api.GET("/events", app.handleEvents)