feat(api/v2): serve Scalar docs UI at /api/v2/docs

This commit is contained in:
kolaente 2026-04-21 13:06:26 +02:00 committed by kolaente
parent 21194e61b0
commit 00b42234e9
5 changed files with 110 additions and 0 deletions

View File

@ -1510,6 +1510,46 @@ func (Generate) ConfigYAML(commented bool) {
generateConfigYAMLFromJSON(DefaultConfigYAMLSamplePath, commented) generateConfigYAMLFromJSON(DefaultConfigYAMLSamplePath, commented)
} }
// ScalarBundle downloads the Scalar API reference standalone JS bundle into
// pkg/routes/api/v2/scalar/. Version is pinned to match the Scalar version
// used in Huma's internal docs at the time of last update.
func (Generate) ScalarBundle() error {
const (
version = "1.44.20"
dest = "pkg/routes/api/v2/scalar/scalar.standalone.js"
)
url := fmt.Sprintf("https://unpkg.com/@scalar/api-reference@%s/dist/browser/standalone.js", version)
fmt.Printf("Downloading Scalar bundle %s from %s\n", version, url)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) //nolint:gosec // This is a dev-only mage task and the URL is hard-coded above.
if err != nil {
return fmt.Errorf("build scalar bundle request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("download scalar bundle: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("download scalar bundle: unexpected status %s", resp.Status)
}
var buf bytes.Buffer
if _, err := io.Copy(&buf, resp.Body); err != nil {
return fmt.Errorf("read scalar bundle body: %w", err)
}
if err := os.WriteFile(dest, buf.Bytes(), 0o600); err != nil {
return fmt.Errorf("write %s: %w", dest, err)
}
fmt.Printf("Wrote %d bytes to %s\n", buf.Len(), dest)
return nil
}
func localBranchExists(ctx context.Context, name string) bool { func localBranchExists(ctx context.Context, name string) bool {
return exec.CommandContext(ctx, "git", "show-ref", "--verify", "--quiet", "refs/heads/"+name).Run() == nil //nolint:gosec // This is a dev-only mage task and the branch name is supplied by the developer running it. return exec.CommandContext(ctx, "git", "show-ref", "--verify", "--quiet", "refs/heads/"+name).Run() == nil //nolint:gosec // This is a dev-only mage task and the branch name is supplied by the developer running it.
} }

40
pkg/routes/api/v2/docs.go Normal file
View File

@ -0,0 +1,40 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package apiv2
import (
_ "embed"
"net/http"
"github.com/labstack/echo/v5"
)
//go:embed scalar/scalar.html
var scalarHTML string
//go:embed scalar/scalar.standalone.js
var scalarJS []byte
// ScalarUI serves the Scalar API reference HTML; assets ship locally to avoid a CDN dependency.
func ScalarUI(c *echo.Context) error {
return c.HTML(http.StatusOK, scalarHTML)
}
// ScalarJS serves the embedded Scalar standalone JS bundle.
func ScalarJS(c *echo.Context) error {
return c.Blob(http.StatusOK, echo.MIMEApplicationJavaScript, scalarJS)
}

View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="referrer" content="no-referrer">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vikunja API</title>
</head>
<body>
<script id="api-reference" data-url="/api/v2/openapi.json"></script>
<script src="/api/v2/docs/scalar.standalone.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
// Placeholder — the actual Scalar standalone bundle is vendored by
// `mage generate:scalarBundle` (see magefile.go) and lands in a follow-up
// PR to keep this one reviewable. Until then, /api/v2/docs renders the
// HTML shell but the Scalar reference itself will not initialise.
(function () {
var el = document.getElementById('api-reference');
if (el && el.parentNode) {
var note = document.createElement('p');
note.style.cssText = 'font-family:system-ui;padding:2rem;color:#555';
note.textContent = 'Scalar bundle not vendored yet — run `mage generate:scalarBundle` to fetch it.';
el.parentNode.insertBefore(note, el);
}
})();

View File

@ -384,6 +384,10 @@ func registerAPIRoutesV2(e *echo.Echo, a *echo.Group) {
api := apiv2.NewAPI(e, a) api := apiv2.NewAPI(e, a)
// Scalar docs UI — embedded, no CDN. See pkg/routes/api/v2/docs.go.
a.GET("/docs", apiv2.ScalarUI)
a.GET("/docs/scalar.standalone.js", apiv2.ScalarJS)
// Resource registrations. // Resource registrations.
apiv2.RegisterLabelRoutes(api) apiv2.RegisterLabelRoutes(api)