diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43bf721..f4a4664 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,14 @@
The release notes on GitHub are taken from this file: the section whose
heading is the version number.
+## 2.5.2
+
+- The sponsor banner is in the sidebar, above the language and theme; the strip at the top of the page is gone.
+- The version is in the page footer; the collection status is at the foot of the overview.
+- Rooms and racks is called Device locations and is a tab of the inventory page.
+- Deleting one interface's data is removed.
+- Deleting data no longer leaves the last minute, or summaries still being built, to be written back afterwards.
+
## 2.5.1
- Long ranges on large installations no longer run out of database memory, and a device or an interface over days is read from summaries of its own (built once in the background for the data already stored).
diff --git a/cmd/traffic66/main.go b/cmd/traffic66/main.go
index c36bcaf..679c5b7 100644
--- a/cmd/traffic66/main.go
+++ b/cmd/traffic66/main.go
@@ -55,7 +55,7 @@ var version = ""
// CHANGELOG.md. TestDevVersionMatchesChangelog keeps the two in step: the
// default used to be a hardcoded 0.1.0-dev, and because nothing checked it
// it stayed that way for two major versions.
-const devVersion = "2.5.1"
+const devVersion = "2.5.2"
func init() {
if version == "" {
diff --git a/docs/MANUAL.md b/docs/MANUAL.md
index c0b9b8f..284fc94 100644
--- a/docs/MANUAL.md
+++ b/docs/MANUAL.md
@@ -496,21 +496,14 @@ The data directory holds `raw/` (detail, one file per hour),
`traffic66.duckdb` (summaries and counters), `password`, `inventory.txt`,
`license.json`, your logo and databases. Back up by stopping traffic66 and
copying it; upgrade by replacing the program file. **Data cleanup** deletes
-data older than 7–120 days, or all of it. A registered copy can also delete
-one interface's data on its own — the flow records that went in or out of it
-and its counter samples — for when a link is decommissioned or its data must
-not be kept. A flow record names both the interface it came in on and the
-one it went out by, so those flows leave the other interface's view too;
-they are the same records. The hourly and daily summaries record traffic by
-application, host and dimension, never by interface, so they keep their
-totals.
-
-### Rooms and racks
-
-A picture of where the flows come from: for every device that sends flows,
+data older than 7–120 days, or all of it.
+
+### Device locations
+
+A tab of the inventory page. A picture of where the flows come from: for every device that sends flows,
the room and rack it stands in, and for each of its ports the carrier and
circuit ID it goes out to. Devices and ports appear by themselves from the
-flows; click ✎ to type in the room, rack, carrier and circuit. It is filled
+flows; click Edit to type in the device's room and rack and every port's carrier and circuit in one form. It is filled
in once and kept in `inventory.txt` beside the names
(`device 192.0.2.1 Core router room=Room_1 rack=A03`,
`iface 192.0.2.1 3 Uplink isp=China_Telecom circuit=SH-12345`; a space is
diff --git a/internal/api/cleanup.go b/internal/api/cleanup.go
index a222559..7c2fcb8 100644
--- a/internal/api/cleanup.go
+++ b/internal/api/cleanup.go
@@ -53,6 +53,9 @@ func (s *Server) postCleanup(w http.ResponseWriter, r *http.Request) {
fail(w, errors.New("days must be 0 or more"))
return
}
+ if s.Pipe != nil {
+ s.Pipe.Forget(before)
+ }
disk := s.Store.Usage().DiskBytes
done, err := s.Store.PurgeBefore(before)
if err != nil {
diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go
index e1ee88a..542ff00 100644
--- a/internal/pipeline/pipeline.go
+++ b/internal/pipeline/pipeline.go
@@ -9,6 +9,7 @@ import (
"log"
"math"
"net/netip"
+ "slices"
"strconv"
"sync"
"sync/atomic"
@@ -218,6 +219,32 @@ func (p *Pipeline) FlushRows() {
p.Rows.Add(uint64(len(b)))
}
+// Forget drops what is held in memory from before t (everything for a
+// zero t): the rows not written yet and the summaries of the minute being
+// added up. Deleting data calls it first, or up to a minute of what was
+// deleted would be written back a moment later.
+func (p *Pipeline) Forget(t time.Time) {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ old := func(ts time.Time) bool { return t.IsZero() || ts.Before(t) }
+ p.batch = slices.DeleteFunc(p.batch, func(r store.Row) bool { return old(r.TS) })
+ for k := range p.rTS {
+ if old(k.TS) {
+ delete(p.rTS, k)
+ }
+ }
+ for k := range p.rHost {
+ if old(k.TS) {
+ delete(p.rHost, k)
+ }
+ }
+ for k := range p.rDim {
+ if old(k.TS) {
+ delete(p.rDim, k)
+ }
+ }
+}
+
// FlushRollups writes accumulated rollup rows.
func (p *Pipeline) FlushRollups() {
p.mu.Lock()
diff --git a/internal/pipeline/pipeline_test.go b/internal/pipeline/pipeline_test.go
index 3661f94..c280e54 100644
--- a/internal/pipeline/pipeline_test.go
+++ b/internal/pipeline/pipeline_test.go
@@ -608,3 +608,41 @@ func TestRingsAndSegmentCountries(t *testing.T) {
t.Error("three-letter country accepted")
}
}
+
+// Deleting data drops what is still held in memory from before the cut,
+// so it is not written back a moment after the delete.
+func TestForget(t *testing.T) {
+ st, err := store.Open(store.Options{Dir: t.TempDir()})
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer st.Close()
+ p := New(Config{}, st, enrich.NewInventory(), enrich.NewASNDB(), enrich.NewThreats())
+ now := time.Now().UTC().Truncate(time.Minute)
+ rec := func(at time.Time) flow.Record {
+ return flow.Record{Start: at, End: at, Src: netip.MustParseAddr("10.1.1.1"), Dst: netip.MustParseAddr("198.51.100.9"),
+ SrcPort: 50000, DstPort: 443, Proto: 6, Bytes: 1000, Packets: 1, Mult: 1, SamplingKnown: true,
+ Exporter: netip.MustParseAddr("10.0.0.1"), Source: flow.SrcIPFIX}
+ }
+ p.Ingest([]flow.Record{rec(now.Add(-3 * time.Hour)), rec(now.Add(-time.Minute))})
+ p.Forget(now.Add(-time.Hour))
+ p.FlushRows()
+ p.FlushRollups()
+ for _, q := range []store.Query{
+ {From: now.Add(-4 * time.Hour), To: now.Add(-2 * time.Hour)}, // the records
+ {From: now.Add(-12 * time.Hour), To: now.Add(-2 * time.Hour).Truncate(time.Hour)}, // the summaries
+ {From: now.Add(-12 * time.Hour), To: now.Add(time.Minute), Filters: []store.Filter{{Field: "client", Value: "10.1.1.1"}}}, // what is left
+ } {
+ tot, err := st.Totals(q)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := uint64(0)
+ if len(q.Filters) > 0 {
+ want = 1 // the recent flow stays
+ }
+ if tot.Flows != want {
+ t.Errorf("%v..%v: %d flows, want %d", q.From, q.To, tot.Flows, want)
+ }
+ }
+}
diff --git a/internal/store/purge.go b/internal/store/purge.go
index 93dff1f..77dd861 100644
--- a/internal/store/purge.go
+++ b/internal/store/purge.go
@@ -72,6 +72,20 @@ func (s *Store) PurgeSizeBefore(before time.Time) PurgeSize {
func (s *Store) PurgeBefore(before time.Time) (PurgeSize, error) {
all := before.IsZero()
var done PurgeSize
+ // A build of the summaries still running would write back what is
+ // deleted here. It is stopped first and, where something is left to
+ // summarise, started again afterwards; deleting everything leaves
+ // nothing to summarise.
+ building := s.stopViews()
+ defer func() {
+ switch {
+ case all:
+ s.SetMeta(viewsDone, time.Now().UTC().Format(time.RFC3339))
+ s.viewsReady.Store(true)
+ case building:
+ s.startViews()
+ }
+ }()
s.segMu.RLock()
var old []segment
for _, g := range s.segCache {
diff --git a/internal/store/store.go b/internal/store/store.go
index 1e071f4..0b62162 100644
--- a/internal/store/store.go
+++ b/internal/store/store.go
@@ -118,8 +118,8 @@ type Store struct {
viewsReady atomic.Bool
closing atomic.Bool
bg sync.WaitGroup
- bgCtx context.Context
- bgStop context.CancelFunc
+ vmu sync.Mutex
+ vStop context.CancelFunc // stops the build in progress, nil when none
dsMu sync.Mutex // guards the cached start of the data
dsAt time.Time // when dsTS was read
@@ -192,7 +192,6 @@ func Open(opt Options) (*Store, error) {
return nil, fmt.Errorf("cannot open the database in %s: %w", opt.Dir, err)
}
s := &Store{opt: opt, conn: c, DB: sql.OpenDB(c)}
- s.bgCtx, s.bgStop = context.WithCancel(context.Background())
for _, q := range append(schema, viewSchema...) {
if _, err := s.DB.Exec(q); err != nil {
return nil, fmt.Errorf("schema: %w", err)
@@ -222,8 +221,7 @@ func Open(opt Options) (*Store, error) {
}
s.viewsReady.Store(true)
default:
- s.bg.Add(1)
- go s.buildViews()
+ s.startViews()
}
log.Printf("store: %s, DuckDB memory limit %d MB, %d threads", opt.Dir, memMB, opt.Threads)
return s, nil
@@ -543,8 +541,7 @@ func (s *Store) Usage() Usage {
// Close flushes and closes everything.
func (s *Store) Close() error {
s.closing.Store(true)
- s.bgStop()
- s.bg.Wait()
+ s.stopViews()
s.wmu.Lock()
defer s.wmu.Unlock()
for _, a := range []*duckdb.Appender{s.appHot, s.appTS, s.appHost, s.appDim, s.appIfc} {
diff --git a/internal/store/views.go b/internal/store/views.go
index a585502..13edf8e 100644
--- a/internal/store/views.go
+++ b/internal/store/views.go
@@ -1,6 +1,7 @@
package store
import (
+ "context"
"database/sql"
"fmt"
"log"
@@ -245,8 +246,13 @@ func sealViews(tx *sql.Tx, boundary time.Time) error {
// existed, once per data directory, a file at a time so the memory it
// needs is a file's worth. Until it is done the pages read those ranges
// from the flow records as before.
-func (s *Store) buildViews() {
- defer s.bg.Done()
+func (s *Store) buildViews(ctx context.Context) {
+ defer func() {
+ s.vmu.Lock()
+ s.vStop = nil
+ s.vmu.Unlock()
+ s.bg.Done()
+ }()
// Start over: an earlier run may have stopped half way. The files
// sealed from here on are summarised by the seal itself, so the list
// is taken, and the tables emptied, with sealing held off.
@@ -264,23 +270,23 @@ func (s *Store) buildViews() {
s.wmu.Unlock()
t0 := time.Now()
for i, g := range segs {
- if s.closing.Load() {
+ if ctx.Err() != nil {
return
}
src := "(SELECT * FROM read_parquet('" + sqlPath(g.path) + "'))"
- tx, err := s.DB.BeginTx(s.bgCtx, nil)
+ tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
log.Printf("store: summaries: %v", err)
return
}
for _, q := range viewInserts(src) {
- if _, err = tx.ExecContext(s.bgCtx, q); err != nil {
+ if _, err = tx.ExecContext(ctx, q); err != nil {
break
}
}
if err != nil {
tx.Rollback()
- if !s.closing.Load() {
+ if ctx.Err() == nil {
log.Printf("store: summaries of %s: %v", g.path, err)
}
return
@@ -302,3 +308,27 @@ func (s *Store) buildViews() {
log.Printf("store: summarised %d flow files by device and interface in %s", len(segs), time.Since(t0).Round(time.Second))
}
}
+
+// startViews builds the summaries of the flow files in the background.
+func (s *Store) startViews() {
+ ctx, stop := context.WithCancel(context.Background())
+ s.vmu.Lock()
+ s.vStop = stop
+ s.vmu.Unlock()
+ s.bg.Add(1)
+ go s.buildViews(ctx)
+}
+
+// stopViews stops a build in progress, waits for it, and reports whether
+// there was one.
+func (s *Store) stopViews() bool {
+ s.vmu.Lock()
+ stop := s.vStop
+ s.vmu.Unlock()
+ if stop == nil {
+ return false
+ }
+ stop()
+ s.bg.Wait()
+ return true
+}
diff --git a/internal/web/static/app.css b/internal/web/static/app.css
index a05af34..3d563c1 100644
--- a/internal/web/static/app.css
+++ b/internal/web/static/app.css
@@ -54,7 +54,7 @@ button,input,select,textarea{font:inherit;color:inherit}
[hidden]{display:none!important}
.app{display:grid;grid-template-columns:236px 1fr;min-height:100vh}
-aside{background:var(--side);border-inline-end:1px solid var(--side-line);display:flex;flex-direction:column;position:sticky;top:0;height:calc(100vh - var(--sp-h, 0px))}
+aside{background:var(--side);border-inline-end:1px solid var(--side-line);display:flex;flex-direction:column;position:sticky;top:0;height:100vh}
.brand{padding:12px 14px 10px;flex-wrap:wrap;position:relative;font-weight:700;font-size:18px;letter-spacing:-.01em;border-bottom:1px solid var(--side-line);display:flex;align-items:center;gap:8px}
/* the logo fills the sidebar's width (236px less the brand padding = 208px)
and keeps its aspect ratio; 72px tall at most. Recommended upload:
@@ -75,28 +75,26 @@ nav.dash button[aria-current="page"]::before{content:"";position:absolute;inset-
@keyframes navbar{from{transform:scaleY(0)}to{transform:scaleY(1)}}
.ni{width:18px;height:18px;flex:none;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;opacity:.85}
nav.dash button[aria-current="page"] .ni{opacity:1}
-.ver{display:flex;justify-content:space-between;gap:8px;padding:6px 16px 8px;font-size:11.5px;color:var(--ink-3);border-bottom:1px solid var(--side-line);font-variant-numeric:tabular-nums}
+/* the version and the clock, in the page footer under the licence line */
+.ver{display:flex;justify-content:center;gap:14px;margin-top:4px;font-size:11.5px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.badge{min-width:18px;padding:0 5px;border-radius:9px;background:var(--warn);color:#1b2432;font-size:11px;line-height:18px;text-align:center;font-weight:700}
-.self{border-top:1px solid var(--side-line);padding:12px 18px;font-size:12.5px;color:var(--ink-2);display:grid;grid-template-columns:auto 1fr;gap:3px 10px}
-.self .k{color:var(--ink-3)}.self .val{text-align:end;font-weight:600;color:var(--ink)}
-.self .live{grid-column:1/-1;display:flex;align-items:center;gap:7px;font-weight:600;margin-bottom:4px}
+/* how collection is going, at the foot of the overview */
+.self{display:flex;flex-wrap:wrap;align-items:center;gap:10px 28px;font-size:13px;color:var(--ink-2)}
+.self .it{display:flex;flex-direction:column;gap:1px}
+.self .k{color:var(--ink-3);font-size:12px}.self .val{font-weight:600;color:var(--ink);font-size:15px}
+.self .live{display:flex;align-items:center;gap:7px;font-weight:600}
.self .live i{width:8px;height:8px;border-radius:50%;background:currentColor}
.self .live.ok{color:var(--good)}.self .live.idle{color:var(--ink-3)}
.foot{display:flex;flex-direction:column;gap:8px;padding:0 16px 14px}
-/* The sponsor strip of an unregistered installation: the full width of the
- browser at the top of the page, above the sidebar and the content, and it
- scrolls away with the page rather than taking a band of every screen.
-
- One advertiser at a time: a long banner alone, a small 728x90 one three
- times side by side (sponsor.js decides, and sets the row height). The
- sidebar's own height is measured against the strip (--sp-h) instead of
- a fixed 100vh. */
-.sponsor{grid-column:1/-1;background:var(--side);border-bottom:1px solid var(--side-line)}
+/* The sponsor slot of an unregistered installation: in the sidebar, above
+ the language and theme, one small banner at a time at the sidebar's
+ width (sponsor.js decides, and sets the height). */
+.sponsor{border-top:1px solid var(--side-line);padding:8px 12px 4px}
.sponsor[hidden]{display:none}
-.sponsor .sp-h{font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);padding:3px 12px 2px}
-.sponsor .sp-text{margin:0 12px 8px;border:1px dashed var(--line-2);border-radius:6px;padding:6px 8px;font-size:11.5px;line-height:1.35;color:var(--ink-3);overflow-wrap:anywhere}
+.sponsor .sp-h{font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);padding:0 0 3px}
+.sponsor .sp-text{margin:0 0 6px;border:1px dashed var(--line-2);border-radius:6px;padding:6px 8px;font-size:11.5px;line-height:1.35;color:var(--ink-3);overflow-wrap:anywhere}
.sponsor .sp-text[hidden],.spc[hidden]{display:none}
-.spc-strip{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);gap:1px;background:var(--side-line)}
+.spc-strip{display:grid;border-radius:4px;overflow:hidden}
.spc-cell{position:relative;overflow:hidden;background:var(--surface)}
.spc-cell>a,.spc-cell>div{display:block;width:100%;height:100%}
.spc-cell img{width:100%;height:100%;object-fit:cover;display:block}
@@ -151,7 +149,7 @@ main{padding:16px 20px 32px;min-width:0}
.ph .sub{font-size:12.5px;color:var(--ink-3)}
.c12{grid-column:span 12}.c8{grid-column:span 8}.c6{grid-column:span 6}.c4{grid-column:span 4}
@media (max-width:1180px){.c8,.c4{grid-column:span 12}}
-@media (max-width:820px){.app{grid-template-columns:minmax(0,1fr)}.main{min-width:0}aside{position:static;height:auto}nav.dash{display:flex;overflow-x:auto;padding:6px}nav.dash hr{display:none}nav.dash button{white-space:nowrap;width:auto}.ver{display:none}.self{display:none}.c6{grid-column:span 12}main{padding:12px}.row1,.row2{padding-inline:12px}.range{max-width:100%;overflow-x:auto;margin-inline-start:0}}
+@media (max-width:820px){.app{grid-template-columns:minmax(0,1fr)}.main{min-width:0}aside{position:static;height:auto}nav.dash{display:flex;overflow-x:auto;padding:6px}nav.dash hr{display:none}nav.dash button{white-space:nowrap;width:auto}.c6{grid-column:span 12}main{padding:12px}.row1,.row2{padding-inline:12px}.range{max-width:100%;overflow-x:auto;margin-inline-start:0}}
.kpis{display:grid;grid-template-columns:repeat(5,1fr)}
.kpi{padding:4px 18px;border-inline-start:1px solid var(--line);min-width:0}
@@ -484,17 +482,6 @@ dialog.aedit h2{margin:0 0 14px;font-size:16px}
.acctinline input{min-width:0;flex:1 1 140px}
body.mustchange #nav,body.mustchange #acct,body.mustchange .ifsel{display:none}
-/* Licence badge: a block under the logo and above the version, rather
- than a pill in a toolbar whose contents change from view to view. */
-.licbadge{display:block;margin:8px 14px;text-align:center;font-size:12.5px;font-weight:600;text-decoration:none;
- padding:3px 9px;border-radius:999px;border:1px solid var(--line);color:var(--muted);
- background:var(--surface);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-/* who the licence is made out to, under the word: a long company name is
- cut rather than widening the sidebar */
-.licbadge .who{display:block;font-weight:500;font-size:11.5px;opacity:.85;overflow:hidden;text-overflow:ellipsis}
-.licbadge:hover{border-color:var(--accent);color:var(--accent)}
-.licbadge.unregistered,.licbadge.expired{border-color:var(--warn,#b9770e);color:var(--warn,#b9770e)}
-
/* The evaluation mark: drawn over the view, light enough that nothing
underneath becomes unreadable, and kept when the page is printed. */
body.marked .view.on{position:relative}
@@ -587,3 +574,6 @@ body.marked .view.on::after{
dialog.embdlg{width:min(620px,96vw)}
.embdlg textarea{font:inherit;font-size:13px;font-family:ui-monospace,monospace;padding:5px 7px;border:1px solid var(--line-2);border-radius:6px;background:var(--surface);color:var(--ink);resize:vertical}
.afhint{font-size:11.5px;color:var(--ink-3)}
+
+/* the tabs of the inventory page: the inventory and device locations */
+.invtabs{display:inline-flex;margin:0 0 14px}
diff --git a/internal/web/static/app.js b/internal/web/static/app.js
index b10e2e6..dba690e 100644
--- a/internal/web/static/app.js
+++ b/internal/web/static/app.js
@@ -383,14 +383,11 @@ setInterval(tickClock, 1000);
const IFV_VIEWS = new Set(['overview', 'topn', 'traffic', 'sankey', 'geo', 'records', 'detail']);
const IFV_DATA = new Set(['overview', 'topn', 'series', 'sankey', 'records', 'rings', 'geolines']);
const ifScoped = path => state.ifv && state.ifv !== 'all' && IFV_VIEWS.has(state.v) && IFV_DATA.has(path) && !inSB();
-// What the licence grants, for the pages that have to grey a
-// registered-only option out. The server checks it too and is the only
-// thing that decides: this keeps the page from offering what would be
-// refused.
+// What the licence grants. The server checks it too and is the only thing
+// that decides.
let lic = null;
let waiting = null; // what the collector is still waiting for, from /api/status
let setup = null; // sources, UDP ports and addresses, for the first-run guide
-const licOK = f => !!lic && lic.registered && (lic.features || []).includes(f);
let ifList = null, ifListKey = '';
// the interfaces of the range (busiest first) and the default one, cached a minute
@@ -1003,7 +1000,9 @@ views.overview = async (el) => {
${panel('c6', t('ov.top_services'), t('ov.top_services_sub'), `
${t('col.server')}
${t('col.service')}
${t('col.traffic')}
${t('col.share')}
${sv.map((r, i) => `
${i + 1}
${ipCell(r.key)}
${V('port', r.key2, r.key2)} ${esc(r.extra)}
${fmtBytes(r.wire)}
${bar(r.wire, smax)}
`).join('') || `
${emptyText()}
`}
`)}
${fd ? findingsPanel(fd) : ''}
+ ${offlineMode || inSB() ? '' : panel('c12', t('self.title'), '', '')}
`;
+ fillSelf();
const dirWord = od && t(od === 'inbound' ? 'if.in' : 'if.out');
tsChart($('#chStack'), {times: series.times, areas, lines, settledTo: series.settled_to, dataFrom: series.data_from,
empty: od && viaIf ? esc(t('ov.dir_empty', {if: ifSel ? ifName(ifSel) : ifk, d: /^[a-z]/i.test(dirWord) ? dirWord.toLocaleLowerCase(LANG) : dirWord})) : ''});
@@ -1688,73 +1687,9 @@ views.cleanup = async (el) => {
loadStatus(); render();
});
if (clDone) { const m = $('#clMsg'); m.style.color = 'var(--good)'; m.textContent = clDone; clDone = ''; }
- await ifaceCleanup(el);
};
let clDone = '';
-// Deleting one interface's data (registered only). The summaries hold
-// traffic by application, host and dimension, never by interface, so they
-// cannot have one interface subtracted from them; the panel says so rather
-// than letting anyone believe the interface is gone from every page.
-async function ifaceCleanup(el) {
- const on = licOK('purge-iface');
- const ages = [0, ...cleanupDaysUI];
- const ageOpt = d => ``;
- const wrap = document.createElement('div');
- wrap.className = 'grid';
- wrap.innerHTML = panel('c12' + (on ? '' : ' locked'), t('cl.if_title'), on ? t('cl.if_sub') : t('cl.reg_only'),
- `
`;
const uniq = k => [...new Set(devs.map(x => x[k]).filter(Boolean))].sort();
const carriers = [...new Set(ports.map(f => f.isp).filter(Boolean))].sort();
el.querySelectorAll('[data-rkd]').forEach(b => b.onclick = async () => {
@@ -2446,9 +2381,14 @@ views.sources = async (el) => {
// recording a rack, and it is the one page there that an unregistered
// installation keeps: a flow analyser that cannot tell you which address is
// which is of little use, and that is not a feature to sell.
+// The inventory page has two tabs: the inventory itself and where the
+// devices stand.
+const invTabs = cur => `
@@ -2663,9 +2603,11 @@ async function render(push) {
renderFilters();
renderFbar();
writeHash(push);
- $('#title').textContent = t('nav.' + state.v); // the detail view refines it
+ // device locations is a tab of the inventory page, and lights its entry
+ const navV = state.v === 'racks' ? 'names' : state.v;
+ $('#title').textContent = t('nav.' + navV); // the detail view refines it
document.title = t('nav.' + state.v) + ' · traffic66';
- document.querySelectorAll('#nav button').forEach(b => b.dataset.v === state.v ? b.setAttribute('aria-current', 'page') : b.removeAttribute('aria-current'));
+ document.querySelectorAll('#nav button').forEach(b => b.dataset.v === navV ? b.setAttribute('aria-current', 'page') : b.removeAttribute('aria-current'));
renderRange();
await renderIfsel();
writeHash(false);
@@ -2710,36 +2652,38 @@ async function loadStatus() {
// registered to whom, and as what: a licence bought by one person is
// not a company's, and the footer is where a screenshot shows it
const as = L.registered ? ` ${esc(licenseeText(L))}` : '';
- $('#lic').innerHTML = `${esc(head)}${as}${tail ? tail : ' '}${id}`;
- $('#lic').className = 'lic ' + L.kind; $('#lic').hidden = false;
+ $('#licText').innerHTML = `${esc(head)}${as}${tail ? tail : ' '}${id}`;
+ $('#lic').className = 'lic ' + L.kind; $('#licText').hidden = false;
marked = !!L.watermark;
markNotice = L.notice || '';
applyMark();
- const b = $('#licBadge');
- const bt = L.registered ? (L.days < 0 ? t('lic.badge_ok') : t('lic.badge_days', {n: L.days})) : t('lic.badge_get');
- // the badge carries the name under the word, so the sidebar says who
- // this installation belongs to and not only that it is licensed
- b.innerHTML = esc(bt) + (L.registered && L.customer ? `${esc(L.customer)}` : '');
- b.className = 'licbadge ' + L.kind;
- b.title = head + (L.registered ? ' · ' + licenseeText(L) : '');
- b.hidden = false;
}
$('#verNo').textContent = 'v' + s.version + (s.build && s.build !== 'official' ? ' · ' + s.build : '');
if (s.now) clockSkew = s.now - Date.now();
- $('#self').hidden = offlineMode; // nothing is collected
- const live = s.records_per_sec > 0.2;
- $('#self').innerHTML = `
${t(live ? 'self.receiving' : 'self.idle')}
- ${t('self.ingest')}${esc(t('self.per_sec', {n: nf(s.records_per_sec, s.records_per_sec < 10 ? 1 : 0)}))}
- ${t('self.dropped')}${nf(s.dropped)}
- ${t('self.disk')}${fmtBytes(s.disk_bytes)}
- ${t('self.free')} s.disk_free ? 'style="color:var(--crit)"' : ''} title="${esc(s.disk_need >= 0 ? t('self.need', {d: nf(s.retention_days), n: fmtBytes(s.disk_need)}) : t('self.need_later', {d: nf(s.retention_days)}))}">${fmtBytes(s.disk_free)}
- ${s.write_errors ? `!${nf(s.write_errors)}` : ''}`;
+ selfStatus = s;
+ fillSelf();
$('#srcBadge').hidden = !s.source_warnings; $('#srcBadge').textContent = s.source_warnings;
$('#findBadge').hidden = !s.findings_open; $('#findBadge').textContent = s.findings_open;
} catch (e) {}
}
+// How collection is going, at the foot of the overview: whether records
+// are arriving, how fast, what was dropped and how the disk stands.
+let selfStatus = null;
+function fillSelf() {
+ const el = $('#self'), s = selfStatus;
+ if (!el || !s) return;
+ const live = s.records_per_sec > 0.2;
+ const it = (k, v, attrs = '') => `
${k}${v}
`;
+ el.innerHTML = `
${t(live ? 'self.receiving' : 'self.idle')}
+ ${it(t('self.ingest'), esc(t('self.per_sec', {n: nf(s.records_per_sec, s.records_per_sec < 10 ? 1 : 0)})))}
+ ${it(t('self.dropped'), nf(s.dropped), s.dropped ? 'style="color:var(--crit)"' : '')}
+ ${it(t('self.disk'), fmtBytes(s.disk_bytes))}
+ ${it(t('self.free'), fmtBytes(s.disk_free), `${s.disk_need > s.disk_free ? 'style="color:var(--crit)"' : ''} title="${esc(s.disk_need >= 0 ? t('self.need', {d: nf(s.retention_days), n: fmtBytes(s.disk_need)}) : t('self.need_later', {d: nf(s.retention_days)}))}"`)}
+ ${s.write_errors ? it('!', nf(s.write_errors), `style="color:var(--crit)" title="${esc(s.last_error)}"`) : ''}`;
+}
+
function showLogin() {
$('#app').hidden = true; $('#login').hidden = false;
$('#loginForm [name=user]').focus();
diff --git a/internal/web/static/i18n/ar.json b/internal/web/static/i18n/ar.json
index 717e2ee..0e4e425 100644
--- a/internal/web/static/i18n/ar.json
+++ b/internal/web/static/i18n/ar.json
@@ -9,6 +9,7 @@
"nav.ifaces": "مطابقة الواجهات",
"nav.sources": "الإعدادات",
"self.receiving": "الاستقبال جارٍ",
+"self.title": "حالة الجمع",
"self.idle": "لا تصل بيانات",
"self.ingest": "الوارد",
"self.per_sec": "{n} تدفق/ث",
@@ -511,23 +512,7 @@
"cl.confirm_all": "حذف كل البيانات: السجلات والملخصات والعدادات والاكتشافات؟",
"cl.confirm_all2": "هل تريد حقاً حذف كل شيء؟ ستبقى الصفحات فارغة حتى تصل تدفقات جديدة.",
"cl.done": "حُذف {n} سجل تدفق وتحررت {s}.",
-"cl.reg_only": "للنسخة المسجَّلة فقط",
-"cl.if_title": "حذف بيانات واجهة واحدة",
-"cl.if_sub": "عندما يجب إزالة حركة وصلة واحدة",
-"cl.if_what": "يحذف سجلات التدفق التي دخلت أو خرجت عبر الواجهة المختارة، مع عيّنات عدّاداتها. أما الملخصات الساعية واليومية فتسجّل الحركة حسب التطبيق والمضيف والبُعد، لا حسب الواجهة، فتبقى مجاميعها كما هي: ومخطط مدة طويلة ما زال يحسب نصيب هذه الواجهة.",
-"cl.if_both": "يحمل سجل التدفق اسم الواجهة التي دخل منها والواجهة التي خرج عبرها، لذا فحذف سجلات هذه الواجهة يُخرج تلك الحركة من عرض الواجهة الأخرى أيضًا — فهي السجلات نفسها.",
-"cl.if_any_age": "كل بياناتها",
-"cl.if_pick": "الواجهة",
-"cl.if_none": "لا توجد واجهات مسجَّلة",
-"cl.if_locked": "الحذف حسب الواجهة يتطلب نسخة مسجَّلة.",
-"cl.if_estimating": "يجري حساب ما سيُحذف…",
-"cl.if_counters": "{n} عيّنة عدّاد",
-"cl.if_frees": "يحرر نحو {s}",
-"cl.if_keep": "{n} صفًا من الملخص الساعي تحفظ مجاميعها",
-"cl.if_confirm": "حذف كل البيانات المحفوظة لـ {i}؟ لا يمكن التراجع.",
-"cl.if_confirm_days": "حذف بيانات {i} الأقدم من {n} يومًا؟ لا يمكن التراجع.",
-"cl.if_done": "حُذف {n} سجل تدفق و{c} عيّنة عدّاد من {i}، وتحررت {s}.",
-"nav.racks": "المواقع والخزانات",
+"nav.racks": "مواقع الأجهزة",
"nav.devices": "الأجهزة والمنافذ",
"nm.k_host": "مضيف",
"nm.k_net": "شبكة",
@@ -575,9 +560,6 @@
"lic.exp": "انتهى ترخيص {c} في {d}.",
"lic.mark": "تحمل التقارير والرسوم علامة نسخة تقييمية.",
"lic.perp": "مُرخَّص لـ {c}",
-"lic.badge_get": "غير مُسجَّل",
-"lic.badge_days": "مُرخَّص · {n} يوم",
-"lic.badge_ok": "مُرخَّص",
"mark.text": "نسخة تقييمية",
"if.bw": "عرض نطاق الواجهة",
"rec.live": "يُحدَّث كل 5 ثوانٍ",
diff --git a/internal/web/static/i18n/bn.json b/internal/web/static/i18n/bn.json
index 7a0c40e..2519e6c 100644
--- a/internal/web/static/i18n/bn.json
+++ b/internal/web/static/i18n/bn.json
@@ -9,6 +9,7 @@
"nav.ifaces": "ইন্টারফেস মিলানো",
"nav.sources": "সেটিংস",
"self.receiving": "ডেটা আসছে",
+"self.title": "সংগ্রহের অবস্থা",
"self.idle": "কোনো ডেটা আসছে না",
"self.ingest": "আগত",
"self.per_sec": "{n} ফ্লো/সে.",
@@ -511,23 +512,7 @@
"cl.confirm_all": "সব ডেটা মুছবেন: রেকর্ড, সারাংশ, কাউন্টার ও ফলাফল?",
"cl.confirm_all2": "সত্যিই সব মুছবেন? নতুন ফ্লো না আসা পর্যন্ত সব পেজ খালি থাকবে।",
"cl.done": "{n}টি ফ্লো রেকর্ড মোছা হয়েছে, {s} খালি হয়েছে।",
-"cl.reg_only": "শুধু নিবন্ধিত সংস্করণে",
-"cl.if_title": "একটি ইন্টারফেসের ডেটা মুছুন",
-"cl.if_sub": "যখন একটি লিঙ্কের ট্রাফিক সরাতে হয়",
-"cl.if_what": "বেছে নেওয়া ইন্টারফেস দিয়ে ঢোকা বা বেরোনো ফ্লো রেকর্ড এবং তার কাউন্টার নমুনা মুছে দেয়। ঘণ্টা ও দিনভিত্তিক সারসংক্ষেপ ট্রাফিক রাখে অ্যাপ্লিকেশন, হোস্ট ও ডাইমেনশন অনুযায়ী, ইন্টারফেস অনুযায়ী নয়, তাই তাদের মোট অপরিবর্তিত থাকে: দীর্ঘ সময়ের চার্টে এই ইন্টারফেসের অংশ এখনও গণনা হয়।",
-"cl.if_both": "প্রতিটি ফ্লো রেকর্ডে ঢোকার ও বেরোনোর দুটি ইন্টারফেসই লেখা থাকে। তাই এই ইন্টারফেসের রেকর্ড মুছলে সেই ট্রাফিক অন্য ইন্টারফেসের দৃশ্য থেকেও চলে যায় — সেগুলি একই রেকর্ড।",
-"cl.if_any_age": "এর সব ডেটা",
-"cl.if_pick": "ইন্টারফেস",
-"cl.if_none": "কোনও ইন্টারফেস নথিভুক্ত নেই",
-"cl.if_locked": "ইন্টারফেস অনুযায়ী মোছার জন্য নিবন্ধিত সংস্করণ দরকার।",
-"cl.if_estimating": "কত মুছবে তা হিসাব করা হচ্ছে…",
-"cl.if_counters": "{n}টি কাউন্টার নমুনা",
-"cl.if_frees": "প্রায় {s} খালি হবে",
-"cl.if_keep": "ঘণ্টাভিত্তিক সারসংক্ষেপের {n}টি সারি তাদের মোট ধরে রাখে",
-"cl.if_confirm": "{i}-এর সব সংরক্ষিত ডেটা মুছবেন? এটি ফেরানো যায় না।",
-"cl.if_confirm_days": "{i}-এর {n} দিনের পুরনো ডেটা মুছবেন? এটি ফেরানো যায় না।",
-"cl.if_done": "{i}-এর {n}টি ফ্লো রেকর্ড ও {c}টি কাউন্টার নমুনা মোছা হয়েছে, {s} খালি হয়েছে।",
-"nav.racks": "সাইট ও র্যাক",
+"nav.racks": "ডিভাইসের অবস্থান",
"nav.devices": "ডিভাইস ও পোর্ট",
"nm.k_host": "হোস্ট",
"nm.k_net": "নেটওয়ার্ক",
@@ -575,9 +560,6 @@
"lic.exp": "{c}-এর লাইসেন্স {d} তারিখে শেষ হয়েছে।",
"lic.mark": "রিপোর্ট ও চিত্রে মূল্যায়ন কপির চিহ্ন থাকে।",
"lic.perp": "{c}-কে লাইসেন্স দেওয়া",
-"lic.badge_get": "নিবন্ধিত নয়",
-"lic.badge_days": "লাইসেন্স · {n} দিন",
-"lic.badge_ok": "লাইসেন্স করা",
"mark.text": "মূল্যায়ন কপি",
"if.bw": "ইন্টারফেস ব্যান্ডউইথ",
"rec.live": "প্রতি ৫ সেকেন্ডে হালনাগাদ",
diff --git a/internal/web/static/i18n/de.json b/internal/web/static/i18n/de.json
index 383a4d2..6d1f34f 100644
--- a/internal/web/static/i18n/de.json
+++ b/internal/web/static/i18n/de.json
@@ -9,6 +9,7 @@
"nav.ifaces": "Schnittstellenprüfung",
"nav.sources": "Einstellungen",
"self.receiving": "Empfang aktiv",
+"self.title": "Erfassung",
"self.idle": "Keine eingehenden Daten",
"self.ingest": "Eingang",
"self.per_sec": "{n} Flows/s",
@@ -511,23 +512,7 @@
"cl.confirm_all": "ALLE Daten löschen: Flow-Datensätze, Zusammenfassungen, Zähler und Befunde?",
"cl.confirm_all2": "Wirklich alles löschen? Die Seiten bleiben leer, bis neue Flows eintreffen.",
"cl.done": "{n} Flow-Datensätze gelöscht, {s} freigegeben.",
-"cl.reg_only": "Nur mit Registrierung",
-"cl.if_title": "Daten einer Schnittstelle löschen",
-"cl.if_sub": "Wenn der Verkehr einer Leitung weg muss",
-"cl.if_what": "Löscht die Flow-Datensätze, die über die gewählte Schnittstelle ein- oder ausgingen, samt ihrer Zählerwerte. Die stündlichen und täglichen Zusammenfassungen erfassen den Verkehr nach Anwendung, Host und Dimension, nie nach Schnittstelle, und behalten daher ihre Summen: ein Diagramm über einen langen Zeitraum zählt den Anteil dieser Schnittstelle weiterhin mit.",
-"cl.if_both": "Ein Flow-Datensatz nennt die Schnittstelle, über die er hereinkam, und die, über die er hinausging. Werden die Datensätze dieser Schnittstelle gelöscht, verschwindet dieser Verkehr daher auch aus der Ansicht der anderen Schnittstelle — es sind dieselben Datensätze.",
-"cl.if_any_age": "Alle Daten",
-"cl.if_pick": "Schnittstelle",
-"cl.if_none": "keine Schnittstellen erfasst",
-"cl.if_locked": "Das Löschen nach Schnittstelle erfordert eine registrierte Installation.",
-"cl.if_estimating": "Es wird ermittelt, was entfällt…",
-"cl.if_counters": "{n} Zählerwerte",
-"cl.if_frees": "gibt etwa {s} frei",
-"cl.if_keep": "{n} stündliche Zeilen behalten ihre Summen",
-"cl.if_confirm": "Alle gespeicherten Daten von {i} löschen? Das ist nicht umkehrbar.",
-"cl.if_confirm_days": "Die Daten von {i} löschen, die älter als {n} Tage sind? Das ist nicht umkehrbar.",
-"cl.if_done": "{n} Flow-Datensätze und {c} Zählerwerte von {i} gelöscht, {s} freigegeben.",
-"nav.racks": "Standorte & Racks",
+"nav.racks": "Gerätestandorte",
"nav.devices": "Geräte & Ports",
"nm.k_host": "Host",
"nm.k_net": "Netz",
@@ -575,9 +560,6 @@
"lic.exp": "Die Lizenz für {c} endete am {d}.",
"lic.mark": "Berichte und Diagramme tragen einen Testversions-Hinweis.",
"lic.perp": "Lizenziert für {c}",
-"lic.badge_get": "Nicht registriert",
-"lic.badge_days": "Lizenziert · {n} Tage",
-"lic.badge_ok": "Lizenziert",
"mark.text": "TESTVERSION",
"if.bw": "Schnittstellenbandbreite",
"rec.live": "Aktualisierung alle 5 s",
diff --git a/internal/web/static/i18n/en.json b/internal/web/static/i18n/en.json
index 0a87fcf..33ed73a 100644
--- a/internal/web/static/i18n/en.json
+++ b/internal/web/static/i18n/en.json
@@ -9,6 +9,7 @@
"nav.ifaces": "Interface check",
"nav.sources": "Settings",
"self.receiving": "Receiving",
+"self.title": "Collection",
"self.idle": "No data arriving",
"self.ingest": "Ingest",
"self.per_sec": "{n} flows/s",
@@ -511,23 +512,7 @@
"cl.confirm_all": "Delete ALL data: flow records, summaries, counters and findings?",
"cl.confirm_all2": "Really delete everything? The pages will be empty until new flows arrive.",
"cl.done": "Deleted {n} flow records, freed {s}.",
-"cl.reg_only": "Registered only",
-"cl.if_title": "Delete one interface's data",
-"cl.if_sub": "When one link's traffic must go",
-"cl.if_what": "Deletes the flow records that went in or out of the chosen interface, and its counter samples. The hourly and daily summaries record traffic by application, host and dimension, never by interface, so they keep their totals: a chart over a long range still counts this interface's share.",
-"cl.if_both": "A flow record names the interface it came in on and the one it went out by, so deleting this interface's records takes those flows out of the other interface's view as well — they are the same records.",
-"cl.if_any_age": "All of its data",
-"cl.if_pick": "Interface",
-"cl.if_none": "no interfaces recorded",
-"cl.if_locked": "Deleting data by interface needs a registered copy.",
-"cl.if_estimating": "Working out what this would remove…",
-"cl.if_counters": "{n} counter samples",
-"cl.if_frees": "frees about {s}",
-"cl.if_keep": "{n} hourly summary rows keep their totals",
-"cl.if_confirm": "Delete all stored data of {i}? This cannot be undone.",
-"cl.if_confirm_days": "Delete the data of {i} older than {n} days? This cannot be undone.",
-"cl.if_done": "Deleted {n} flow records and {c} counter samples of {i}, freed {s}.",
-"nav.racks": "Sites & racks",
+"nav.racks": "Device locations",
"nav.devices": "Devices & ports",
"nm.k_host": "Host",
"nm.k_net": "Network",
@@ -575,9 +560,6 @@
"lic.exp": "The licence for {c} ended on {d}.",
"lic.mark": "Reports and diagrams carry an evaluation mark.",
"lic.perp": "Licensed to {c}",
-"lic.badge_get": "Not registered",
-"lic.badge_days": "Licensed · {n} days",
-"lic.badge_ok": "Licensed",
"mark.text": "EVALUATION COPY",
"if.bw": "Interface bandwidth",
"rec.live": "updated every 5 s",
diff --git a/internal/web/static/i18n/es.json b/internal/web/static/i18n/es.json
index cfb4d9f..0cc682a 100644
--- a/internal/web/static/i18n/es.json
+++ b/internal/web/static/i18n/es.json
@@ -9,6 +9,7 @@
"nav.ifaces": "Verificación de interfaces",
"nav.sources": "Configuración",
"self.receiving": "Recibiendo",
+"self.title": "Recolección",
"self.idle": "No llegan datos",
"self.ingest": "Entrada",
"self.per_sec": "{n} flujos/s",
@@ -511,23 +512,7 @@
"cl.confirm_all": "¿Borrar TODOS los datos: registros, resúmenes, contadores y hallazgos?",
"cl.confirm_all2": "¿Seguro que quiere borrarlo todo? Las páginas estarán vacías hasta que lleguen nuevos flujos.",
"cl.done": "Se borraron {n} registros de flujo y se liberaron {s}.",
-"cl.reg_only": "Solo versión registrada",
-"cl.if_title": "Borrar los datos de una interfaz",
-"cl.if_sub": "Cuando hay que quitar el tráfico de un enlace",
-"cl.if_what": "Borra los registros de flujo que entraron o salieron por la interfaz elegida, junto con sus muestras de contadores. Los resúmenes por hora y por día registran el tráfico por aplicación, host y dimensión, nunca por interfaz, así que conservan sus totales: un gráfico de un rango largo sigue contando la parte de esta interfaz.",
-"cl.if_both": "Un registro de flujo indica la interfaz por la que entró y la que usó para salir, así que borrar los registros de esta interfaz quita ese tráfico también de la vista de la otra interfaz: son los mismos registros.",
-"cl.if_any_age": "Todos sus datos",
-"cl.if_pick": "Interfaz",
-"cl.if_none": "no hay interfaces registradas",
-"cl.if_locked": "Borrar por interfaz requiere una copia registrada.",
-"cl.if_estimating": "Calculando lo que se borraría…",
-"cl.if_counters": "{n} muestras de contadores",
-"cl.if_frees": "libera unos {s}",
-"cl.if_keep": "{n} filas de resumen por hora conservan sus totales",
-"cl.if_confirm": "¿Borrar todos los datos guardados de {i}? No se puede deshacer.",
-"cl.if_confirm_days": "¿Borrar los datos de {i} con más de {n} días? No se puede deshacer.",
-"cl.if_done": "Se borraron {n} registros de flujo y {c} muestras de contadores de {i}, liberando {s}.",
-"nav.racks": "Sedes y racks",
+"nav.racks": "Ubicación de equipos",
"nav.devices": "Equipos y puertos",
"nm.k_host": "Host",
"nm.k_net": "Red",
@@ -575,9 +560,6 @@
"lic.exp": "La licencia de {c} terminó el {d}.",
"lic.mark": "Los informes y los diagramas llevan una marca de evaluación.",
"lic.perp": "Con licencia para {c}",
-"lic.badge_get": "Sin registrar",
-"lic.badge_days": "Con licencia · {n} días",
-"lic.badge_ok": "Con licencia",
"mark.text": "EVALUACIÓN",
"if.bw": "Ancho de banda de la interfaz",
"rec.live": "se actualiza cada 5 s",
diff --git a/internal/web/static/i18n/fr.json b/internal/web/static/i18n/fr.json
index 23e468c..7f7d42e 100644
--- a/internal/web/static/i18n/fr.json
+++ b/internal/web/static/i18n/fr.json
@@ -9,6 +9,7 @@
"nav.ifaces": "Contrôle des interfaces",
"nav.sources": "Paramètres",
"self.receiving": "Réception en cours",
+"self.title": "Collecte",
"self.idle": "Aucune donnée reçue",
"self.ingest": "Réception",
"self.per_sec": "{n} flux/s",
@@ -511,23 +512,7 @@
"cl.confirm_all": "Supprimer TOUTES les données : enregistrements, résumés, compteurs et détections ?",
"cl.confirm_all2": "Vraiment tout supprimer ? Les pages resteront vides jusqu'à l'arrivée de nouveaux flux.",
"cl.done": "{n} enregistrements supprimés, {s} libérés.",
-"cl.reg_only": "Version enregistrée",
-"cl.if_title": "Supprimer les données d'une interface",
-"cl.if_sub": "Quand le trafic d'un lien doit disparaître",
-"cl.if_what": "Supprime les enregistrements de flux entrés ou sortis par l'interface choisie, ainsi que ses relevés de compteurs. Les résumés horaires et quotidiens enregistrent le trafic par application, hôte et dimension, jamais par interface : leurs totaux restent donc inchangés et un graphique sur une longue période compte encore la part de cette interface.",
-"cl.if_both": "Un enregistrement de flux indique l'interface d'entrée et celle de sortie : supprimer les enregistrements de cette interface retire donc ce trafic de la vue de l'autre interface aussi — ce sont les mêmes enregistrements.",
-"cl.if_any_age": "Toutes ses données",
-"cl.if_pick": "Interface",
-"cl.if_none": "aucune interface enregistrée",
-"cl.if_locked": "La suppression par interface nécessite une version enregistrée.",
-"cl.if_estimating": "Calcul de ce qui serait supprimé…",
-"cl.if_counters": "{n} relevés de compteurs",
-"cl.if_frees": "libère environ {s}",
-"cl.if_keep": "{n} lignes de résumé horaire gardent leurs totaux",
-"cl.if_confirm": "Supprimer toutes les données de {i} ? Irréversible.",
-"cl.if_confirm_days": "Supprimer les données de {i} antérieures à {n} jours ? Irréversible.",
-"cl.if_done": "{n} enregistrements et {c} relevés de compteurs de {i} supprimés, {s} libérés.",
-"nav.racks": "Sites et baies",
+"nav.racks": "Emplacement des équipements",
"nav.devices": "Équipements et ports",
"nm.k_host": "Hôte",
"nm.k_net": "Réseau",
@@ -575,9 +560,6 @@
"lic.exp": "La licence de {c} a expiré le {d}.",
"lic.mark": "Les rapports et les schémas portent une marque d'évaluation.",
"lic.perp": "Sous licence pour {c}",
-"lic.badge_get": "Non enregistré",
-"lic.badge_days": "Sous licence · {n} jours",
-"lic.badge_ok": "Sous licence",
"mark.text": "ÉVALUATION",
"if.bw": "Bande passante de l'interface",
"rec.live": "mis à jour toutes les 5 s",
diff --git a/internal/web/static/i18n/hi.json b/internal/web/static/i18n/hi.json
index 2b252f0..122d5c1 100644
--- a/internal/web/static/i18n/hi.json
+++ b/internal/web/static/i18n/hi.json
@@ -9,6 +9,7 @@
"nav.ifaces": "इंटरफ़ेस मिलान",
"nav.sources": "सेटिंग्स",
"self.receiving": "डेटा आ रहा है",
+"self.title": "संग्रह की स्थिति",
"self.idle": "कोई डेटा नहीं आ रहा",
"self.ingest": "आवक",
"self.per_sec": "{n} फ़्लो/से.",
@@ -511,23 +512,7 @@
"cl.confirm_all": "सारा डेटा हटाएँ: रिकॉर्ड, सारांश, काउंटर और निष्कर्ष?",
"cl.confirm_all2": "सच में सब हटाएँ? नए फ़्लो आने तक सभी पेज खाली रहेंगे।",
"cl.done": "{n} फ़्लो रिकॉर्ड हटाए, {s} खाली हुआ।",
-"cl.reg_only": "केवल पंजीकृत संस्करण",
-"cl.if_title": "एक इंटरफ़ेस का डेटा हटाएँ",
-"cl.if_sub": "जब किसी एक लिंक का ट्रैफ़िक हटाना हो",
-"cl.if_what": "चुने गए इंटरफ़ेस से अंदर या बाहर गए फ़्लो रिकॉर्ड और उसके काउंटर नमूने हटाता है। प्रति घंटा और प्रतिदिन के सारांश ट्रैफ़िक को ऐप्लिकेशन, होस्ट और डाइमेंशन के हिसाब से रखते हैं, इंटरफ़ेस के हिसाब से नहीं, इसलिए उनके कुल वही रहते हैं: लंबी अवधि के चार्ट में इस इंटरफ़ेस का हिस्सा अब भी गिना जाता है।",
-"cl.if_both": "हर फ़्लो रिकॉर्ड में आने वाला और जाने वाला, दोनों इंटरफ़ेस दर्ज होते हैं। इसलिए इस इंटरफ़ेस के रिकॉर्ड हटाने पर वह ट्रैफ़िक दूसरे इंटरफ़ेस के दृश्य से भी हट जाता है — वे वही रिकॉर्ड हैं।",
-"cl.if_any_age": "इसका पूरा डेटा",
-"cl.if_pick": "इंटरफ़ेस",
-"cl.if_none": "कोई इंटरफ़ेस दर्ज नहीं",
-"cl.if_locked": "इंटरफ़ेस के हिसाब से हटाने के लिए पंजीकृत प्रति चाहिए।",
-"cl.if_estimating": "कितना हटेगा, इसका अनुमान लगाया जा रहा है…",
-"cl.if_counters": "{n} काउंटर नमूने",
-"cl.if_frees": "लगभग {s} खाली होगा",
-"cl.if_keep": "प्रति घंटा सारांश की {n} पंक्तियाँ अपने कुल बनाए रखती हैं",
-"cl.if_confirm": "{i} का सारा संग्रहित डेटा हटाएँ? इसे पलटा नहीं जा सकता।",
-"cl.if_confirm_days": "{i} का {n} दिन से पुराना डेटा हटाएँ? इसे पलटा नहीं जा सकता।",
-"cl.if_done": "{i} के {n} फ़्लो रिकॉर्ड और {c} काउंटर नमूने हटाए, {s} खाली हुआ।",
-"nav.racks": "साइट और रैक",
+"nav.racks": "डिवाइस का स्थान",
"nav.devices": "उपकरण और पोर्ट",
"nm.k_host": "होस्ट",
"nm.k_net": "नेटवर्क",
@@ -575,9 +560,6 @@
"lic.exp": "{c} का लाइसेंस {d} को समाप्त हो गया।",
"lic.mark": "रिपोर्ट और आरेखों पर मूल्यांकन प्रति का निशान रहता है।",
"lic.perp": "{c} को लाइसेंस प्राप्त",
-"lic.badge_get": "पंजीकृत नहीं",
-"lic.badge_days": "लाइसेंस · {n} दिन",
-"lic.badge_ok": "लाइसेंस प्राप्त",
"mark.text": "मूल्यांकन प्रति",
"if.bw": "इंटरफ़ेस बैंडविड्थ",
"rec.live": "हर 5 सेकंड अपडेट",
diff --git a/internal/web/static/i18n/id.json b/internal/web/static/i18n/id.json
index bc77b01..0e17000 100644
--- a/internal/web/static/i18n/id.json
+++ b/internal/web/static/i18n/id.json
@@ -9,6 +9,7 @@
"nav.ifaces": "Pencocokan antarmuka",
"nav.sources": "Pengaturan",
"self.receiving": "Menerima",
+"self.title": "Pengumpulan",
"self.idle": "Tidak ada data masuk",
"self.ingest": "Masuk",
"self.per_sec": "{n} flow/dtk",
@@ -511,23 +512,7 @@
"cl.confirm_all": "Hapus SEMUA data: catatan, ringkasan, penghitung, dan temuan?",
"cl.confirm_all2": "Yakin hapus semuanya? Halaman akan kosong sampai flow baru masuk.",
"cl.done": "{n} catatan flow dihapus, {s} dibebaskan.",
-"cl.reg_only": "Hanya versi terdaftar",
-"cl.if_title": "Hapus data satu antarmuka",
-"cl.if_sub": "Ketika trafik satu tautan harus pergi",
-"cl.if_what": "Menghapus catatan flow yang masuk atau keluar melalui antarmuka terpilih, beserta sampel pencacahnya. Ringkasan per jam dan per hari mencatat trafik menurut aplikasi, host dan dimensi, bukan menurut antarmuka, jadi totalnya tetap: grafik rentang panjang masih menghitung bagian antarmuka ini.",
-"cl.if_both": "Sebuah catatan flow menyebut antarmuka tempat ia masuk dan tempat ia keluar, jadi menghapus catatan antarmuka ini juga mengeluarkan trafik itu dari tampilan antarmuka lainnya — catatannya sama.",
-"cl.if_any_age": "Semua datanya",
-"cl.if_pick": "Antarmuka",
-"cl.if_none": "tidak ada antarmuka tercatat",
-"cl.if_locked": "Menghapus menurut antarmuka memerlukan salinan terdaftar.",
-"cl.if_estimating": "Menghitung apa yang akan dihapus…",
-"cl.if_counters": "{n} sampel pencacah",
-"cl.if_frees": "membebaskan sekitar {s}",
-"cl.if_keep": "{n} baris ringkasan per jam tetap dengan totalnya",
-"cl.if_confirm": "Hapus semua data tersimpan dari {i}? Tidak dapat dibatalkan.",
-"cl.if_confirm_days": "Hapus data {i} yang lebih tua dari {n} hari? Tidak dapat dibatalkan.",
-"cl.if_done": "Menghapus {n} catatan flow dan {c} sampel pencacah dari {i}, membebaskan {s}.",
-"nav.racks": "Lokasi & rak",
+"nav.racks": "Lokasi perangkat",
"nav.devices": "Perangkat & port",
"nm.k_host": "Host",
"nm.k_net": "Jaringan",
@@ -575,9 +560,6 @@
"lic.exp": "Lisensi {c} berakhir pada {d}.",
"lic.mark": "Laporan dan diagram diberi tanda versi evaluasi.",
"lic.perp": "Dilisensikan untuk {c}",
-"lic.badge_get": "Belum terdaftar",
-"lic.badge_days": "Berlisensi · {n} hari",
-"lic.badge_ok": "Berlisensi",
"mark.text": "EVALUASI",
"if.bw": "Bandwidth antarmuka",
"rec.live": "diperbarui tiap 5 dtk",
diff --git a/internal/web/static/i18n/ja.json b/internal/web/static/i18n/ja.json
index 73c9a81..5f7c34a 100644
--- a/internal/web/static/i18n/ja.json
+++ b/internal/web/static/i18n/ja.json
@@ -9,6 +9,7 @@
"nav.ifaces": "インターフェース照合",
"nav.sources": "設定",
"self.receiving": "受信中",
+"self.title": "収集の状態",
"self.idle": "データが届いていません",
"self.ingest": "取り込み",
"self.per_sec": "{n} 件/秒",
@@ -511,23 +512,7 @@
"cl.confirm_all": "すべてのデータ(フローレコード、集計、カウンター、検出結果)を削除しますか?",
"cl.confirm_all2": "本当にすべて削除しますか?新しいフローが届くまで全ページが空になります。",
"cl.done": "フローレコード {n} 件を削除し、{s} 空きました。",
-"cl.reg_only": "登録版のみ",
-"cl.if_title": "インターフェース単位でデータを削除",
-"cl.if_sub": "特定の回線のデータだけを消す",
-"cl.if_what": "選んだインターフェースを出入りしたフローレコードと、そのカウンター値を削除します。時間別・日別の集計はアプリ・ホスト・ディメンション単位で記録されており、インターフェース別ではないため合計はそのまま残ります。長期間のグラフにはこのインターフェース分も含まれたままです。",
-"cl.if_both": "フローレコードには入力インターフェースと出力インターフェースの両方が記録されています。そのため、このインターフェースのレコードを削除すると、同じレコードであるもう一方のインターフェースの表示からもそのトラフィックが消えます。",
-"cl.if_any_age": "すべての期間",
-"cl.if_pick": "インターフェース",
-"cl.if_none": "インターフェースの記録がありません",
-"cl.if_locked": "インターフェース単位の削除は登録版の機能です。",
-"cl.if_estimating": "削除対象を集計しています…",
-"cl.if_counters": "カウンター {n} 件",
-"cl.if_frees": "約 {s} 解放",
-"cl.if_keep": "時間別集計 {n} 行は合計のまま残ります",
-"cl.if_confirm": "{i} の保存データをすべて削除しますか?元に戻せません。",
-"cl.if_confirm_days": "{i} の {n} 日より古いデータを削除しますか?元に戻せません。",
-"cl.if_done": "{i} のフローレコード {n} 件とカウンター {c} 件を削除し、{s} 空きました。",
-"nav.racks": "拠点とラック",
+"nav.racks": "機器の設置場所",
"nav.devices": "機器とポート",
"nm.k_host": "ホスト",
"nm.k_net": "ネットワーク",
@@ -575,9 +560,6 @@
"lic.exp": "{c} のライセンスは {d} に終了しました。",
"lic.mark": "レポートと図に評価版の透かしが入ります。",
"lic.perp": "{c} にライセンス済み",
-"lic.badge_get": "未登録",
-"lic.badge_days": "ライセンス済み · 残り {n} 日",
-"lic.badge_ok": "ライセンス済み",
"mark.text": "評価版",
"if.bw": "インターフェース帯域",
"rec.live": "5 秒ごとに更新",
diff --git a/internal/web/static/i18n/ko.json b/internal/web/static/i18n/ko.json
index 7248d61..93ae6ab 100644
--- a/internal/web/static/i18n/ko.json
+++ b/internal/web/static/i18n/ko.json
@@ -9,6 +9,7 @@
"nav.ifaces": "인터페이스 대조",
"nav.sources": "설정",
"self.receiving": "수신 중",
+"self.title": "수집 상태",
"self.idle": "들어오는 데이터 없음",
"self.ingest": "수집",
"self.per_sec": "초당 {n}개 플로",
@@ -511,23 +512,7 @@
"cl.confirm_all": "플로 레코드, 집계, 카운터, 탐지 결과 등 모든 데이터를 삭제할까요?",
"cl.confirm_all2": "정말 전부 삭제할까요? 새 플로가 들어올 때까지 모든 페이지가 비게 됩니다.",
"cl.done": "플로 레코드 {n}개를 삭제하고 {s}를 확보했습니다.",
-"cl.reg_only": "등록판 전용",
-"cl.if_title": "인터페이스 단위로 데이터 삭제",
-"cl.if_sub": "특정 회선의 데이터만 지울 때",
-"cl.if_what": "선택한 인터페이스로 들어오거나 나간 플로 레코드와 그 카운터 표본을 삭제합니다. 시간별·일별 집계는 애플리케이션, 호스트, 차원별로 기록되며 인터페이스별이 아니므로 합계는 그대로 남습니다. 장기간 차트에는 이 인터페이스의 몫이 계속 포함됩니다.",
-"cl.if_both": "플로 레코드에는 들어온 인터페이스와 나간 인터페이스가 함께 기록됩니다. 따라서 이 인터페이스의 레코드를 삭제하면 같은 레코드인 다른 인터페이스의 화면에서도 그 트래픽이 사라집니다.",
-"cl.if_any_age": "전체 데이터",
-"cl.if_pick": "인터페이스",
-"cl.if_none": "기록된 인터페이스가 없습니다",
-"cl.if_locked": "인터페이스 단위 삭제는 등록판 기능입니다.",
-"cl.if_estimating": "삭제될 양을 계산하고 있습니다…",
-"cl.if_counters": "카운터 표본 {n}개",
-"cl.if_frees": "약 {s} 확보",
-"cl.if_keep": "시간별 집계 {n}행은 합계를 유지합니다",
-"cl.if_confirm": "{i}의 저장된 데이터를 모두 삭제하시겠습니까? 되돌릴 수 없습니다.",
-"cl.if_confirm_days": "{i}의 {n}일보다 오래된 데이터를 삭제하시겠습니까? 되돌릴 수 없습니다.",
-"cl.if_done": "{i}의 플로 레코드 {n}개와 카운터 표본 {c}개를 삭제하고 {s}를 확보했습니다.",
-"nav.racks": "사이트와 랙",
+"nav.racks": "장비 위치",
"nav.devices": "장비와 포트",
"nm.k_host": "호스트",
"nm.k_net": "네트워크",
@@ -575,9 +560,6 @@
"lic.exp": "{c}의 라이선스가 {d}에 끝났습니다.",
"lic.mark": "보고서와 도표에 평가판 표시가 들어갑니다.",
"lic.perp": "{c}에 라이선스됨",
-"lic.badge_get": "미등록",
-"lic.badge_days": "라이선스 · {n}일",
-"lic.badge_ok": "라이선스됨",
"mark.text": "평가판",
"if.bw": "인터페이스 대역폭",
"rec.live": "5초마다 업데이트",
diff --git a/internal/web/static/i18n/pt.json b/internal/web/static/i18n/pt.json
index 9c939c4..1e5a61e 100644
--- a/internal/web/static/i18n/pt.json
+++ b/internal/web/static/i18n/pt.json
@@ -9,6 +9,7 @@
"nav.ifaces": "Conferência de interfaces",
"nav.sources": "Configurações",
"self.receiving": "Recebendo",
+"self.title": "Coleta",
"self.idle": "Nenhum dado chegando",
"self.ingest": "Entrada",
"self.per_sec": "{n} fluxos/s",
@@ -511,23 +512,7 @@
"cl.confirm_all": "Apagar TODOS os dados: registros, resumos, contadores e detecções?",
"cl.confirm_all2": "Apagar tudo mesmo? As páginas ficarão vazias até chegarem novos fluxos.",
"cl.done": "{n} registros apagados, {s} liberados.",
-"cl.reg_only": "Somente versão registrada",
-"cl.if_title": "Apagar os dados de uma interface",
-"cl.if_sub": "Quando o tráfego de um enlace tem de sair",
-"cl.if_what": "Apaga os registros de fluxo que entraram ou saíram pela interface escolhida, junto com suas amostras de contadores. Os resumos por hora e por dia registram o tráfego por aplicação, host e dimensão, nunca por interface, por isso mantêm seus totais: um gráfico de um período longo ainda conta a parte desta interface.",
-"cl.if_both": "Um registro de fluxo indica a interface de entrada e a de saída, por isso apagar os registros desta interface tira esse tráfego também da visão da outra interface — são os mesmos registros.",
-"cl.if_any_age": "Todos os seus dados",
-"cl.if_pick": "Interface",
-"cl.if_none": "nenhuma interface registrada",
-"cl.if_locked": "Apagar por interface exige uma cópia registrada.",
-"cl.if_estimating": "Calculando o que seria apagado…",
-"cl.if_counters": "{n} amostras de contadores",
-"cl.if_frees": "libera cerca de {s}",
-"cl.if_keep": "{n} linhas de resumo por hora mantêm seus totais",
-"cl.if_confirm": "Apagar todos os dados guardados de {i}? Não há como desfazer.",
-"cl.if_confirm_days": "Apagar os dados de {i} com mais de {n} dias? Não há como desfazer.",
-"cl.if_done": "Apagados {n} registros de fluxo e {c} amostras de contadores de {i}, liberados {s}.",
-"nav.racks": "Sites e racks",
+"nav.racks": "Localização dos equipamentos",
"nav.devices": "Equipamentos e portas",
"nm.k_host": "Host",
"nm.k_net": "Rede",
@@ -575,9 +560,6 @@
"lic.exp": "A licença de {c} terminou em {d}.",
"lic.mark": "Os relatórios e os diagramas levam uma marca de avaliação.",
"lic.perp": "Licenciado para {c}",
-"lic.badge_get": "Não registado",
-"lic.badge_days": "Licenciado · {n} dias",
-"lic.badge_ok": "Licenciado",
"mark.text": "AVALIAÇÃO",
"if.bw": "Largura de banda da interface",
"rec.live": "atualizado a cada 5 s",
diff --git a/internal/web/static/i18n/ru.json b/internal/web/static/i18n/ru.json
index ab9f25a..5d23a70 100644
--- a/internal/web/static/i18n/ru.json
+++ b/internal/web/static/i18n/ru.json
@@ -9,6 +9,7 @@
"nav.ifaces": "Сверка интерфейсов",
"nav.sources": "Настройки",
"self.receiving": "Приём идёт",
+"self.title": "Сбор данных",
"self.idle": "Данные не поступают",
"self.ingest": "Приём",
"self.per_sec": "{n} потоков/с",
@@ -511,23 +512,7 @@
"cl.confirm_all": "Удалить ВСЕ данные: записи, сводки, счётчики и находки?",
"cl.confirm_all2": "Точно удалить всё? Страницы будут пустыми, пока не придут новые потоки.",
"cl.done": "Удалено записей: {n}, освобождено {s}.",
-"cl.reg_only": "Только для зарегистрированных",
-"cl.if_title": "Удалить данные одного интерфейса",
-"cl.if_sub": "Когда нужно убрать трафик одного канала",
-"cl.if_what": "Удаляет записи потоков, прошедшие через выбранный интерфейс на вход или на выход, и его показания счётчиков. Часовые и суточные сводки хранят трафик по приложениям, узлам и измерениям, а не по интерфейсам, поэтому их суммы остаются прежними: на графике за длинный период доля этого интерфейса по-прежнему учитывается.",
-"cl.if_both": "В записи потока указаны и входной, и выходной интерфейс, поэтому удаление записей этого интерфейса убирает этот трафик и из представления другого интерфейса — это одни и те же записи.",
-"cl.if_any_age": "Все данные",
-"cl.if_pick": "Интерфейс",
-"cl.if_none": "интерфейсы не записаны",
-"cl.if_locked": "Удаление по интерфейсу доступно в зарегистрированной версии.",
-"cl.if_estimating": "Подсчёт того, что будет удалено…",
-"cl.if_counters": "показаний счётчиков: {n}",
-"cl.if_frees": "освободит около {s}",
-"cl.if_keep": "строк часовых сводок сохранят суммы: {n}",
-"cl.if_confirm": "Удалить все сохранённые данные {i}? Отменить нельзя.",
-"cl.if_confirm_days": "Удалить данные {i} старше {n} дней? Отменить нельзя.",
-"cl.if_done": "Удалено записей потоков: {n} и показаний счётчиков: {c} для {i}, освобождено {s}.",
-"nav.racks": "Площадки и стойки",
+"nav.racks": "Размещение устройств",
"nav.devices": "Устройства и порты",
"nm.k_host": "Хост",
"nm.k_net": "Сеть",
@@ -575,9 +560,6 @@
"lic.exp": "Лицензия для {c} закончилась {d}.",
"lic.mark": "На отчётах и схемах стоит пометка ознакомительной версии.",
"lic.perp": "Лицензия на {c}",
-"lic.badge_get": "Не зарегистрировано",
-"lic.badge_days": "Лицензия · {n} дн.",
-"lic.badge_ok": "Лицензия",
"mark.text": "ОЗНАКОМИТЕЛЬНАЯ",
"if.bw": "Полоса интерфейса",
"rec.live": "обновляется каждые 5 с",
diff --git a/internal/web/static/i18n/ur.json b/internal/web/static/i18n/ur.json
index eb8e2ba..35f075a 100644
--- a/internal/web/static/i18n/ur.json
+++ b/internal/web/static/i18n/ur.json
@@ -9,6 +9,7 @@
"nav.ifaces": "انٹرفیس جانچ",
"nav.sources": "ترتیبات",
"self.receiving": "وصول ہو رہا ہے",
+"self.title": "جمع کرنے کی حالت",
"self.idle": "کوئی ڈیٹا نہیں آ رہا",
"self.ingest": "اندراج",
"self.per_sec": "{n} فلو فی سیکنڈ",
@@ -511,23 +512,7 @@
"cl.confirm_all": "سارا ڈیٹا حذف کریں: ریکارڈز، خلاصے، کاؤنٹرز اور نتائج؟",
"cl.confirm_all2": "کیا واقعی سب حذف کریں؟ نئے فلو آنے تک تمام صفحات خالی رہیں گے۔",
"cl.done": "{n} فلو ریکارڈز حذف ہوئے، {s} خالی ہوا۔",
-"cl.reg_only": "صرف رجسٹرڈ نسخہ",
-"cl.if_title": "کسی ایک انٹرفیس کا ڈیٹا حذف کریں",
-"cl.if_sub": "جب کسی ایک لنک کا ٹریفک ہٹانا ہو",
-"cl.if_what": "منتخب انٹرفیس سے اندر یا باہر جانے والے فلو ریکارڈز اور اس کے کاؤنٹر نمونے حذف کرتا ہے۔ گھنٹہ وار اور روزانہ خلاصے ٹریفک کو ایپلیکیشن، میزبان اور جہت کے حساب سے رکھتے ہیں، انٹرفیس کے حساب سے نہیں، لہٰذا ان کے مجموعے وہی رہتے ہیں: طویل مدت کے چارٹ میں اس انٹرفیس کا حصہ اب بھی شامل رہتا ہے۔",
-"cl.if_both": "ہر فلو ریکارڈ میں داخل ہونے اور باہر جانے والے دونوں انٹرفیس درج ہوتے ہیں، چنانچہ اس انٹرفیس کے ریکارڈ حذف کرنے سے وہ ٹریفک دوسرے انٹرفیس کے منظر سے بھی نکل جاتا ہے — وہ ایک ہی ریکارڈ ہیں۔",
-"cl.if_any_age": "اس کا سارا ڈیٹا",
-"cl.if_pick": "انٹرفیس",
-"cl.if_none": "کوئی انٹرفیس ریکارڈ نہیں",
-"cl.if_locked": "انٹرفیس کے حساب سے حذف کرنے کے لیے رجسٹرڈ نسخہ درکار ہے۔",
-"cl.if_estimating": "حساب لگایا جا رہا ہے کہ کتنا حذف ہوگا…",
-"cl.if_counters": "{n} کاؤنٹر نمونے",
-"cl.if_frees": "تقریباً {s} خالی ہوگا",
-"cl.if_keep": "گھنٹہ وار خلاصے کی {n} سطریں اپنے مجموعے برقرار رکھتی ہیں",
-"cl.if_confirm": "{i} کا تمام محفوظ ڈیٹا حذف کریں؟ واپس نہیں ہوسکتا۔",
-"cl.if_confirm_days": "{i} کا {n} دن سے پرانا ڈیٹا حذف کریں؟ واپس نہیں ہوسکتا۔",
-"cl.if_done": "{i} کے {n} فلو ریکارڈز اور {c} کاؤنٹر نمونے حذف ہوئے، {s} خالی ہوا۔",
-"nav.racks": "سائٹس اور ریک",
+"nav.racks": "آلات کا مقام",
"nav.devices": "ڈیوائسز اور پورٹس",
"nm.k_host": "ہوسٹ",
"nm.k_net": "نیٹ ورک",
@@ -575,9 +560,6 @@
"lic.exp": "{c} کا لائسنس {d} کو ختم ہو گیا۔",
"lic.mark": "رپورٹوں اور خاکوں پر تشخیصی نسخے کا نشان ہوتا ہے۔",
"lic.perp": "{c} کو لائسنس یافتہ",
-"lic.badge_get": "رجسٹرڈ نہیں",
-"lic.badge_days": "لائسنس · {n} دن",
-"lic.badge_ok": "لائسنس یافتہ",
"mark.text": "تشخیصی نسخہ",
"if.bw": "انٹرفیس بینڈوڈتھ",
"rec.live": "ہر 5 سیکنڈ میں تازہ",
diff --git a/internal/web/static/i18n/vi.json b/internal/web/static/i18n/vi.json
index 70e8a62..a7d01cb 100644
--- a/internal/web/static/i18n/vi.json
+++ b/internal/web/static/i18n/vi.json
@@ -9,6 +9,7 @@
"nav.ifaces": "Kiểm tra giao diện",
"nav.sources": "Cài đặt",
"self.receiving": "Đang nhận",
+"self.title": "Tình trạng thu thập",
"self.idle": "Không có dữ liệu đến",
"self.ingest": "Thu nhận",
"self.per_sec": "{n} luồng/s",
@@ -511,23 +512,7 @@
"cl.confirm_all": "Xóa TOÀN BỘ dữ liệu: bản ghi luồng, bản tổng hợp, bộ đếm và phát hiện?",
"cl.confirm_all2": "Thật sự xóa tất cả? Các trang sẽ trống cho đến khi có luồng mới.",
"cl.done": "Đã xóa {n} bản ghi luồng, giải phóng {s}.",
-"cl.reg_only": "Chỉ bản đã đăng ký",
-"cl.if_title": "Xóa dữ liệu của một giao diện",
-"cl.if_sub": "Khi cần bỏ lưu lượng của một đường truyền",
-"cl.if_what": "Xóa các bản ghi luồng đi vào hoặc đi ra qua giao diện đã chọn, cùng các mẫu bộ đếm của nó. Bản tổng hợp theo giờ và theo ngày ghi lưu lượng theo ứng dụng, máy chủ và chiều, không theo giao diện, nên tổng của chúng giữ nguyên: biểu đồ trong khoảng thời gian dài vẫn tính phần của giao diện này.",
-"cl.if_both": "Mỗi bản ghi luồng đều ghi cả giao diện vào và giao diện ra, nên xóa bản ghi của giao diện này cũng loại lưu lượng đó khỏi khung nhìn của giao diện bên kia — chúng là cùng những bản ghi.",
-"cl.if_any_age": "Toàn bộ dữ liệu",
-"cl.if_pick": "Giao diện",
-"cl.if_none": "chưa ghi nhận giao diện nào",
-"cl.if_locked": "Xóa theo giao diện cần bản đã đăng ký.",
-"cl.if_estimating": "Đang tính phần sẽ bị xóa…",
-"cl.if_counters": "{n} mẫu bộ đếm",
-"cl.if_frees": "giải phóng khoảng {s}",
-"cl.if_keep": "{n} dòng tổng hợp theo giờ giữ nguyên tổng",
-"cl.if_confirm": "Xóa toàn bộ dữ liệu đã lưu của {i}? Không thể hoàn tác.",
-"cl.if_confirm_days": "Xóa dữ liệu của {i} cũ hơn {n} ngày? Không thể hoàn tác.",
-"cl.if_done": "Đã xóa {n} bản ghi luồng và {c} mẫu bộ đếm của {i}, giải phóng {s}.",
-"nav.racks": "Địa điểm & tủ rack",
+"nav.racks": "Vị trí thiết bị",
"nav.devices": "Thiết bị & cổng",
"nm.k_host": "Máy",
"nm.k_net": "Mạng",
@@ -575,9 +560,6 @@
"lic.exp": "Giấy phép của {c} đã hết hạn ngày {d}.",
"lic.mark": "Báo cáo và sơ đồ có dấu bản dùng thử.",
"lic.perp": "Đã cấp phép cho {c}",
-"lic.badge_get": "Chưa đăng ký",
-"lic.badge_days": "Đã cấp phép · {n} ngày",
-"lic.badge_ok": "Đã cấp phép",
"mark.text": "DÙNG THỬ",
"if.bw": "Băng thông giao diện",
"rec.live": "cập nhật mỗi 5 s",
diff --git a/internal/web/static/i18n/zh.json b/internal/web/static/i18n/zh.json
index 8965489..fcbc148 100644
--- a/internal/web/static/i18n/zh.json
+++ b/internal/web/static/i18n/zh.json
@@ -9,6 +9,7 @@
"nav.ifaces": "接口对账",
"nav.sources": "设定",
"self.receiving": "正在接收",
+"self.title": "采集状态",
"self.idle": "没有数据进来",
"self.ingest": "入库",
"self.per_sec": "{n} 条/秒",
@@ -511,23 +512,7 @@
"cl.confirm_all": "删除全部数据:流记录、统计汇总、计数器和发现?",
"cl.confirm_all2": "真的全部删除吗?在新的流量进来之前,所有页面都会是空的。",
"cl.done": "已删除 {n} 条流记录,释放 {s}。",
-"cl.reg_only": "仅注册版",
-"cl.if_title": "按接口删除数据",
-"cl.if_sub": "只清掉某条链路的数据",
-"cl.if_what": "删除经过所选接口(入向或出向)的流记录及其计数器采样。按小时和按天的汇总是按应用、主机和维度记录的,并不区分接口,因此汇总数字保持不变:长时间范围的图表仍然包含这个接口的流量。",
-"cl.if_both": "每条流记录同时记录了入接口和出接口,因此删除这个接口的记录,也会把这些流量从另一端接口的视图里一并删掉——它们是同一批记录。",
-"cl.if_any_age": "全部数据",
-"cl.if_pick": "接口",
-"cl.if_none": "没有接口记录",
-"cl.if_locked": "按接口删除数据属于注册版功能。",
-"cl.if_estimating": "正在统计将删除的数据…",
-"cl.if_counters": "计数器采样 {n} 条",
-"cl.if_frees": "约释放 {s}",
-"cl.if_keep": "按小时汇总的 {n} 行保留原有合计",
-"cl.if_confirm": "确定删除 {i} 的全部存储数据吗?无法撤销。",
-"cl.if_confirm_days": "确定删除 {i} 中早于 {n} 天的数据吗?无法撤销。",
-"cl.if_done": "已删除 {i} 的 {n} 条流记录和 {c} 条计数器采样,释放 {s}。",
-"nav.racks": "机房与机柜",
+"nav.racks": "设备位置",
"nav.devices": "设备与端口",
"nm.k_host": "主机",
"nm.k_net": "网段",
@@ -575,9 +560,6 @@
"lic.exp": "{c} 的授权已于 {d} 到期。",
"lic.mark": "报表和图上会带评估版水印。",
"lic.perp": "已授权给 {c}",
-"lic.badge_get": "未注册",
-"lic.badge_days": "已授权 · {n} 天",
-"lic.badge_ok": "已授权",
"mark.text": "评估版",
"if.bw": "接口带宽",
"rec.live": "每 5 秒更新",
diff --git a/internal/web/static/index.html b/internal/web/static/index.html
index fa0fb09..0e65234 100644
--- a/internal/web/static/index.html
+++ b/internal/web/static/index.html
@@ -11,11 +11,8 @@
-
diff --git a/internal/web/static/sponsor.js b/internal/web/static/sponsor.js
index 1eedd9b..f8273a4 100644
--- a/internal/web/static/sponsor.js
+++ b/internal/web/static/sponsor.js
@@ -14,13 +14,11 @@
var URL_ = 'https://raw.githubusercontent.com/githubflyideas/ads-sponsor/main/traffic66/sponsors.json';
// MAX is how many advertisers are fetched and rotated through.
var TIMEOUT_MS = 3000, ROTATE_MS = 5500, MAX = 6;
- // A cell narrower than this is not worth drawing a 728x90 banner into, so
- // it decides how many fit across the window.
- var MIN_CELL = 420;
- // A small banner is 728x90 (1456x180 at 2x) and shown SMALL_COPIES times
- // side by side; an image LONG_RATIO times as wide as it is high or wider
- // is a long banner and fills the strip alone.
- var SMALL_W = 728, SMALL_H = 90, SMALL_COPIES = 3, LONG_RATIO = 12;
+ // A small banner is 728x90 (1456x180 at 2x). The slot is in the
+ // sidebar, as wide as it is, so only small banners are shown there: an
+ // image LONG_RATIO times as wide as it is high or wider is a long banner,
+ // too thin at that width to read, and is left out.
+ var SMALL_W = 728, SMALL_H = 90, LONG_RATIO = 12;
var CACHE_KEY = 't66.sponsor', CACHE_MS = 30 * 60 * 1000;
var started = false, timerId = null, roId = null;
@@ -89,7 +87,10 @@
if (!want.length) return Promise.reject();
return settle(want.map(function (s) { return loadImg(s.src); })).then(function (imgs) {
var ok = [];
- want.forEach(function (s, i) { if (imgs[i]) { s.img = imgs[i]; ok.push(s); } });
+ want.forEach(function (s, i) {
+ var im = imgs[i];
+ if (im && im.naturalWidth < LONG_RATIO * im.naturalHeight) { s.img = im; ok.push(s); }
+ });
if (!ok.length) throw 0;
return ok;
});
@@ -107,17 +108,8 @@
return out;
}
- // The strip shows one advertiser at a time and rotates between them.
- // - a long banner (LONG_RATIO wide to 1 high or wider, 2880x120 is the
- // intended size) has the strip to itself;
- // - a small banner (the 728x90 kind) is shown three times side by side,
- // the same ad in every cell, so the strip never mixes two advertisers.
- // A narrow window has room for fewer cells (one per MIN_CELL pixels).
- // The strip keeps the height of a row of small banners whichever ad is
- // up, so rotating never moves the page underneath.
- //
- // One image element cannot be in two cells at once, so each cell gets a
- // clone of an already-loaded image; nothing is fetched twice.
+ // The slot shows one advertiser at a time and rotates between them, at
+ // the sidebar's width and the banner's own proportions.
function build(slot, slides) {
var car = slot.querySelector('.spc');
car.hidden = false;
@@ -134,29 +126,17 @@
a.appendChild(img);
return a;
}
- function isLong(s) { return s.img.naturalWidth >= LONG_RATIO * s.img.naturalHeight; }
function draw() {
var s = slides[cur];
- var w = car.clientWidth || window.innerWidth || MIN_CELL;
- var k = Math.max(1, Math.min(SMALL_COPIES, Math.floor(w / MIN_CELL)));
- var rowH = Math.round((w - (k - 1)) / k * SMALL_H / SMALL_W);
- var n = isLong(s) ? 1 : k;
+ var w = car.clientWidth || SMALL_W;
strip.textContent = '';
- strip.style.gridTemplateColumns = 'repeat(' + n + ', minmax(0, 1fr))';
- for (var i = 0; i < n; i++) {
- var box = document.createElement('div'); box.className = 'spc-cell';
- box.style.height = rowH + 'px';
- box.appendChild(slideEl(s));
- strip.appendChild(box);
- }
+ var box = document.createElement('div'); box.className = 'spc-cell';
+ box.style.height = Math.round(w * SMALL_H / SMALL_W) + 'px';
+ box.appendChild(slideEl(s));
+ strip.appendChild(box);
ds.forEach(function (d, i) { d.classList.toggle('on', i === cur); });
- measure();
}
function show(i) { cur = ((i % slides.length) + slides.length) % slides.length; draw(); }
- // The sidebar's height is the window less this strip (see app.css).
- function measure() {
- try { document.documentElement.style.setProperty('--sp-h', slot.offsetHeight + 'px'); } catch (e) {}
- }
car.textContent = '';
car.appendChild(strip);
@@ -192,14 +172,12 @@
if (!started) return;
slot.querySelector('.sp-text').hidden = false;
slot.hidden = false;
- try { document.documentElement.style.setProperty('--sp-h', slot.offsetHeight + 'px'); } catch (e) {}
});
},
remove: function () {
started = false;
if (timerId) clearInterval(timerId);
if (roId) { roId.disconnect(); roId = null; }
- try { document.documentElement.style.setProperty('--sp-h', '0px'); } catch (e) {}
var slot = document.getElementById('sponsor');
if (slot) slot.remove();
}