feat(funcs-console): показывать sless_service вместе с функциями — единый листинг

This commit is contained in:
Naeel
2026-03-21 07:00:59 +03:00
parent e8d0d78310
commit 683d7282af
2 changed files with 97 additions and 16 deletions
+36 -2
View File
@@ -176,6 +176,18 @@
color: #f85149;
}
.badge-kind-service {
background: #0d2538;
border: 1px solid #1158a7;
color: #58a6ff;
}
.badge-kind-function {
background: #1a2538;
border: 1px solid #30363d;
color: #8b949e;
}
.fn-url {
font-size: 0.76rem;
color: #58a6ff;
@@ -328,7 +340,10 @@
var byFn = data.triggersByFn || {};
document.getElementById('ns-label').textContent = ns;
document.getElementById('summary').textContent = 'Функций: ' + fns.length;
var svcsCount = fns.filter(function(f){ return f.kind === 'service'; }).length;
var fnsCount = fns.filter(function(f){ return f.kind !== 'service'; }).length;
document.getElementById('summary').textContent =
'Функций: ' + fnsCount + ' | Сервисов: ' + svcsCount + ' | Всего: ' + fns.length;
var list = document.getElementById('fn-list');
if (fns.length === 0) {
@@ -348,11 +363,29 @@
$txt(hdr, 'span', 'fn-name', fn.name);
$txt(hdr, 'span', 'badge badge-runtime', fn.runtime);
$txt(hdr, 'span', 'badge badge-' + fn.phase, fn.phase);
$txt(hdr, 'span', 'badge badge-' + fn.phase, fn.phase || 'Pending');
if (fn.kind) {
var kindLabel = fn.kind === 'service' ? 'always-on' : 'job';
$txt(hdr, 'span', 'badge badge-kind-' + fn.kind, kindLabel);
}
// sless_service имеет прямой URL из статуса деплоя
if (fn.url) {
var a = document.createElement('a');
a.className = 'fn-url';
a.href = fn.url;
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.textContent = fn.url;
a.addEventListener('click', function (e) { e.stopPropagation(); });
hdr.appendChild(a);
}
for (var t = 0; t < triggers.length; t++) {
var tr = triggers[t];
if (tr.type === 'http') {
// Для job-style функций URL берём из триггера; для сервисов уже показан fn.url
if (!fn.url) {
var url = extURL ? extURL + '/fn/' + ns + '/' + fn.name : (tr.url || '');
if (url) {
var a = document.createElement('a');
@@ -364,6 +397,7 @@
a.addEventListener('click', function (e) { e.stopPropagation(); });
hdr.appendChild(a);
}
}
hdr.appendChild(makeTriggerBtn(tr, ns));
} else if (tr.type === 'cron') {
$txt(hdr, 'span', 'fn-cron', 'cron: ' + tr.schedule);
+51 -4
View File
@@ -1,4 +1,4 @@
// Изменено: 2026-04-25 (добавлена HTML web-консоль, proxy endpoints для source/triggers)
// Изменено: 2026-03-21 (добавлен вывод sless_service вместе с функциями — пользователь видит оба типа как «функции»)
// main.go — глобальный HTTP сервис листинга функций пользователя.
// Развёрнут ОДИН РАЗ в namespace sless; работает для ВСЕХ пользователей.
// Не связан с terraform — деплоится манифестом deployments/k8s/funcs-service.yaml.
@@ -39,7 +39,9 @@ import (
//go:embed index.html
var htmlPageTemplate string
// fnResponse — ответ /v1/namespaces/{ns}/functions (подмножество полей оператора)
// fnResponse — унифицированная запись функции для листинга.
// Kind="function" — job-style (запускается per-request).
// Kind="service" — always-on Deployment (всегда готов к запросам, имеет прямой URL).
type fnResponse struct {
Name string `json:"name"`
Runtime string `json:"runtime"`
@@ -47,6 +49,19 @@ type fnResponse struct {
Message string `json:"message"`
CreatedAt string `json:"created_at"`
LastBuiltAt string `json:"last_built_at"`
Kind string `json:"kind"` // "function" | "service"
URL string `json:"url,omitempty"` // прямой URL для sless_service
}
// svcResponse — ответ /v1/namespaces/{ns}/services (подмножество полей оператора).
type svcResponse struct {
Name string `json:"name"`
Runtime string `json:"runtime"`
Phase string `json:"phase"`
Message string `json:"message"`
CreatedAt string `json:"created_at"`
LastBuiltAt string `json:"last_built_at"`
URL string `json:"url"`
}
// trResponse — ответ /v1/namespaces/{ns}/triggers
@@ -182,13 +197,35 @@ func handleFuncsNS(operatorURL, externalURL, serviceToken string, exclude map[st
}
}
// fetchAndRender загружает данные из оператора и отдаёт ответ: HTML (браузер) или plain text (curl).
// fetchAndRender загружает functions + services из оператора и отдаёт ответ.
// Оба типа объединяются в единый список — пользователь видит их как «функции».
func fetchAndRender(w http.ResponseWriter, r *http.Request, operatorURL, externalURL, authHeader, ns string, exclude map[string]bool) {
fns, err := apiGet[[]fnResponse](operatorURL+"/v1/namespaces/"+ns+"/functions", authHeader)
if err != nil {
http.Error(w, fmt.Sprintf("operator error (functions): %s\n", err), http.StatusBadGateway)
return
}
for i := range fns {
fns[i].Kind = "function"
}
// sless_service — always-on Deployment + URL. Объединяем с функциями.
svcs, svcErr := apiGet[[]svcResponse](operatorURL+"/v1/namespaces/"+ns+"/services", authHeader)
if svcErr == nil {
for _, svc := range svcs {
fns = append(fns, fnResponse{
Name: svc.Name,
Runtime: svc.Runtime,
Phase: svc.Phase,
Message: svc.Message,
CreatedAt: svc.CreatedAt,
LastBuiltAt: svc.LastBuiltAt,
Kind: "service",
URL: svc.URL,
})
}
}
trs, err := apiGet[[]trResponse](operatorURL+"/v1/namespaces/"+ns+"/triggers", authHeader)
if err != nil {
http.Error(w, fmt.Sprintf("operator error (triggers): %s\n", err), http.StatusBadGateway)
@@ -279,7 +316,10 @@ func servePlainText(w http.ResponseWriter, ns, externalURL string, fns []fnRespo
} else {
sb.WriteString(" active: нет\n")
}
if len(it.httpT) > 0 {
// Для sless_service — прямой URL из статуса деплоя
if fn.URL != "" {
sb.WriteString(fmt.Sprintf(" url: %s\n", fn.URL))
} else if len(it.httpT) > 0 {
url := it.httpT[0].URL
if externalURL != "" {
url = externalURL + "/fn/" + ns + "/" + fn.Name
@@ -415,6 +455,13 @@ func isValidK8sName(s string) bool {
}
func buildComment(fn fnResponse, httpT, cronT []trResponse) string {
if fn.Kind == "service" {
active := "неактивна"
if fn.Phase == "Ready" {
active = "активна"
}
return fmt.Sprintf("always-on сервис (%s) — %s, %s", fn.Runtime, fn.Phase, active)
}
if len(httpT) > 0 {
active := "активна"
if !httpT[0].Active {