Skip to content

Commit e4a5fd4

Browse files
authored
add unique CID file export function to monitor (#51)
* re-run bootstrap if all nodes are lost * fixed inconsitent docu * removed compose v2 syntax * dlockss monitor to allow export of unique CID list * ci fix
1 parent 5216399 commit e4a5fd4

7 files changed

Lines changed: 70 additions & 16 deletions

File tree

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ services:
9999
# DLOCKSS_NODE_NAME: my-node # human-readable name shown in the monitor;
100100
# # if empty the peer ID is displayed instead
101101
volumes:
102-
- dlockss-data:/data # persistent D-LOCKSS data (identity, cluster state, ingested files)
102+
- ./dlockss-files:/data # persistent D-LOCKSS data (identity, cluster state, ingested files)
103103
- ipfs-data:/ipfs-repo:ro # read-only access to Kubo config for identity
104104
depends_on:
105105
- ipfs
@@ -138,7 +138,6 @@ services:
138138
volumes:
139139
ipfs-staging: # IPFS staging area on /export
140140
ipfs-data: # IPFS repo on /data/ipfs (shared read-only with D-LOCKSS for identity)
141-
dlockss-data: # persistent D-LOCKSS data (identity key, cluster state, ingested files)
142141
```
143142
144143
See [docs/DLOCKSS_PROTOCOL.md](docs/DLOCKSS_PROTOCOL.md) for protocol details.

cmd/dlockss-monitor/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ func main() {
4444
cfg.PubsubTopicPrefix = v
4545
slog.Info("pubsub topic prefix from env", "prefix", cfg.PubsubTopicPrefix)
4646
}
47+
if v := os.Getenv("DLOCKSS_TOPIC_NAME"); v != "" {
48+
cfg.TopicName = v
49+
slog.Info("topic name from env", "topic", cfg.TopicName)
50+
}
4751

4852
geoDBPath := *geoipDB
4953
if geoDBPath == "" {

internal/keywords/keywords.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,20 @@ func (s *Store) GetRecentSearches() []RecentSearch {
827827
return result
828828
}
829829

830+
// PayloadCIDs returns all known payload CIDs from the keyword index.
831+
func (s *Store) PayloadCIDs() []string {
832+
s.mu.RLock()
833+
defer s.mu.RUnlock()
834+
out := make([]string, 0, len(s.cidKeywords))
835+
for _, entry := range s.cidKeywords {
836+
if entry.PayloadCID != "" {
837+
out = append(out, entry.PayloadCID)
838+
}
839+
}
840+
sort.Strings(out)
841+
return out
842+
}
843+
830844
// GetStats returns keyword extraction progress statistics.
831845
func (s *Store) GetStats(totalUniqueCIDs int) Stats {
832846
s.mu.RLock()

internal/monitor/monitor_models.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,19 @@ type MonitorConfig struct {
3939
NodeCleanupTimeout time.Duration
4040
BootstrapShardDepth int
4141
PubsubTopicPrefix string
42+
TopicName string
4243
}
4344

4445
const DefaultPubsubTopicPrefix = "dlockss-v0.0.3"
4546

47+
const DefaultTopicName = "creative-commons"
48+
4649
func DefaultMonitorConfig() MonitorConfig {
4750
return MonitorConfig{
4851
NodeCleanupTimeout: DefaultNodeCleanupTimeout,
4952
BootstrapShardDepth: DefaultBootstrapShardDepth,
5053
PubsubTopicPrefix: DefaultPubsubTopicPrefix,
54+
TopicName: DefaultTopicName,
5155
}
5256
}
5357

@@ -62,7 +66,7 @@ type NodeState struct {
6266
PeerID string `json:"peer_id"`
6367
NodeName string `json:"node_name,omitempty"`
6468
CurrentShard string `json:"current_shard"`
65-
Role string `json:"role,omitempty"` // ACTIVE, PASSIVE, or PROBE (empty = ACTIVE)
69+
Role string `json:"role,omitempty"` // ACTIVE, PASSIVE, REPLICATOR, or PROBE (empty = ACTIVE)
6670
PinnedFiles int `json:"pinned_files"`
6771
KnownFiles int `json:"known_files"`
6872
LastSeen time.Time `json:"last_seen"`

internal/monitor/monitor_routes.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func (m *Monitor) RegisterRoutes(mux *http.ServeMux) {
2929
mux.HandleFunc("/api/root-topic", m.handleRootTopic)
3030
mux.HandleFunc("/api/node-files", m.handleNodeFiles)
3131
mux.HandleFunc("/api/unique-cids", m.handleUniqueCIDs)
32+
mux.HandleFunc("/api/payload-cids-export", m.handlePayloadCIDsExport)
3233
mux.HandleFunc("/api/replication", m.handleReplication)
3334
mux.HandleFunc("/api/replication-cids", m.handleReplicationCIDs)
3435
mux.HandleFunc("/api/manifest-payload", m.handleManifestPayload)
@@ -172,11 +173,11 @@ func (m *Monitor) handleRootTopic(w http.ResponseWriter, r *http.Request) {
172173
return
173174
}
174175
m.SwitchTopicPrefix(r.Context(), body.TopicPrefix)
175-
rootTopic := fmt.Sprintf("%s-creative-commons-shard-", m.getTopicPrefix())
176+
rootTopic := fmt.Sprintf("%s-%s-shard-", m.getTopicPrefix(), m.cfg.TopicName)
176177
json.NewEncoder(w).Encode(map[string]string{"root_topic": rootTopic, "topic_prefix": m.getTopicPrefix()})
177178
return
178179
}
179-
rootTopic := fmt.Sprintf("%s-creative-commons-shard-", m.getTopicPrefix())
180+
rootTopic := fmt.Sprintf("%s-%s-shard-", m.getTopicPrefix(), m.cfg.TopicName)
180181
json.NewEncoder(w).Encode(map[string]string{"root_topic": rootTopic, "topic_prefix": m.getTopicPrefix()})
181182
}
182183

@@ -206,6 +207,16 @@ func (m *Monitor) handleUniqueCIDs(w http.ResponseWriter, r *http.Request) {
206207
json.NewEncoder(w).Encode(map[string]interface{}{"cids": entries, "count": len(entries)})
207208
}
208209

210+
func (m *Monitor) handlePayloadCIDsExport(w http.ResponseWriter, r *http.Request) {
211+
cids := m.keywords.PayloadCIDs()
212+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
213+
w.Header().Set("Content-Disposition", "attachment; filename=\"payload_cids.txt\"")
214+
for _, c := range cids {
215+
w.Write([]byte(c))
216+
w.Write([]byte("\n"))
217+
}
218+
}
219+
209220
func (m *Monitor) handleReplication(w http.ResponseWriter, r *http.Request) {
210221
if r.Method != http.MethodGet {
211222
writeJSONError(w, "method not allowed", http.StatusMethodNotAllowed)

internal/monitor/monitor_subscription.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (m *Monitor) ensureShardSubscriptionUnlocked(ctx context.Context, shardID s
7373
if _, exists := m.shardTopics[shardID]; exists {
7474
return
7575
}
76-
topicName := fmt.Sprintf("%s-creative-commons-shard-%s", m.getTopicPrefixUnlocked(), shardID)
76+
topicName := fmt.Sprintf("%s-%s-shard-%s", m.getTopicPrefixUnlocked(), m.cfg.TopicName, shardID)
7777
topic, err := m.ps.Join(topicName)
7878
if err != nil {
7979
if errors.Is(err, context.Canceled) || strings.Contains(err.Error(), "context canceled") {
@@ -134,6 +134,8 @@ func (m *Monitor) handleShardMessages(ctx context.Context, sub *pubsub.Subscript
134134
role = "PASSIVE"
135135
case "PROBE":
136136
role = "PROBE"
137+
case "REPLICATOR":
138+
role = "REPLICATOR"
137139
}
138140
}
139141
nodeName := ""
@@ -189,8 +191,13 @@ func (m *Monitor) handleShardMessages(ctx context.Context, sub *pubsub.Subscript
189191
parts := strings.SplitN(string(msg.Data[5:]), ":", 3)
190192
peerIDStr := strings.TrimSpace(parts[0])
191193
role := "ACTIVE"
192-
if len(parts) >= 2 && strings.ToUpper(parts[1]) == "PASSIVE" {
193-
role = "PASSIVE"
194+
if len(parts) >= 2 {
195+
switch strings.ToUpper(parts[1]) {
196+
case "PASSIVE":
197+
role = "PASSIVE"
198+
case "REPLICATOR":
199+
role = "REPLICATOR"
200+
}
194201
}
195202
nodeName := ""
196203
if len(parts) >= 3 {

internal/monitor/static/dashboard.html

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@
5050
.modal { background: white; border: 2px solid #333; max-width: 90vw; max-height: 85vh; overflow: hidden; display: flex; flex-direction: column; }
5151
.modal-header { padding: 15px 20px; border-bottom: 1px solid #333; display: flex; justify-content: space-between; align-items: center; }
5252
.modal-body { padding: 20px; overflow-y: auto; flex: 1; }
53-
.cid-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid #eee; font-size: 0.85em; flex-wrap: wrap; }
54-
.cid-row .cid-value { font-family: monospace; word-break: break-all; flex: 1; min-width: 200px; }
55-
.cid-row .cid-meta { color: #666; font-size: 0.9em; }
53+
.cid-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid #eee; font-size: 0.85em; flex-wrap: nowrap; }
54+
.cid-row .cid-value { font-family: monospace; white-space: nowrap; flex: 1; min-width: 200px; cursor: pointer; }
55+
.cid-row .cid-value:hover { color: #06A77D; }
56+
.cid-row .cid-meta { color: #666; font-size: 0.9em; white-space: nowrap; }
57+
.copy-toast { position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); background: #333; color: #fff; padding: 8px 20px; font-size: 0.85em; z-index: 2000; opacity: 0; transition: opacity 0.3s; pointer-events: none; }
5658
.cid-row .cid-actions { display: flex; gap: 6px; flex-shrink: 0; }
5759
.cid-payload { margin-top: 4px; font-size: 0.8em; color: #06A77D; }
5860
.load-spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid #ccc; border-top-color: #06A77D; border-radius: 50%; animation: spin 0.8s linear infinite; vertical-align: middle; }
@@ -147,9 +149,10 @@ <h3 style="margin:0; text-transform:uppercase; font-size:1em;">Network Nodes</h3
147149
<table id="nodeTable"><thead><tr><th style="width: 80px;">Action</th><th>Peer ID</th><th>Shard</th><th>Peers</th><th>Pinned</th><th>Known</th><th>Uptime</th><th>Last Seen</th></tr></thead><tbody id="nodeTableBody"></tbody></table>
148150
</div>
149151
</div>
152+
<div id="copy-toast" class="copy-toast">Copied to clipboard</div>
150153
<div id="cids-modal" class="modal-overlay" style="display: none;">
151-
<div class="modal" style="width: 700px;">
152-
<div class="modal-header"><h3 style="margin:0; text-transform: uppercase; font-size: 1em;">Unique CIDs</h3><input type="text" id="cids-modal-search" placeholder="Filter CIDs..." style="padding: 6px; font-family: inherit; font-size: 0.85em; width: 180px; border: 1px solid #333;"><button class="btn-text" id="cids-modal-close">CLOSE</button></div>
154+
<div class="modal" style="width: 1100px;">
155+
<div class="modal-header"><h3 style="margin:0; text-transform: uppercase; font-size: 1em;">Unique CIDs</h3><div style="display:flex; gap:8px; align-items:center;"><input type="text" id="cids-modal-search" placeholder="Filter CIDs..." style="padding: 6px; font-family: inherit; font-size: 0.85em; width: 180px; border: 1px solid #333;"><button class="btn-text" id="cids-export-btn" title="Export payload CIDs as text file">EXPORT PAYLOAD CIDS</button><button class="btn-text" id="cids-modal-close">CLOSE</button></div></div>
153156
<div class="modal-body"><div id="cids-modal-list"></div></div>
154157
</div>
155158
</div>
@@ -273,18 +276,29 @@ <h3 style="margin:0; text-transform:uppercase; font-size:1em;">Network Nodes</h3
273276
document.getElementById('root-topic-cancel-btn').onclick = hideTopicEdit;
274277
document.getElementById('root-topic-input').onkeydown = function(e) { if (e.key === 'Enter') saveTopicPrefix(); else if (e.key === 'Escape') hideTopicEdit(); };
275278
const GATEWAY = 'https://ipfs.io';
279+
function showCopyToast() {
280+
const toast = document.getElementById('copy-toast');
281+
toast.style.opacity = '1';
282+
setTimeout(function() { toast.style.opacity = '0'; }, 1200);
283+
}
284+
function copyCID(text) {
285+
navigator.clipboard.writeText(text).then(showCopyToast).catch(function() {});
286+
}
276287
function showCidsModal() {
277288
document.getElementById('cids-modal').style.display = 'flex';
278289
fetch('/api/unique-cids?t=' + Date.now()).then(r=>r.json()).then(data => {
279290
const list = document.getElementById('cids-modal-list');
280291
const cids = data.cids || [];
281292
list.innerHTML = cids.length === 0 ? '<p style="color:#666;">No CIDs yet. CIDs appear when nodes announce pinned files.</p>' : cids.map((entry, i) => {
282293
const cid = entry.cid || entry.CID || entry; const shard = (entry.shard || entry.Shard || '') === '' ? 'root' : (entry.shard || entry.Shard); const replicas = entry.replicas ?? entry.Replicas ?? 0;
283-
return '<div class="cid-row" data-cid="' + escapeHtml(cid) + '"><div class="cid-value">' + escapeHtml(cid) + '</div><div class="cid-meta">Shard: ' + escapeHtml(shard) + ' | Replicas: ' + replicas + '</div><div class="cid-actions"><button class="btn-text" onclick="loadManifest(\'' + escapeJs(cid) + '\', this.parentElement)">LOAD MANIFEST</button></div></div>';
294+
return '<div class="cid-row" data-cid="' + escapeHtml(cid) + '"><div class="cid-value" onclick="copyCID(\'' + escapeJs(cid) + '\')" title="Click to copy">' + escapeHtml(cid) + '</div><div class="cid-meta">Shard: ' + escapeHtml(shard) + ' | Replicas: ' + replicas + '</div><div class="cid-actions"><button class="btn-text" onclick="loadManifest(\'' + escapeJs(cid) + '\', this.parentElement)">LOAD MANIFEST</button></div></div>';
284295
}).join('');
285296
}).catch(() => { document.getElementById('cids-modal-list').innerHTML = '<p style="color:#999;">Failed to load CIDs.</p>'; });
286297
}
287298
function hideCidsModal() { document.getElementById('cids-modal').style.display = 'none'; }
299+
function exportPayloadCIDs() {
300+
window.open('/api/payload-cids-export', '_blank');
301+
}
288302
function showNodeFilesModal(peerId) {
289303
const aliases = loadAliases();
290304
const label = aliases[peerId] || (window._nodeNames && window._nodeNames[peerId]) || peerId.slice(-6);
@@ -296,7 +310,7 @@ <h3 style="margin:0; text-transform:uppercase; font-size:1em;">Network Nodes</h3
296310
const cids = data.cids || [];
297311
list.innerHTML = cids.length === 0 ? '<p style="color:#666;">No pinned files for this node.</p>' : cids.map((entry, i) => {
298312
const cid = entry.cid || entry.CID || entry; const shard = (entry.shard || entry.Shard || '') === '' ? 'root' : (entry.shard || entry.Shard); const replicas = entry.replicas ?? entry.Replicas ?? 0;
299-
return '<div class="cid-row" data-cid="' + escapeHtml(cid) + '"><div class="cid-value">' + escapeHtml(cid) + '</div><div class="cid-meta">Shard: ' + escapeHtml(shard) + ' | Replicas: ' + replicas + '</div><div class="cid-actions"><button class="btn-text" onclick="loadManifest(\'' + escapeJs(cid) + '\', this.parentElement)">LOAD MANIFEST</button></div></div>';
313+
return '<div class="cid-row" data-cid="' + escapeHtml(cid) + '"><div class="cid-value" onclick="copyCID(\'' + escapeJs(cid) + '\')" title="Click to copy">' + escapeHtml(cid) + '</div><div class="cid-meta">Shard: ' + escapeHtml(shard) + ' | Replicas: ' + replicas + '</div><div class="cid-actions"><button class="btn-text" onclick="loadManifest(\'' + escapeJs(cid) + '\', this.parentElement)">LOAD MANIFEST</button></div></div>';
300314
}).join('');
301315
}).catch(() => { document.getElementById('node-files-modal-list').innerHTML = '<p style="color:#999;">Failed to load node files.</p>'; });
302316
}
@@ -311,7 +325,7 @@ <h3 style="margin:0; text-transform:uppercase; font-size:1em;">Network Nodes</h3
311325
const cids = data.cids || [];
312326
list.innerHTML = cids.length === 0 ? '<p style="color:#666;">No files at this replication level.</p>' : cids.map((entry, i) => {
313327
const cid = entry.cid || entry.CID || entry; const shard = (entry.shard || entry.Shard || '') === '' ? 'root' : (entry.shard || entry.Shard); const replicas = entry.replicas ?? entry.Replicas ?? 0;
314-
return '<div class="cid-row" data-cid="' + escapeHtml(cid) + '"><div class="cid-value">' + escapeHtml(cid) + '</div><div class="cid-meta">Shard: ' + escapeHtml(shard) + ' | Replicas: ' + replicas + '</div><div class="cid-actions"><button class="btn-text" onclick="loadManifest(\'' + escapeJs(cid) + '\', this.parentElement)">LOAD MANIFEST</button></div></div>';
328+
return '<div class="cid-row" data-cid="' + escapeHtml(cid) + '"><div class="cid-value" onclick="copyCID(\'' + escapeJs(cid) + '\')" title="Click to copy">' + escapeHtml(cid) + '</div><div class="cid-meta">Shard: ' + escapeHtml(shard) + ' | Replicas: ' + replicas + '</div><div class="cid-actions"><button class="btn-text" onclick="loadManifest(\'' + escapeJs(cid) + '\', this.parentElement)">LOAD MANIFEST</button></div></div>';
315329
}).join('');
316330
}).catch(() => { document.getElementById('replication-modal-list').innerHTML = '<p style="color:#999;">Failed to load CIDs.</p>'; });
317331
}
@@ -398,6 +412,7 @@ <h3 style="margin:0; text-transform:uppercase; font-size:1em;">Network Nodes</h3
398412
alert('Failed to load manifest');
399413
});
400414
}
415+
document.getElementById('cids-export-btn').onclick = exportPayloadCIDs;
401416
document.getElementById('unique-cids-card').onclick = showCidsModal;
402417
document.getElementById('total-nodes-card').onclick = function() { document.getElementById('network-nodes-section').scrollIntoView({ behavior: 'smooth' }); };
403418
document.getElementById('total-shards-card').onclick = function() { document.getElementById('shard-topology-section').scrollIntoView({ behavior: 'smooth' }); };

0 commit comments

Comments
 (0)