Skip to content

Commit 629bc19

Browse files
committed
removed the option to edit topics in the monitor when live. Now it can be set on startup via CLI flags
1 parent 3f904c2 commit 629bc19

6 files changed

Lines changed: 24 additions & 144 deletions

File tree

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,13 @@ go build -ldflags="-s -w" -o dlockss ./cmd/dlockss
182182
Optional monitor (dashboard):
183183
```bash
184184
go build -o dlockss-monitor ./cmd/dlockss-monitor
185-
./dlockss-monitor
185+
./dlockss-monitor # uses default topic (creative-commons)
186+
./dlockss-monitor -topic my-archive # monitor a specific topic
187+
./dlockss-monitor -topic my-archive -prefix dlockss-v0.0.4 # custom topic + prefix
186188
```
187-
Open http://localhost:8080. The monitor displays each node's **name** (if configured via `DLOCKSS_NODE_NAME`), falling back to the Peer ID. Names propagate via HEARTBEAT/JOIN messages and appear in the node table, charts, and shard modals. Client-side aliases (EDIT button) override server-side names. Each node has **one peer ID**: when `DLOCKSS_IPFS_CONFIG` is set (e.g. in testnet), D-LOCKSS uses the IPFS repo identity so the same ID appears in the monitor and in `node_x.ipfs.log`.
189+
Open http://localhost:8080. The `-topic` and `-prefix` flags override the `DLOCKSS_TOPIC_NAME` and `DLOCKSS_PUBSUB_TOPIC_PREFIX` environment variables respectively. The topic is fixed at startup (the dashboard displays it read-only).
190+
191+
The monitor displays each node's **name** (if configured via `DLOCKSS_NODE_NAME`), falling back to the Peer ID. Names propagate via HEARTBEAT/JOIN messages and appear in the node table, charts, and shard modals. Client-side aliases (EDIT button) override server-side names. Each node has **one peer ID**: when `DLOCKSS_IPFS_CONFIG` is set (e.g. in testnet), D-LOCKSS uses the IPFS repo identity so the same ID appears in the monitor and in `node_x.ipfs.log`.
188192

189193
The monitor bootstrap-subscribes to all shards up to depth 6 (127 shards) so it can see nodes even when started late. Set `DLOCKSS_MONITOR_BOOTSTRAP_SHARD_DEPTH` (0–12) to tune.
190194

cmd/dlockss-monitor/main.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package main
44
import (
55
"context"
66
"errors"
7+
"flag"
78
"fmt"
89
"log"
910
"log/slog"
@@ -18,6 +19,10 @@ import (
1819
)
1920

2021
func main() {
22+
topicFlag := flag.String("topic", "", "archive topic name (overrides DLOCKSS_TOPIC_NAME env var)")
23+
prefixFlag := flag.String("prefix", "", "pubsub topic prefix / protocol version (overrides DLOCKSS_PUBSUB_TOPIC_PREFIX env var)")
24+
flag.Parse()
25+
2126
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})))
2227

2328
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
@@ -44,6 +49,14 @@ func main() {
4449
cfg.TopicName = v
4550
slog.Info("topic name from env", "topic", cfg.TopicName)
4651
}
52+
if *topicFlag != "" {
53+
cfg.TopicName = *topicFlag
54+
slog.Info("topic name from flag", "topic", cfg.TopicName)
55+
}
56+
if *prefixFlag != "" {
57+
cfg.PubsubTopicPrefix = *prefixFlag
58+
slog.Info("pubsub topic prefix from flag", "prefix", cfg.PubsubTopicPrefix)
59+
}
4760

4861
m := monitor.NewMonitor(cfg)
4962
defer m.Close()

internal/monitor/monitor_models.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ import (
1313
"github.com/libp2p/go-libp2p/core/host"
1414
)
1515

16-
// shardSub bundles a PubSub topic with its subscription so that
17-
// SwitchTopic / SwitchTopicPrefix can cancel the subscription
18-
// before closing the topic (Topic.Close fails if subs are active).
16+
// shardSub bundles a PubSub topic with its subscription.
1917
type shardSub struct {
2018
topic *pubsub.Topic
2119
sub *pubsub.Subscription
@@ -106,10 +104,8 @@ type Monitor struct {
106104
mu sync.RWMutex
107105
cfg MonitorConfig
108106
appCtx context.Context // long-lived context from StartLibP2P
109-
subCtx context.Context // per-generation context; cancelled on topic switch to kill goroutines immediately
107+
subCtx context.Context // per-generation context
110108
subCancel context.CancelFunc // cancels subCtx
111-
topicPrefixOverride string // if set, overrides config.PubsubTopicPrefix for subscriptions
112-
topicNameOverride string // if set, overrides config.TopicName for subscriptions
113109
nodes map[string]*nodeState
114110
splitEvents []ShardSplitEvent
115111
treeCache *ShardTreeNode
@@ -168,11 +164,8 @@ func (m *Monitor) getTopicPrefix() string {
168164
return m.getTopicPrefixUnlocked()
169165
}
170166

171-
// getTopicPrefixUnlocked returns the effective topic prefix. Call only when holding m.mu.
167+
// getTopicPrefixUnlocked returns the topic prefix from config. Call only when holding m.mu.
172168
func (m *Monitor) getTopicPrefixUnlocked() string {
173-
if m.topicPrefixOverride != "" {
174-
return m.topicPrefixOverride
175-
}
176169
return m.cfg.PubsubTopicPrefix
177170
}
178171

@@ -183,11 +176,8 @@ func (m *Monitor) getTopicName() string {
183176
return m.getTopicNameUnlocked()
184177
}
185178

186-
// getTopicNameUnlocked returns the effective topic name. Call only when holding m.mu.
179+
// getTopicNameUnlocked returns the topic name from config. Call only when holding m.mu.
187180
func (m *Monitor) getTopicNameUnlocked() string {
188-
if m.topicNameOverride != "" {
189-
return m.topicNameOverride
190-
}
191181
return m.cfg.TopicName
192182
}
193183

internal/monitor/monitor_routes.go

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package monitor
33
import (
44
"context"
55
"encoding/base64"
6-
"encoding/json"
76
"fmt"
87
"io"
98
"log/slog"
@@ -149,25 +148,7 @@ func (m *Monitor) handleShardNodes(w http.ResponseWriter, r *http.Request) {
149148
writeJSON(w, map[string]interface{}{"shard_id": shardFilter, "shard_label": shardLabel(shardFilter), "nodes": response, "count": len(response)})
150149
}
151150

152-
func (m *Monitor) handleRootTopic(w http.ResponseWriter, r *http.Request) {
153-
if r.Method == http.MethodPost {
154-
var body struct {
155-
TopicPrefix string `json:"topic_prefix,omitempty"`
156-
TopicName string `json:"topic_name,omitempty"`
157-
}
158-
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
159-
writeJSONError(w, `invalid JSON`, http.StatusBadRequest)
160-
return
161-
}
162-
if body.TopicPrefix != "" || body.TopicName == "" {
163-
m.SwitchTopicPrefix(r.Context(), body.TopicPrefix)
164-
}
165-
if body.TopicName != "" {
166-
m.SwitchTopic(r.Context(), body.TopicName)
167-
}
168-
m.writeRootTopicResponse(w)
169-
return
170-
}
151+
func (m *Monitor) handleRootTopic(w http.ResponseWriter, _ *http.Request) {
171152
m.writeRootTopicResponse(w)
172153
}
173154

internal/monitor/monitor_subscription.go

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -361,78 +361,13 @@ func (m *Monitor) collectShardTargets() map[string]bool {
361361
return targets
362362
}
363363

364-
// closeAllShardSubsUnlocked tears down the current subscription generation:
365-
// cancels the generation context (killing all goroutines immediately), then
366-
// cancels each subscription and closes the underlying topic. A fresh
367-
// generation context is created for subsequent subscriptions.
368-
// Caller must hold m.mu.
369-
func (m *Monitor) closeAllShardSubsUnlocked() {
370-
if m.subCancel != nil {
371-
m.subCancel()
372-
}
373-
for shardID, ss := range m.shardTopics {
374-
ss.sub.Cancel()
375-
_ = ss.topic.Close()
376-
delete(m.shardTopics, shardID)
377-
}
378-
m.subCtx, m.subCancel = context.WithCancel(m.appCtx)
379-
}
380-
381-
// clearNodeStateUnlocked resets all per-network state maps so the monitor
382-
// starts fresh after a topic switch. Caller must hold m.mu.
383-
func (m *Monitor) clearNodeStateUnlocked() {
384-
m.nodes = make(map[string]*nodeState)
385-
m.splitEvents = m.splitEvents[:0]
386-
m.uniqueCIDs = make(map[string]time.Time)
387-
m.manifestReplication = make(map[string]map[string]time.Time)
388-
m.manifestShard = make(map[string]string)
389-
m.nodeFiles = make(map[string]map[string]time.Time)
390-
m.peerShardLastSeen = make(map[string]map[string]time.Time)
391-
m.treeCache = nil
392-
m.treeDirty = true
393-
}
394-
395364
// resubscribeBootstrap subscribes to all bootstrap shards in the current topic.
396365
func (m *Monitor) resubscribeBootstrap() {
397366
for _, shardID := range shardIDsUpToDepth(m.cfg.BootstrapShardDepth) {
398367
m.ensureShardSubscription(m.appCtx, shardID)
399368
}
400369
}
401370

402-
// SwitchTopicPrefix changes the topic prefix (protocol version) and
403-
// re-subscribes to the new network. Pass "" to reset to the config default.
404-
func (m *Monitor) SwitchTopicPrefix(_ context.Context, newPrefix string) {
405-
effectivePrefix := newPrefix
406-
if effectivePrefix == "" {
407-
effectivePrefix = m.cfg.PubsubTopicPrefix
408-
}
409-
m.mu.Lock()
410-
m.closeAllShardSubsUnlocked()
411-
m.topicPrefixOverride = newPrefix
412-
m.clearNodeStateUnlocked()
413-
m.mu.Unlock()
414-
415-
m.resubscribeBootstrap()
416-
slog.Info("switched topic prefix", "prefix", effectivePrefix, "shards", 1<<(m.cfg.BootstrapShardDepth+1)-1)
417-
}
418-
419-
// SwitchTopic changes the archive topic name and re-subscribes to the new
420-
// topic's shard tree. Pass "" to reset to the config default.
421-
func (m *Monitor) SwitchTopic(_ context.Context, newTopic string) {
422-
effectiveTopic := newTopic
423-
if effectiveTopic == "" {
424-
effectiveTopic = m.cfg.TopicName
425-
}
426-
m.mu.Lock()
427-
m.closeAllShardSubsUnlocked()
428-
m.topicNameOverride = newTopic
429-
m.clearNodeStateUnlocked()
430-
m.mu.Unlock()
431-
432-
m.resubscribeBootstrap()
433-
slog.Info("switched topic name", "topic", effectiveTopic, "shards", 1<<(m.cfg.BootstrapShardDepth+1)-1)
434-
}
435-
436371
func isPrivateIP(ipStr string) bool {
437372
ip := net.ParseIP(ipStr)
438373
if ip == nil {

internal/monitor/static/dashboard.html

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -69,19 +69,7 @@ <h1>D-LOCKSS Monitor</h1>
6969
<div>SYSTEM STATUS: <span class="status-text status-online">[ONLINE]</span></div>
7070
<div id="root-topic-row" style="font-size: 0.75em; color: #666; margin-top: 4px; word-break: break-all;">
7171
Topic: <span id="topic-name-value" style="font-weight:bold;">--</span>
72-
<button class="btn-text" id="topic-name-edit-btn" style="margin-left: 4px; font-size: 0.9em;">SWITCH</button>
7372
<span style="margin-left: 8px;">Prefix: <span id="root-topic-value">--</span></span>
74-
<button class="btn-text" id="root-topic-edit-btn" style="margin-left: 4px; font-size: 0.9em;">EDIT</button>
75-
</div>
76-
<div id="topic-name-edit-row" style="display: none; margin-top: 4px;">
77-
<input type="text" id="topic-name-input" placeholder="e.g. creative-commons" style="padding: 4px; font-family: inherit; font-size: 0.85em; width: 180px; border: 1px solid #333;" title="Archive topic name">
78-
<button class="btn-text btn-save" id="topic-name-save-btn">SWITCH</button>
79-
<button class="btn-text btn-cancel" id="topic-name-cancel-btn">CANCEL</button>
80-
</div>
81-
<div id="root-topic-edit-row" style="display: none; margin-top: 4px;">
82-
<input type="text" id="root-topic-input" placeholder="e.g. dlockss-v0.0.3" style="padding: 4px; font-family: inherit; font-size: 0.85em; width: 160px; border: 1px solid #333;" title="Topic prefix (protocol version)">
83-
<button class="btn-text btn-save" id="root-topic-save-btn">SAVE</button>
84-
<button class="btn-text btn-cancel" id="root-topic-cancel-btn">CANCEL</button>
8573
</div>
8674
</div>
8775
</div>
@@ -224,37 +212,6 @@ <h3 style="margin:0; text-transform:uppercase; font-size:1em;">Network Nodes</h3
224212
if (d.topic_name) { window._currentTopicName = d.topic_name; document.getElementById('topic-name-value').textContent = d.topic_name; }
225213
}).catch(() => {});
226214
}
227-
function applyTopicResponse(d) {
228-
if (d.topic_prefix) { window._currentTopicPrefix = d.topic_prefix; document.getElementById('root-topic-value').textContent = d.topic_prefix; }
229-
if (d.topic_name) { window._currentTopicName = d.topic_name; document.getElementById('topic-name-value').textContent = d.topic_name; }
230-
updateDashboard(); updateShardTree();
231-
}
232-
function showTopicNameEdit() { document.getElementById('root-topic-row').style.display = 'none'; document.getElementById('topic-name-edit-row').style.display = 'block'; document.getElementById('topic-name-input').value = window._currentTopicName || ''; document.getElementById('topic-name-input').focus(); }
233-
function hideTopicNameEdit() { document.getElementById('topic-name-edit-row').style.display = 'none'; document.getElementById('root-topic-row').style.display = ''; }
234-
function saveTopicName() {
235-
const name = document.getElementById('topic-name-input').value.trim();
236-
fetch('/api/root-topic', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic_name: name || undefined }) })
237-
.then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); })
238-
.then(d => { applyTopicResponse(d); hideTopicNameEdit(); })
239-
.catch(e => alert('Failed to switch topic: ' + e.message));
240-
}
241-
function showTopicEdit() { document.getElementById('root-topic-row').style.display = 'none'; document.getElementById('root-topic-edit-row').style.display = 'block'; document.getElementById('root-topic-input').value = window._currentTopicPrefix || ''; document.getElementById('root-topic-input').focus(); }
242-
function hideTopicEdit() { document.getElementById('root-topic-edit-row').style.display = 'none'; document.getElementById('root-topic-row').style.display = ''; }
243-
function saveTopicPrefix() {
244-
const prefix = document.getElementById('root-topic-input').value.trim();
245-
fetch('/api/root-topic', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topic_prefix: prefix || undefined }) })
246-
.then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); })
247-
.then(d => { applyTopicResponse(d); hideTopicEdit(); })
248-
.catch(e => alert('Failed to switch prefix: ' + e.message));
249-
}
250-
document.getElementById('topic-name-edit-btn').onclick = showTopicNameEdit;
251-
document.getElementById('topic-name-save-btn').onclick = saveTopicName;
252-
document.getElementById('topic-name-cancel-btn').onclick = hideTopicNameEdit;
253-
document.getElementById('topic-name-input').onkeydown = function(e) { if (e.key === 'Enter') saveTopicName(); else if (e.key === 'Escape') hideTopicNameEdit(); };
254-
document.getElementById('root-topic-edit-btn').onclick = showTopicEdit;
255-
document.getElementById('root-topic-save-btn').onclick = saveTopicPrefix;
256-
document.getElementById('root-topic-cancel-btn').onclick = hideTopicEdit;
257-
document.getElementById('root-topic-input').onkeydown = function(e) { if (e.key === 'Enter') saveTopicPrefix(); else if (e.key === 'Escape') hideTopicEdit(); };
258215
const GATEWAY = 'https://ipfs.io';
259216
function showCopyToast() {
260217
const toast = document.getElementById('copy-toast');

0 commit comments

Comments
 (0)