Compare commits
2
Commits
8a50ba44b3
...
1e13ef9e70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e13ef9e70 | ||
|
|
47764345f6 |
No files matched your search
@@ -339,6 +339,10 @@ components:
|
||||
type: ["string", "null"]
|
||||
default_community_id:
|
||||
type: ["string", "null"]
|
||||
last_refreshed_at:
|
||||
type: ["string", "null"]
|
||||
format: date-time
|
||||
description: Время последнего успешного обновления данных модуля.
|
||||
additionalProperties: true
|
||||
|
||||
ModuleCreate:
|
||||
@@ -361,6 +365,10 @@ components:
|
||||
type: ["string", "null"]
|
||||
refresh_interval_sec:
|
||||
type: ["integer", "null"]
|
||||
last_refreshed_at:
|
||||
type: ["string", "null"]
|
||||
format: date-time
|
||||
description: Время последнего успешного обновления этого CDN-источника.
|
||||
cron_expr:
|
||||
type: ["string", "null"]
|
||||
default_community_id:
|
||||
|
||||
@@ -110,6 +110,11 @@ func moduleJSON(mod *store.Module) map[string]any {
|
||||
"refresh_interval_sec": mod.RefreshIntervalSec,
|
||||
"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 {
|
||||
m["default_community_id"] = *mod.DefaultCommunityID
|
||||
} else {
|
||||
|
||||
@@ -199,6 +199,11 @@ func cdnSourceJSON(x *store.CDNSource) map[string]any {
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,8 @@ func RefreshModuleIngest(ctx context.Context, st store.Backend, hc *http.Client,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refreshedAt := time.Now().UTC()
|
||||
_, _ = st.UpdateModule(tenantID, moduleID, &store.ModulePatch{LastRefreshedAt: &refreshedAt})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -200,7 +202,16 @@ func collectModulePrefixRows(ctx context.Context, st store.Backend, hc *http.Cli
|
||||
return nil, err
|
||||
}
|
||||
var rows []store.PrefixRow
|
||||
latestCDNRows := latestCDNRowsBySource(st, tenantID)
|
||||
for _, src := range sources {
|
||||
sourceKey := "cdn:" + src.ID
|
||||
now := time.Now().UTC()
|
||||
if shouldSkipCDNSourceFetch(src, now) {
|
||||
if cached := latestCDNRows[sourceKey]; len(cached) > 0 {
|
||||
rows = append(rows, cached...)
|
||||
continue
|
||||
}
|
||||
}
|
||||
u := strings.TrimSpace(src.URL)
|
||||
if u == "" {
|
||||
continue
|
||||
@@ -224,10 +235,14 @@ func collectModulePrefixRows(ctx context.Context, st store.Backend, hc *http.Cli
|
||||
return nil, err
|
||||
}
|
||||
etag := strings.TrimSpace(resp.Header.Get("ETag"))
|
||||
patch := &store.CDNSourcePatch{}
|
||||
if etag != "" && etag != strings.TrimSpace(src.Etag) {
|
||||
e := etag
|
||||
_, _ = st.UpdateCDNSource(tenantID, moduleID, src.ID, &store.CDNSourcePatch{Etag: &e})
|
||||
patch.Etag = &e
|
||||
}
|
||||
refreshedAt := now
|
||||
patch.LastRefreshedAt = &refreshedAt
|
||||
_, _ = st.UpdateCDNSource(tenantID, moduleID, src.ID, patch)
|
||||
pfxs, err := ExtractCIDRs(string(body), src.SourceKind, src.PrefixPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cdn parse %s: %w", u, err)
|
||||
@@ -238,7 +253,7 @@ func collectModulePrefixRows(ctx context.Context, st store.Backend, hc *http.Cli
|
||||
c := *mod.DefaultCommunityID
|
||||
comm = &c
|
||||
}
|
||||
rows = append(rows, store.PrefixRow{Prefix: pfx.String(), CommunityID: comm, Source: "cdn:" + src.ID})
|
||||
rows = append(rows, store.PrefixRow{Prefix: pfx.String(), CommunityID: comm, Source: sourceKey})
|
||||
}
|
||||
}
|
||||
return rows, nil
|
||||
@@ -293,6 +308,41 @@ func collectModulePrefixRows(ctx context.Context, st store.Backend, hc *http.Cli
|
||||
}
|
||||
}
|
||||
|
||||
func shouldSkipCDNSourceFetch(src *store.CDNSource, now time.Time) bool {
|
||||
if src == nil || src.RefreshIntervalSec == nil || *src.RefreshIntervalSec <= 0 || src.LastRefreshedAt == nil {
|
||||
return false
|
||||
}
|
||||
nextRefreshAt := src.LastRefreshedAt.UTC().Add(time.Duration(*src.RefreshIntervalSec) * time.Second)
|
||||
return now.UTC().Before(nextRefreshAt)
|
||||
}
|
||||
|
||||
func latestCDNRowsBySource(st store.Backend, tenantID string) map[string][]store.PrefixRow {
|
||||
out := make(map[string][]store.PrefixRow)
|
||||
if st == nil {
|
||||
return out
|
||||
}
|
||||
revs, _, _ := st.ListRevisions(tenantID, "", "", 1)
|
||||
if len(revs) == 0 || strings.TrimSpace(revs[0].ID) == "" {
|
||||
return out
|
||||
}
|
||||
revID := strings.TrimSpace(revs[0].ID)
|
||||
cursor := ""
|
||||
for {
|
||||
page, next, more := st.ListRevisionPrefixes(tenantID, revID, cursor, 2000)
|
||||
for _, row := range page {
|
||||
if !strings.HasPrefix(strings.TrimSpace(row.Source), "cdn:") {
|
||||
continue
|
||||
}
|
||||
out[row.Source] = append(out[row.Source], row)
|
||||
}
|
||||
if !more || strings.TrimSpace(next) == "" {
|
||||
break
|
||||
}
|
||||
cursor = next
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type dohJSONAnswer struct {
|
||||
Type int `json:"type"`
|
||||
Data string `json:"data"`
|
||||
|
||||
@@ -113,7 +113,7 @@ func (p *Postgres) PeerSessionCountsByState() map[string]int {
|
||||
func (p *Postgres) ListModules(tenantID string) []*store.Module {
|
||||
ctx := context.Background()
|
||||
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)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -125,7 +125,8 @@ func (p *Postgres) ListModules(tenantID string) []*store.Module {
|
||||
m.TenantID = tenantID
|
||||
var doh, dc, cron *string
|
||||
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
|
||||
}
|
||||
if refresh != nil {
|
||||
@@ -140,6 +141,10 @@ func (p *Postgres) ListModules(tenantID string) []*store.Module {
|
||||
if dc != nil && *dc != "" {
|
||||
m.DefaultCommunityID = dc
|
||||
}
|
||||
if last != nil {
|
||||
t := last.UTC()
|
||||
m.LastRefreshedAt = &t
|
||||
}
|
||||
out = append(out, &m)
|
||||
}
|
||||
return out
|
||||
@@ -151,10 +156,11 @@ func (p *Postgres) GetModule(tenantID, moduleID string) (*store.Module, error) {
|
||||
m.TenantID = tenantID
|
||||
var doh, dc, cron *string
|
||||
var refresh *int32
|
||||
var last *time.Time
|
||||
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(
|
||||
&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 errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, store.ErrNotFound
|
||||
@@ -173,6 +179,10 @@ func (p *Postgres) GetModule(tenantID, moduleID string) (*store.Module, error) {
|
||||
if dc != nil && *dc != "" {
|
||||
m.DefaultCommunityID = dc
|
||||
}
|
||||
if last != nil {
|
||||
t := last.UTC()
|
||||
m.LastRefreshedAt = &t
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
@@ -197,10 +207,14 @@ func (p *Postgres) CreateModule(tenantID string, in *store.Module) (*store.Modul
|
||||
if 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, `
|
||||
INSERT INTO module (id, tenant_id, type, name, enabled, priority, doh_profile_id, refresh_interval_sec, cron_expr, default_community_id)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
|
||||
id, tenantID, in.Type, in.Name, in.Enabled, in.Priority, doh, ri, cronArg, dc)
|
||||
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,$11)`,
|
||||
id, tenantID, in.Type, in.Name, in.Enabled, in.Priority, doh, ri, cronArg, dc, lastArg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -224,6 +238,7 @@ func (p *Postgres) UpdateModule(tenantID, moduleID string, patch *store.ModulePa
|
||||
var dc, doh *string
|
||||
dc = base.DefaultCommunityID
|
||||
doh = base.DohProfileID
|
||||
last := base.LastRefreshedAt
|
||||
if patch.Name != nil {
|
||||
name = strings.TrimSpace(*patch.Name)
|
||||
}
|
||||
@@ -255,6 +270,10 @@ func (p *Postgres) UpdateModule(tenantID, moduleID string, patch *store.ModulePa
|
||||
doh = &v
|
||||
}
|
||||
}
|
||||
if patch.LastRefreshedAt != nil {
|
||||
t := patch.LastRefreshedAt.UTC()
|
||||
last = &t
|
||||
}
|
||||
var dcArg, dohArg any
|
||||
if dc != nil {
|
||||
dcArg = *dc
|
||||
@@ -270,11 +289,15 @@ func (p *Postgres) UpdateModule(tenantID, moduleID string, patch *store.ModulePa
|
||||
if strings.TrimSpace(cron) != "" {
|
||||
cronArg = strings.TrimSpace(cron)
|
||||
}
|
||||
var lastArg any
|
||||
if last != nil {
|
||||
lastArg = last.UTC()
|
||||
}
|
||||
_, err = p.pool.Exec(ctx, `
|
||||
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`,
|
||||
moduleID, tenantID, name, en, pr, riArg, cronArg, dcArg, dohArg)
|
||||
moduleID, tenantID, name, en, pr, riArg, cronArg, dcArg, dohArg, lastArg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -698,7 +721,7 @@ func (p *Postgres) CreateRollbackRevision(tenantID, sourceRevisionID string) (st
|
||||
newID := uuid.NewString()
|
||||
parent := sourceRevisionID
|
||||
meta, _ := json.Marshal(map[string]any{
|
||||
"preview_fragments": src.PreviewFragments,
|
||||
"preview_fragments": src.PreviewFragments,
|
||||
"materialized_prefix_count": src.MaterializedPrefixCount,
|
||||
})
|
||||
var modArg any
|
||||
@@ -883,7 +906,7 @@ func (p *Postgres) CreateRenderRevision(revisionID, tenantID, moduleID string, p
|
||||
previewFragments = map[string]string{}
|
||||
}
|
||||
meta, err := json.Marshal(map[string]any{
|
||||
"preview_fragments": previewFragments,
|
||||
"preview_fragments": previewFragments,
|
||||
"materialized_prefix_count": len(prefixes),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -1178,6 +1201,14 @@ func nullInt32Ptr(i *int) *int32 {
|
||||
return &v
|
||||
}
|
||||
|
||||
func nullTimePtr(t *time.Time) *time.Time {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
v := t.UTC()
|
||||
return &v
|
||||
}
|
||||
|
||||
func nullJSON(s string) *string {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
v := "{}"
|
||||
|
||||
@@ -23,7 +23,7 @@ func (p *Postgres) ListCDNSources(tenantID, moduleID string) ([]*store.CDNSource
|
||||
}
|
||||
ctx := context.Background()
|
||||
rows, err := p.pool.Query(ctx, `
|
||||
SELECT id::text, source_kind, url, COALESCE(prefix_path,''), COALESCE(etag,''), refresh_interval_sec, community_id::text
|
||||
SELECT id::text, source_kind, url, COALESCE(prefix_path,''), COALESCE(etag,''), refresh_interval_sec, community_id::text, last_refreshed_at
|
||||
FROM module_cdn_source WHERE module_id=$1 ORDER BY url`, moduleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -35,7 +35,8 @@ func (p *Postgres) ListCDNSources(tenantID, moduleID string) ([]*store.CDNSource
|
||||
s.ModuleID = moduleID
|
||||
var ri *int32
|
||||
var comm *string
|
||||
if err := rows.Scan(&s.ID, &s.SourceKind, &s.URL, &s.PrefixPath, &s.Etag, &ri, &comm); err != nil {
|
||||
var last *time.Time
|
||||
if err := rows.Scan(&s.ID, &s.SourceKind, &s.URL, &s.PrefixPath, &s.Etag, &ri, &comm, &last); err != nil {
|
||||
continue
|
||||
}
|
||||
if ri != nil {
|
||||
@@ -43,6 +44,10 @@ func (p *Postgres) ListCDNSources(tenantID, moduleID string) ([]*store.CDNSource
|
||||
s.RefreshIntervalSec = &v
|
||||
}
|
||||
s.CommunityID = strOrNil(comm)
|
||||
if last != nil {
|
||||
t := last.UTC()
|
||||
s.LastRefreshedAt = &t
|
||||
}
|
||||
out = append(out, &s)
|
||||
}
|
||||
return out, nil
|
||||
@@ -62,9 +67,9 @@ func (p *Postgres) CreateCDNSource(tenantID, moduleID string, in *store.CDNSourc
|
||||
ctx := context.Background()
|
||||
id := uuid.NewString()
|
||||
_, err = p.pool.Exec(ctx, `
|
||||
INSERT INTO module_cdn_source (id, module_id, source_kind, url, prefix_path, etag, refresh_interval_sec, community_id)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7, NULLIF($8::uuid, '00000000-0000-0000-0000-000000000000'::uuid))`,
|
||||
id, moduleID, in.SourceKind, strings.TrimSpace(in.URL), strings.TrimSpace(in.PrefixPath), in.Etag, nullInt32Ptr(in.RefreshIntervalSec), uuidOrNilPtr(in.CommunityID))
|
||||
INSERT INTO module_cdn_source (id, module_id, source_kind, url, prefix_path, etag, refresh_interval_sec, community_id, last_refreshed_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7, NULLIF($8::uuid, '00000000-0000-0000-0000-000000000000'::uuid), $9)`,
|
||||
id, moduleID, in.SourceKind, strings.TrimSpace(in.URL), strings.TrimSpace(in.PrefixPath), in.Etag, nullInt32Ptr(in.RefreshIntervalSec), uuidOrNilPtr(in.CommunityID), nullTimePtr(in.LastRefreshedAt))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -76,9 +81,10 @@ func (p *Postgres) getCDNSource(ctx context.Context, moduleID, id string) (*stor
|
||||
s.ModuleID = moduleID
|
||||
var ri *int32
|
||||
var comm *string
|
||||
var last *time.Time
|
||||
err := p.pool.QueryRow(ctx, `
|
||||
SELECT id::text, source_kind, url, COALESCE(prefix_path,''), COALESCE(etag,''), refresh_interval_sec, community_id::text
|
||||
FROM module_cdn_source WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&s.ID, &s.SourceKind, &s.URL, &s.PrefixPath, &s.Etag, &ri, &comm)
|
||||
SELECT id::text, source_kind, url, COALESCE(prefix_path,''), COALESCE(etag,''), refresh_interval_sec, community_id::text, last_refreshed_at
|
||||
FROM module_cdn_source WHERE id=$1 AND module_id=$2`, id, moduleID).Scan(&s.ID, &s.SourceKind, &s.URL, &s.PrefixPath, &s.Etag, &ri, &comm, &last)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -87,6 +93,10 @@ func (p *Postgres) getCDNSource(ctx context.Context, moduleID, id string) (*stor
|
||||
s.RefreshIntervalSec = &v
|
||||
}
|
||||
s.CommunityID = strOrNil(comm)
|
||||
if last != nil {
|
||||
t := last.UTC()
|
||||
s.LastRefreshedAt = &t
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
@@ -131,12 +141,16 @@ func (p *Postgres) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *s
|
||||
cur.CommunityID = &v
|
||||
}
|
||||
}
|
||||
if patch.LastRefreshedAt != nil {
|
||||
t := patch.LastRefreshedAt.UTC()
|
||||
cur.LastRefreshedAt = &t
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, err = p.pool.Exec(ctx, `
|
||||
UPDATE module_cdn_source SET source_kind=$3, url=$4, prefix_path=$5, etag=$6, refresh_interval_sec=$7,
|
||||
community_id=NULLIF($8::uuid, '00000000-0000-0000-0000-000000000000'::uuid), updated_at=now()
|
||||
community_id=NULLIF($8::uuid, '00000000-0000-0000-0000-000000000000'::uuid), last_refreshed_at=$9, updated_at=now()
|
||||
WHERE id=$1 AND module_id=$2`,
|
||||
sourceID, moduleID, cur.SourceKind, cur.URL, cur.PrefixPath, cur.Etag, nullInt32Ptr(cur.RefreshIntervalSec), uuidOrNilPtr(cur.CommunityID))
|
||||
sourceID, moduleID, cur.SourceKind, cur.URL, cur.PrefixPath, cur.Etag, nullInt32Ptr(cur.RefreshIntervalSec), uuidOrNilPtr(cur.CommunityID), nullTimePtr(cur.LastRefreshedAt))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+24
-21
@@ -83,34 +83,37 @@ type Backend interface {
|
||||
|
||||
// ModulePatch is a partial update for module.
|
||||
type ModulePatch struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
RefreshIntervalSec *int `json:"refresh_interval_sec,omitempty"`
|
||||
CronExpr *string `json:"cron_expr,omitempty"`
|
||||
DefaultCommunityID *string `json:"default_community_id,omitempty"`
|
||||
DohProfileID *string `json:"doh_profile_id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
RefreshIntervalSec *int `json:"refresh_interval_sec,omitempty"`
|
||||
CronExpr *string `json:"cron_expr,omitempty"`
|
||||
DefaultCommunityID *string `json:"default_community_id,omitempty"`
|
||||
DohProfileID *string `json:"doh_profile_id,omitempty"`
|
||||
LastRefreshedAt *time.Time `json:"-"`
|
||||
}
|
||||
|
||||
// CDNSource is a row under a CDN module.
|
||||
type CDNSource struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
ModuleID string `json:"module_id,omitempty"`
|
||||
SourceKind string `json:"source_kind"`
|
||||
URL string `json:"url"`
|
||||
PrefixPath string `json:"prefix_path,omitempty"`
|
||||
Etag string `json:"etag"`
|
||||
RefreshIntervalSec *int `json:"refresh_interval_sec"`
|
||||
CommunityID *string `json:"community_id"`
|
||||
ID string `json:"id,omitempty"`
|
||||
ModuleID string `json:"module_id,omitempty"`
|
||||
SourceKind string `json:"source_kind"`
|
||||
URL string `json:"url"`
|
||||
PrefixPath string `json:"prefix_path,omitempty"`
|
||||
Etag string `json:"etag"`
|
||||
RefreshIntervalSec *int `json:"refresh_interval_sec"`
|
||||
CommunityID *string `json:"community_id"`
|
||||
LastRefreshedAt *time.Time `json:"-"`
|
||||
}
|
||||
|
||||
type CDNSourcePatch struct {
|
||||
SourceKind *string `json:"source_kind,omitempty"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
PrefixPath *string `json:"prefix_path,omitempty"`
|
||||
Etag *string `json:"etag,omitempty"`
|
||||
RefreshIntervalSec *int `json:"refresh_interval_sec,omitempty"`
|
||||
CommunityID *string `json:"community_id,omitempty"`
|
||||
SourceKind *string `json:"source_kind,omitempty"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
PrefixPath *string `json:"prefix_path,omitempty"`
|
||||
Etag *string `json:"etag,omitempty"`
|
||||
RefreshIntervalSec *int `json:"refresh_interval_sec,omitempty"`
|
||||
CommunityID *string `json:"community_id,omitempty"`
|
||||
LastRefreshedAt *time.Time `json:"-"`
|
||||
}
|
||||
|
||||
type ASEntry struct {
|
||||
|
||||
+13
-12
@@ -38,7 +38,7 @@ type Memory struct {
|
||||
asEntries map[string]*ASEntry
|
||||
domainEnt map[string]*DomainEntry
|
||||
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
|
||||
|
||||
// DemoIDs valid after SeedDemo()
|
||||
@@ -61,17 +61,18 @@ type Tenant struct {
|
||||
}
|
||||
|
||||
type Module struct {
|
||||
ID string
|
||||
TenantID string
|
||||
Type string // AS_PREFIXES, CDN_CIDRS, DOMAINS, IP_RANGES
|
||||
Name string
|
||||
Enabled bool
|
||||
RefreshIntervalSec int // 0 = unset
|
||||
CronExpr string // optional cron for scheduler (display / future use)
|
||||
Priority int
|
||||
DefaultCommunityID *string
|
||||
DohProfileID *string
|
||||
DeletedAt *time.Time
|
||||
ID string
|
||||
TenantID string
|
||||
Type string // AS_PREFIXES, CDN_CIDRS, DOMAINS, IP_RANGES
|
||||
Name string
|
||||
Enabled bool
|
||||
RefreshIntervalSec int // 0 = unset
|
||||
CronExpr string // optional cron for scheduler (display / future use)
|
||||
Priority int
|
||||
DefaultCommunityID *string
|
||||
DohProfileID *string
|
||||
LastRefreshedAt *time.Time
|
||||
DeletedAt *time.Time
|
||||
}
|
||||
|
||||
type Revision struct {
|
||||
|
||||
@@ -32,6 +32,7 @@ func (m *Memory) CreateModule(tenantID string, in *Module) (*Module, error) {
|
||||
CronExpr: in.CronExpr,
|
||||
DefaultCommunityID: in.DefaultCommunityID,
|
||||
DohProfileID: in.DohProfileID,
|
||||
LastRefreshedAt: in.LastRefreshedAt,
|
||||
}
|
||||
m.modules[id] = mod
|
||||
return mod, nil
|
||||
@@ -78,6 +79,10 @@ func (m *Memory) UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*M
|
||||
mod.DohProfileID = &v
|
||||
}
|
||||
}
|
||||
if patch.LastRefreshedAt != nil {
|
||||
t := patch.LastRefreshedAt.UTC()
|
||||
mod.LastRefreshedAt = &t
|
||||
}
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
@@ -143,6 +148,7 @@ func (m *Memory) CreateCDNSource(tenantID, moduleID string, in *CDNSource) (*CDN
|
||||
Etag: in.Etag,
|
||||
RefreshIntervalSec: in.RefreshIntervalSec,
|
||||
CommunityID: in.CommunityID,
|
||||
LastRefreshedAt: in.LastRefreshedAt,
|
||||
}
|
||||
m.cdnSources[id] = s
|
||||
return s, nil
|
||||
@@ -184,6 +190,10 @@ func (m *Memory) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *CDN
|
||||
s.CommunityID = &v
|
||||
}
|
||||
}
|
||||
if patch.LastRefreshedAt != nil {
|
||||
t := patch.LastRefreshedAt.UTC()
|
||||
s.LastRefreshedAt = &t
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE module_cdn_source
|
||||
DROP COLUMN last_refreshed_at;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE module_cdn_source
|
||||
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 TIMESTAMPTZ NULL;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE module_cdn_source
|
||||
DROP COLUMN last_refreshed_at;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE module_cdn_source
|
||||
ADD COLUMN last_refreshed_at TEXT;
|
||||
@@ -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;
|
||||
default_community_id: string | null;
|
||||
doh_profile_id: string | null;
|
||||
last_refreshed_at: string | null;
|
||||
};
|
||||
export type ModulesResponse = Page<ModuleRow>;
|
||||
|
||||
@@ -71,6 +72,7 @@ export type CdnSource = {
|
||||
prefix_path: string;
|
||||
community_id: string | null;
|
||||
refresh_interval_sec: number | null;
|
||||
last_refreshed_at: string | null;
|
||||
};
|
||||
export type CdnSourceCreate = {
|
||||
url: string;
|
||||
|
||||
@@ -136,6 +136,13 @@
|
||||
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) {
|
||||
const next = new Set(selectedModuleIds);
|
||||
if (next.has(id)) next.delete(id);
|
||||
@@ -232,6 +239,7 @@
|
||||
<TableHead>Тип</TableHead>
|
||||
<TableHead>Приоритет</TableHead>
|
||||
<TableHead>Интервал</TableHead>
|
||||
<TableHead>Последнее обновление</TableHead>
|
||||
<TableHead>Статус</TableHead>
|
||||
<TableHead class="w-16"></TableHead>
|
||||
</TableRow>
|
||||
@@ -255,6 +263,9 @@
|
||||
<TableCell class="text-muted-foreground font-mono text-xs">
|
||||
{moduleIntervalLabel(m)}
|
||||
</TableCell>
|
||||
<TableCell class="text-muted-foreground text-sm whitespace-nowrap">
|
||||
{formatDateTime(m.last_refreshed_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{#if m.enabled}
|
||||
<Badge variant="default" class="text-xs">вкл</Badge>
|
||||
@@ -270,7 +281,7 @@
|
||||
</TableRow>
|
||||
{:else}
|
||||
<TableRow>
|
||||
<TableCell colspan={7} class="text-muted-foreground text-center py-8">
|
||||
<TableCell colspan={8} class="text-muted-foreground text-center py-8">
|
||||
{loading ? 'Загрузка…' : 'Нет модулей. Создайте первый.'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -762,6 +762,13 @@
|
||||
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(() => {
|
||||
if (!mod) return 'entries';
|
||||
switch (mod.type) {
|
||||
@@ -831,7 +838,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<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>
|
||||
@@ -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="font-semibold text-sm break-all">{communityLabel(mod.default_community_id)}</p>
|
||||
</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>
|
||||
|
||||
<Card>
|
||||
@@ -1058,6 +1069,7 @@
|
||||
<TableHead>Тип</TableHead>
|
||||
<TableHead>Community</TableHead>
|
||||
<TableHead>Интервал</TableHead>
|
||||
<TableHead>Последнее обновление</TableHead>
|
||||
<TableHead class="w-20"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -1089,6 +1101,9 @@
|
||||
? `${src.refresh_interval_sec}с`
|
||||
: '—'}
|
||||
</TableCell>
|
||||
<TableCell class="text-muted-foreground text-sm whitespace-nowrap">
|
||||
{formatDateTime(src.last_refreshed_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex gap-1">
|
||||
<Button variant="ghost" size="icon-sm" onclick={() => openCdnEdit(src)}><Pencil class="size-3.5" /></Button>
|
||||
@@ -1098,7 +1113,7 @@
|
||||
</TableRow>
|
||||
{:else}
|
||||
<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>
|
||||
{/each}
|
||||
</TableBody>
|
||||
|
||||
Reference in new issue
Block a user