feat: add last refreshed timestamp to modules and CDN sources
CI / changes (push) Successful in 6s
CI / openapi (push) Successful in 23s
CI / go (push) Successful in 45s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m14s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m7s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m2s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 6m21s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m32s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m26s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m26s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m8s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m25s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m28s
CI / changes (push) Successful in 6s
CI / openapi (push) Successful in 23s
CI / go (push) Successful in 45s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m14s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m7s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go-prime (push) Successful in 23s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m2s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 6m21s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m32s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m26s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m26s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m8s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m25s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m28s
Introduced a new field, last_refreshed_at, to track the last successful update time for modules and CDN sources. Updated the database schema, API responses, and internal logic to support this feature. Enhanced the frontend to display the last refreshed timestamp, improving visibility into the data update status for users.
This commit is contained in:
@@ -339,6 +339,10 @@ components:
|
|||||||
type: ["string", "null"]
|
type: ["string", "null"]
|
||||||
default_community_id:
|
default_community_id:
|
||||||
type: ["string", "null"]
|
type: ["string", "null"]
|
||||||
|
last_refreshed_at:
|
||||||
|
type: ["string", "null"]
|
||||||
|
format: date-time
|
||||||
|
description: Время последнего успешного обновления данных модуля.
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
|
|
||||||
ModuleCreate:
|
ModuleCreate:
|
||||||
@@ -361,6 +365,10 @@ components:
|
|||||||
type: ["string", "null"]
|
type: ["string", "null"]
|
||||||
refresh_interval_sec:
|
refresh_interval_sec:
|
||||||
type: ["integer", "null"]
|
type: ["integer", "null"]
|
||||||
|
last_refreshed_at:
|
||||||
|
type: ["string", "null"]
|
||||||
|
format: date-time
|
||||||
|
description: Время последнего успешного обновления этого CDN-источника.
|
||||||
cron_expr:
|
cron_expr:
|
||||||
type: ["string", "null"]
|
type: ["string", "null"]
|
||||||
default_community_id:
|
default_community_id:
|
||||||
|
|||||||
@@ -110,6 +110,11 @@ func moduleJSON(mod *store.Module) map[string]any {
|
|||||||
"refresh_interval_sec": mod.RefreshIntervalSec,
|
"refresh_interval_sec": mod.RefreshIntervalSec,
|
||||||
"cron_expr": mod.CronExpr,
|
"cron_expr": mod.CronExpr,
|
||||||
}
|
}
|
||||||
|
if mod.LastRefreshedAt != nil {
|
||||||
|
m["last_refreshed_at"] = mod.LastRefreshedAt.UTC().Format(time.RFC3339Nano)
|
||||||
|
} else {
|
||||||
|
m["last_refreshed_at"] = nil
|
||||||
|
}
|
||||||
if mod.DefaultCommunityID != nil {
|
if mod.DefaultCommunityID != nil {
|
||||||
m["default_community_id"] = *mod.DefaultCommunityID
|
m["default_community_id"] = *mod.DefaultCommunityID
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -199,6 +199,11 @@ func cdnSourceJSON(x *store.CDNSource) map[string]any {
|
|||||||
} else {
|
} else {
|
||||||
m["community_id"] = nil
|
m["community_id"] = nil
|
||||||
}
|
}
|
||||||
|
if x.LastRefreshedAt != nil {
|
||||||
|
m["last_refreshed_at"] = x.LastRefreshedAt.UTC().Format(time.RFC3339Nano)
|
||||||
|
} else {
|
||||||
|
m["last_refreshed_at"] = nil
|
||||||
|
}
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ func RefreshModuleIngest(ctx context.Context, st store.Backend, hc *http.Client,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
refreshedAt := time.Now().UTC()
|
||||||
|
_, _ = st.UpdateModule(tenantID, moduleID, &store.ModulePatch{LastRefreshedAt: &refreshedAt})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ func (p *Postgres) PeerSessionCountsByState() map[string]int {
|
|||||||
func (p *Postgres) ListModules(tenantID string) []*store.Module {
|
func (p *Postgres) ListModules(tenantID string) []*store.Module {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
rows, err := p.pool.Query(ctx, `
|
rows, err := p.pool.Query(ctx, `
|
||||||
SELECT id, type, name, enabled, priority, doh_profile_id::text, refresh_interval_sec, cron_expr, default_community_id::text
|
SELECT id, type, name, enabled, priority, doh_profile_id::text, refresh_interval_sec, cron_expr, default_community_id::text, last_refreshed_at
|
||||||
FROM module WHERE tenant_id = $1 AND deleted_at IS NULL ORDER BY priority, name`, tenantID)
|
FROM module WHERE tenant_id = $1 AND deleted_at IS NULL ORDER BY priority, name`, tenantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -125,7 +125,8 @@ func (p *Postgres) ListModules(tenantID string) []*store.Module {
|
|||||||
m.TenantID = tenantID
|
m.TenantID = tenantID
|
||||||
var doh, dc, cron *string
|
var doh, dc, cron *string
|
||||||
var refresh *int32
|
var refresh *int32
|
||||||
if err := rows.Scan(&m.ID, &m.Type, &m.Name, &m.Enabled, &m.Priority, &doh, &refresh, &cron, &dc); err != nil {
|
var last *time.Time
|
||||||
|
if err := rows.Scan(&m.ID, &m.Type, &m.Name, &m.Enabled, &m.Priority, &doh, &refresh, &cron, &dc, &last); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if refresh != nil {
|
if refresh != nil {
|
||||||
@@ -140,6 +141,10 @@ func (p *Postgres) ListModules(tenantID string) []*store.Module {
|
|||||||
if dc != nil && *dc != "" {
|
if dc != nil && *dc != "" {
|
||||||
m.DefaultCommunityID = dc
|
m.DefaultCommunityID = dc
|
||||||
}
|
}
|
||||||
|
if last != nil {
|
||||||
|
t := last.UTC()
|
||||||
|
m.LastRefreshedAt = &t
|
||||||
|
}
|
||||||
out = append(out, &m)
|
out = append(out, &m)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
@@ -151,10 +156,11 @@ func (p *Postgres) GetModule(tenantID, moduleID string) (*store.Module, error) {
|
|||||||
m.TenantID = tenantID
|
m.TenantID = tenantID
|
||||||
var doh, dc, cron *string
|
var doh, dc, cron *string
|
||||||
var refresh *int32
|
var refresh *int32
|
||||||
|
var last *time.Time
|
||||||
err := p.pool.QueryRow(ctx, `
|
err := p.pool.QueryRow(ctx, `
|
||||||
SELECT id, type, name, enabled, priority, doh_profile_id::text, refresh_interval_sec, cron_expr, default_community_id::text
|
SELECT id, type, name, enabled, priority, doh_profile_id::text, refresh_interval_sec, cron_expr, default_community_id::text, last_refreshed_at
|
||||||
FROM module WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL`, moduleID, tenantID).Scan(
|
FROM module WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL`, moduleID, tenantID).Scan(
|
||||||
&m.ID, &m.Type, &m.Name, &m.Enabled, &m.Priority, &doh, &refresh, &cron, &dc)
|
&m.ID, &m.Type, &m.Name, &m.Enabled, &m.Priority, &doh, &refresh, &cron, &dc, &last)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return nil, store.ErrNotFound
|
return nil, store.ErrNotFound
|
||||||
@@ -173,6 +179,10 @@ func (p *Postgres) GetModule(tenantID, moduleID string) (*store.Module, error) {
|
|||||||
if dc != nil && *dc != "" {
|
if dc != nil && *dc != "" {
|
||||||
m.DefaultCommunityID = dc
|
m.DefaultCommunityID = dc
|
||||||
}
|
}
|
||||||
|
if last != nil {
|
||||||
|
t := last.UTC()
|
||||||
|
m.LastRefreshedAt = &t
|
||||||
|
}
|
||||||
return &m, nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,10 +207,14 @@ func (p *Postgres) CreateModule(tenantID string, in *store.Module) (*store.Modul
|
|||||||
if strings.TrimSpace(in.CronExpr) != "" {
|
if strings.TrimSpace(in.CronExpr) != "" {
|
||||||
cronArg = strings.TrimSpace(in.CronExpr)
|
cronArg = strings.TrimSpace(in.CronExpr)
|
||||||
}
|
}
|
||||||
|
var lastArg any
|
||||||
|
if in.LastRefreshedAt != nil {
|
||||||
|
lastArg = in.LastRefreshedAt.UTC()
|
||||||
|
}
|
||||||
_, err := p.pool.Exec(ctx, `
|
_, err := p.pool.Exec(ctx, `
|
||||||
INSERT INTO module (id, tenant_id, type, name, enabled, priority, doh_profile_id, refresh_interval_sec, cron_expr, default_community_id)
|
INSERT INTO module (id, tenant_id, type, name, enabled, priority, doh_profile_id, refresh_interval_sec, cron_expr, default_community_id, last_refreshed_at)
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`,
|
||||||
id, tenantID, in.Type, in.Name, in.Enabled, in.Priority, doh, ri, cronArg, dc)
|
id, tenantID, in.Type, in.Name, in.Enabled, in.Priority, doh, ri, cronArg, dc, lastArg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -224,6 +238,7 @@ func (p *Postgres) UpdateModule(tenantID, moduleID string, patch *store.ModulePa
|
|||||||
var dc, doh *string
|
var dc, doh *string
|
||||||
dc = base.DefaultCommunityID
|
dc = base.DefaultCommunityID
|
||||||
doh = base.DohProfileID
|
doh = base.DohProfileID
|
||||||
|
last := base.LastRefreshedAt
|
||||||
if patch.Name != nil {
|
if patch.Name != nil {
|
||||||
name = strings.TrimSpace(*patch.Name)
|
name = strings.TrimSpace(*patch.Name)
|
||||||
}
|
}
|
||||||
@@ -255,6 +270,10 @@ func (p *Postgres) UpdateModule(tenantID, moduleID string, patch *store.ModulePa
|
|||||||
doh = &v
|
doh = &v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if patch.LastRefreshedAt != nil {
|
||||||
|
t := patch.LastRefreshedAt.UTC()
|
||||||
|
last = &t
|
||||||
|
}
|
||||||
var dcArg, dohArg any
|
var dcArg, dohArg any
|
||||||
if dc != nil {
|
if dc != nil {
|
||||||
dcArg = *dc
|
dcArg = *dc
|
||||||
@@ -270,11 +289,15 @@ func (p *Postgres) UpdateModule(tenantID, moduleID string, patch *store.ModulePa
|
|||||||
if strings.TrimSpace(cron) != "" {
|
if strings.TrimSpace(cron) != "" {
|
||||||
cronArg = strings.TrimSpace(cron)
|
cronArg = strings.TrimSpace(cron)
|
||||||
}
|
}
|
||||||
|
var lastArg any
|
||||||
|
if last != nil {
|
||||||
|
lastArg = last.UTC()
|
||||||
|
}
|
||||||
_, err = p.pool.Exec(ctx, `
|
_, err = p.pool.Exec(ctx, `
|
||||||
UPDATE module SET name=$3, enabled=$4, priority=$5, refresh_interval_sec=$6, cron_expr=$7,
|
UPDATE module SET name=$3, enabled=$4, priority=$5, refresh_interval_sec=$6, cron_expr=$7,
|
||||||
default_community_id=$8, doh_profile_id=$9, updated_at=now()
|
default_community_id=$8, doh_profile_id=$9, last_refreshed_at=$10, updated_at=now()
|
||||||
WHERE id=$1 AND tenant_id=$2 AND deleted_at IS NULL`,
|
WHERE id=$1 AND tenant_id=$2 AND deleted_at IS NULL`,
|
||||||
moduleID, tenantID, name, en, pr, riArg, cronArg, dcArg, dohArg)
|
moduleID, tenantID, name, en, pr, riArg, cronArg, dcArg, dohArg, lastArg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,13 +83,14 @@ type Backend interface {
|
|||||||
|
|
||||||
// ModulePatch is a partial update for module.
|
// ModulePatch is a partial update for module.
|
||||||
type ModulePatch struct {
|
type ModulePatch struct {
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
Enabled *bool `json:"enabled,omitempty"`
|
Enabled *bool `json:"enabled,omitempty"`
|
||||||
Priority *int `json:"priority,omitempty"`
|
Priority *int `json:"priority,omitempty"`
|
||||||
RefreshIntervalSec *int `json:"refresh_interval_sec,omitempty"`
|
RefreshIntervalSec *int `json:"refresh_interval_sec,omitempty"`
|
||||||
CronExpr *string `json:"cron_expr,omitempty"`
|
CronExpr *string `json:"cron_expr,omitempty"`
|
||||||
DefaultCommunityID *string `json:"default_community_id,omitempty"`
|
DefaultCommunityID *string `json:"default_community_id,omitempty"`
|
||||||
DohProfileID *string `json:"doh_profile_id,omitempty"`
|
DohProfileID *string `json:"doh_profile_id,omitempty"`
|
||||||
|
LastRefreshedAt *time.Time `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CDNSource is a row under a CDN module.
|
// CDNSource is a row under a CDN module.
|
||||||
|
|||||||
+13
-12
@@ -38,7 +38,7 @@ type Memory struct {
|
|||||||
asEntries map[string]*ASEntry
|
asEntries map[string]*ASEntry
|
||||||
domainEnt map[string]*DomainEntry
|
domainEnt map[string]*DomainEntry
|
||||||
ipRanges map[string]*IPRangeEntry
|
ipRanges map[string]*IPRangeEntry
|
||||||
settings map[string]map[string]any // tenantID -> key -> JSON-compatible value
|
settings map[string]map[string]any // tenantID -> key -> JSON-compatible value
|
||||||
revPrefixes map[string][]PrefixRow
|
revPrefixes map[string][]PrefixRow
|
||||||
|
|
||||||
// DemoIDs valid after SeedDemo()
|
// DemoIDs valid after SeedDemo()
|
||||||
@@ -61,17 +61,18 @@ type Tenant struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Module struct {
|
type Module struct {
|
||||||
ID string
|
ID string
|
||||||
TenantID string
|
TenantID string
|
||||||
Type string // AS_PREFIXES, CDN_CIDRS, DOMAINS, IP_RANGES
|
Type string // AS_PREFIXES, CDN_CIDRS, DOMAINS, IP_RANGES
|
||||||
Name string
|
Name string
|
||||||
Enabled bool
|
Enabled bool
|
||||||
RefreshIntervalSec int // 0 = unset
|
RefreshIntervalSec int // 0 = unset
|
||||||
CronExpr string // optional cron for scheduler (display / future use)
|
CronExpr string // optional cron for scheduler (display / future use)
|
||||||
Priority int
|
Priority int
|
||||||
DefaultCommunityID *string
|
DefaultCommunityID *string
|
||||||
DohProfileID *string
|
DohProfileID *string
|
||||||
DeletedAt *time.Time
|
LastRefreshedAt *time.Time
|
||||||
|
DeletedAt *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type Revision struct {
|
type Revision struct {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ func (m *Memory) CreateModule(tenantID string, in *Module) (*Module, error) {
|
|||||||
CronExpr: in.CronExpr,
|
CronExpr: in.CronExpr,
|
||||||
DefaultCommunityID: in.DefaultCommunityID,
|
DefaultCommunityID: in.DefaultCommunityID,
|
||||||
DohProfileID: in.DohProfileID,
|
DohProfileID: in.DohProfileID,
|
||||||
|
LastRefreshedAt: in.LastRefreshedAt,
|
||||||
}
|
}
|
||||||
m.modules[id] = mod
|
m.modules[id] = mod
|
||||||
return mod, nil
|
return mod, nil
|
||||||
@@ -78,6 +79,10 @@ func (m *Memory) UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*M
|
|||||||
mod.DohProfileID = &v
|
mod.DohProfileID = &v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if patch.LastRefreshedAt != nil {
|
||||||
|
t := patch.LastRefreshedAt.UTC()
|
||||||
|
mod.LastRefreshedAt = &t
|
||||||
|
}
|
||||||
return mod, nil
|
return mod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE module
|
||||||
|
DROP COLUMN last_refreshed_at;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE module
|
||||||
|
ADD COLUMN last_refreshed_at TIMESTAMPTZ NULL;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE module
|
||||||
|
DROP COLUMN last_refreshed_at;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE module
|
||||||
|
ADD COLUMN last_refreshed_at TEXT;
|
||||||
@@ -18,6 +18,7 @@ export type ModuleRow = {
|
|||||||
cron_expr: string | null;
|
cron_expr: string | null;
|
||||||
default_community_id: string | null;
|
default_community_id: string | null;
|
||||||
doh_profile_id: string | null;
|
doh_profile_id: string | null;
|
||||||
|
last_refreshed_at: string | null;
|
||||||
};
|
};
|
||||||
export type ModulesResponse = Page<ModuleRow>;
|
export type ModulesResponse = Page<ModuleRow>;
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ export type CdnSource = {
|
|||||||
prefix_path: string;
|
prefix_path: string;
|
||||||
community_id: string | null;
|
community_id: string | null;
|
||||||
refresh_interval_sec: number | null;
|
refresh_interval_sec: number | null;
|
||||||
|
last_refreshed_at: string | null;
|
||||||
};
|
};
|
||||||
export type CdnSourceCreate = {
|
export type CdnSourceCreate = {
|
||||||
url: string;
|
url: string;
|
||||||
|
|||||||
@@ -136,6 +136,13 @@
|
|||||||
return '—';
|
return '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value: string | null | undefined): string {
|
||||||
|
if (typeof value !== 'string' || value.trim().length === 0) return '—';
|
||||||
|
const parsed = new Date(value);
|
||||||
|
if (Number.isNaN(parsed.getTime())) return '—';
|
||||||
|
return parsed.toLocaleString('ru-RU');
|
||||||
|
}
|
||||||
|
|
||||||
function toggleModuleSelection(id: string) {
|
function toggleModuleSelection(id: string) {
|
||||||
const next = new Set(selectedModuleIds);
|
const next = new Set(selectedModuleIds);
|
||||||
if (next.has(id)) next.delete(id);
|
if (next.has(id)) next.delete(id);
|
||||||
@@ -232,6 +239,7 @@
|
|||||||
<TableHead>Тип</TableHead>
|
<TableHead>Тип</TableHead>
|
||||||
<TableHead>Приоритет</TableHead>
|
<TableHead>Приоритет</TableHead>
|
||||||
<TableHead>Интервал</TableHead>
|
<TableHead>Интервал</TableHead>
|
||||||
|
<TableHead>Последнее обновление</TableHead>
|
||||||
<TableHead>Статус</TableHead>
|
<TableHead>Статус</TableHead>
|
||||||
<TableHead class="w-16"></TableHead>
|
<TableHead class="w-16"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -255,6 +263,9 @@
|
|||||||
<TableCell class="text-muted-foreground font-mono text-xs">
|
<TableCell class="text-muted-foreground font-mono text-xs">
|
||||||
{moduleIntervalLabel(m)}
|
{moduleIntervalLabel(m)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell class="text-muted-foreground text-sm whitespace-nowrap">
|
||||||
|
{formatDateTime(m.last_refreshed_at)}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{#if m.enabled}
|
{#if m.enabled}
|
||||||
<Badge variant="default" class="text-xs">вкл</Badge>
|
<Badge variant="default" class="text-xs">вкл</Badge>
|
||||||
@@ -270,7 +281,7 @@
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
{:else}
|
{:else}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colspan={7} class="text-muted-foreground text-center py-8">
|
<TableCell colspan={8} class="text-muted-foreground text-center py-8">
|
||||||
{loading ? 'Загрузка…' : 'Нет модулей. Создайте первый.'}
|
{loading ? 'Загрузка…' : 'Нет модулей. Создайте первый.'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -762,6 +762,13 @@
|
|||||||
return '—';
|
return '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value: string | null | undefined): string {
|
||||||
|
if (typeof value !== 'string' || value.trim().length === 0) return '—';
|
||||||
|
const parsed = new Date(value);
|
||||||
|
if (Number.isNaN(parsed.getTime())) return '—';
|
||||||
|
return parsed.toLocaleString('ru-RU');
|
||||||
|
}
|
||||||
|
|
||||||
const activeTab = $derived.by(() => {
|
const activeTab = $derived.by(() => {
|
||||||
if (!mod) return 'entries';
|
if (!mod) return 'entries';
|
||||||
switch (mod.type) {
|
switch (mod.type) {
|
||||||
@@ -831,7 +838,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Module metadata -->
|
<!-- Module metadata -->
|
||||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-5">
|
||||||
<Card class="bg-chart-3/10 p-3">
|
<Card class="bg-chart-3/10 p-3">
|
||||||
<p class="text-muted-foreground flex items-center gap-1.5 text-xs"><ArrowDownUp class="size-3.5" />Приоритет</p>
|
<p class="text-muted-foreground flex items-center gap-1.5 text-xs"><ArrowDownUp class="size-3.5" />Приоритет</p>
|
||||||
<p class="font-semibold">{mod.priority}</p>
|
<p class="font-semibold">{mod.priority}</p>
|
||||||
@@ -850,6 +857,10 @@
|
|||||||
<p class="text-muted-foreground flex items-center gap-1.5 text-xs"><ShieldCheck class="size-3.5" />Community по умолч.</p>
|
<p class="text-muted-foreground flex items-center gap-1.5 text-xs"><ShieldCheck class="size-3.5" />Community по умолч.</p>
|
||||||
<p class="font-semibold text-sm break-all">{communityLabel(mod.default_community_id)}</p>
|
<p class="font-semibold text-sm break-all">{communityLabel(mod.default_community_id)}</p>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card class="bg-muted/60 p-3">
|
||||||
|
<p class="text-muted-foreground flex items-center gap-1.5 text-xs"><RefreshCw class="size-3.5" />Последнее обновление</p>
|
||||||
|
<p class="font-semibold text-sm whitespace-nowrap">{formatDateTime(mod.last_refreshed_at)}</p>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
@@ -1058,6 +1069,7 @@
|
|||||||
<TableHead>Тип</TableHead>
|
<TableHead>Тип</TableHead>
|
||||||
<TableHead>Community</TableHead>
|
<TableHead>Community</TableHead>
|
||||||
<TableHead>Интервал</TableHead>
|
<TableHead>Интервал</TableHead>
|
||||||
|
<TableHead>Последнее обновление</TableHead>
|
||||||
<TableHead class="w-20"></TableHead>
|
<TableHead class="w-20"></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
@@ -1089,6 +1101,9 @@
|
|||||||
? `${src.refresh_interval_sec}с`
|
? `${src.refresh_interval_sec}с`
|
||||||
: '—'}
|
: '—'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell class="text-muted-foreground text-sm whitespace-nowrap">
|
||||||
|
{formatDateTime(src.last_refreshed_at)}
|
||||||
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<Button variant="ghost" size="icon-sm" onclick={() => openCdnEdit(src)}><Pencil class="size-3.5" /></Button>
|
<Button variant="ghost" size="icon-sm" onclick={() => openCdnEdit(src)}><Pencil class="size-3.5" /></Button>
|
||||||
@@ -1098,7 +1113,7 @@
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
{:else}
|
{:else}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colspan={6} class="text-muted-foreground text-center py-6">Нет источников</TableCell>
|
<TableCell colspan={7} class="text-muted-foreground text-center py-6">Нет источников</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{/each}
|
{/each}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
|||||||
Reference in New Issue
Block a user