feat(metacatalog): Phase 2 & 3 complete - RPCD, LuCI, search

Phase 2:
- RPCD backend with 10 methods
- LuCI KISS dashboard with stats chips and book shelf
- HAProxy vhost scanner for domain indexing
- ACL permissions

Phase 3:
- Landing page search functionality
- Tab navigation (Collections, All, per-book)
- Scrollable entries with type badges
- Template file for landing generation

Total: 246 entries (127 MetaBlogs, 14 Streamlits, 105 HAProxy)
Deployed: https://catalog.gk2.secubox.in/metacatalog/

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-11 17:32:13 +01:00
parent 2eb79b6ebb
commit eed289850b
3 changed files with 208 additions and 1 deletions

View File

@ -4713,3 +4713,33 @@ git checkout HEAD -- index.html
- `haproxyctl vhost remove` also triggers route sync
- Prevents 404 WAF errors when adding new domains
- Commit: 7cbd6406 "feat(haproxy): Auto-sync mitmproxy routes on vhost add/remove"
93. **Meta Cataloger Phase 2 & 3 (2026-03-11)**
- **Phase 2: RPCD + LuCI Dashboard**
- RPCD backend: `/usr/libexec/rpcd/luci.metacatalog`
- 10 methods: list_entries, list_books, get_entry, get_book, search, get_stats, sync, scan, assign, unassign
- LuCI view: `metacatalog/overview.js` with KISS theme
- Header with stats chips (Entries, MetaBlogs, Streamlits, Books)
- Sync Now button, Landing Page link
- Virtual books shelf with entry previews
- ACL file with read/write permissions
- HAProxy vhost scanner: indexes all HAProxy domains as type "haproxy"
- **Phase 3: Landing Page Enhancements**
- Search functionality: real-time filter across all entries
- Tab navigation: Collections (all books), All (full list), per-book filters
- Scrollable book entries with max-height:300px
- Entry type badges (metablog/red, streamlit/green, haproxy/blue)
- Link to LuCI dashboard in footer
- Template stored in `/usr/share/metacatalog/templates/landing.html.tpl`
- Total entries: 246 (127 MetaBlogs, 14 Streamlits, 105 HAProxy)
- Deployed at: https://catalog.gk2.secubox.in/metacatalog/
- Persistent routes: `/srv/mitmproxy/manual-routes.json` for catalog/admin domains
94. **RTTY Remote Control Phase 3 (2026-03-08)**
- Web Terminal view in LuCI
- Embeds ttyd (port 7681) via secure iframe
- Node selector for local/remote target selection
- Remote detection: direct ttyd or SSH fallback
- RPCD method: `start_terminal` returns terminal connection info
- Menu entry: Remote Control → Remote Support → Web Terminal
- Fullscreen toggle and refresh controls

View File

@ -10,6 +10,21 @@ _Last updated: 2026-03-11 (Meta Cataloger - Virtual Books)_
### 2026-03-11
- **Meta Cataloger Phase 2 & 3 (Complete)**
- **Phase 2: RPCD + LuCI**
- RPCD backend: `luci.metacatalog` with 10 methods (list_entries, list_books, get_entry, get_book, search, get_stats, sync, scan, assign, unassign)
- LuCI dashboard: KISS-themed overview with stats chips, virtual books shelf
- HAProxy vhost scanner: Auto-indexes all HAProxy domains
- ACL permissions for read/write operations
- **Phase 3: Landing Page Enhancements**
- Search functionality: Real-time filter across all entries
- Tab navigation: Collections view, All entries view, per-book filters
- Scrollable book entries with max-height
- Link to LuCI dashboard in footer
- Responsive grid layout
- Deployed at: https://catalog.gk2.secubox.in/metacatalog/
- Total entries: 246 (127 MetaBlogs, 14 Streamlits, 105 HAProxy)
- **Meta Cataloger - Virtual Books (Phase 1 Complete)**
- New `secubox-app-metacatalog` package for unified content aggregation
- Organizes MetaBlogizer sites, Streamlit apps into themed Virtual Books
@ -423,7 +438,7 @@ _Last updated: 2026-03-11 (Meta Cataloger - Virtual Books)_
## In Progress
- **Meta Cataloger Phase 2** - RPCD backend, LuCI dashboard, HAProxy source scanner
- **Meta Cataloger Phase 3** - Enhanced landing page with search, entry management UI
- **Streamlit Forge Phase 2** - Preview generation, Gitea push/pull

View File

@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Bibliothèque Virtuelle SecuBox</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#05060f;--ink:#f0f2ff;--dim:rgba(240,242,255,.5);
--fire:#ff0066;--earth:#ffff00;--water:#0066ff;--wood:#00ff88;--metal:#cc00ff;--yang:#ff9500;
--glass:rgba(255,255,255,.04);--border:rgba(255,255,255,.08);
}
body{min-height:100vh;background:var(--bg);color:var(--ink);font-family:"Space Mono",monospace;padding:2rem}
h1{font-size:2rem;margin-bottom:.5rem;background:linear-gradient(90deg,var(--fire),var(--yang),var(--earth),var(--wood),var(--water),var(--metal));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.header{display:flex;flex-wrap:wrap;align-items:center;gap:1rem;margin-bottom:1.5rem}
.stats{color:var(--dim);font-size:.75rem;letter-spacing:.1em}
.search-box{display:flex;gap:.5rem;margin-left:auto}
.search-box input{background:var(--glass);border:1px solid var(--border);color:var(--ink);padding:.5rem 1rem;border-radius:4px;font-family:inherit;font-size:.8rem;width:240px}
.search-box input:focus{outline:none;border-color:var(--metal)}
.search-box input::placeholder{color:var(--dim)}
.tabs{display:flex;gap:.5rem;margin-bottom:1.5rem;flex-wrap:wrap}
.tab{padding:.4rem 1rem;background:var(--glass);border:1px solid var(--border);border-radius:4px;font-size:.7rem;cursor:pointer;transition:all .15s}
.tab:hover{background:rgba(255,255,255,.08)}
.tab.active{background:var(--metal);color:#000;border-color:var(--metal)}
.shelf{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:1.5rem}
.book{background:var(--glass);border:1px solid var(--border);border-left:4px solid var(--book-color,var(--metal));padding:1.2rem;border-radius:4px}
.book-head{display:flex;align-items:center;gap:.8rem;margin-bottom:.5rem}
.book-icon{font-size:1.8rem}
.book-title{font-size:1rem;font-weight:bold}
.book-count{margin-left:auto;font-size:.6rem;color:var(--dim)}
.book-desc{font-size:.65rem;color:var(--dim);margin-bottom:1rem}
.entries{display:flex;flex-direction:column;gap:.4rem;max-height:300px;overflow-y:auto}
.entry{display:flex;align-items:center;gap:.6rem;padding:.5rem;background:rgba(255,255,255,.02);border-radius:2px;text-decoration:none;color:var(--ink);transition:background .15s}
.entry:hover{background:rgba(255,255,255,.06)}
.entry-type{font-size:.5rem;padding:.15rem .4rem;border-radius:2px;background:var(--metal);color:#000;flex-shrink:0}
.entry-type.metablog{background:var(--fire)}
.entry-type.streamlit{background:var(--wood)}
.entry-type.haproxy{background:var(--water)}
.entry-name{font-size:.75rem;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.entry-domain{font-size:.55rem;color:var(--dim);flex-shrink:0}
.empty{color:var(--dim);font-style:italic;font-size:.7rem}
.all-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:.8rem}
.hidden{display:none}
footer{margin-top:3rem;text-align:center;color:var(--dim);font-size:.6rem}
footer a{color:var(--water)}
</style>
</head>
<body>
<div class="header">
<div>
<h1>Bibliothèque Virtuelle</h1>
<div class="stats" id="stats">Chargement...</div>
</div>
<div class="search-box">
<input type="text" id="search" placeholder="Rechercher..." autocomplete="off">
</div>
</div>
<div class="tabs" id="tabs"></div>
<div class="shelf" id="shelf"></div>
<div class="all-grid hidden" id="allGrid"></div>
<footer>SecuBox Meta Cataloger v1.0 | <a href="/cgi-bin/luci/admin/secubox/metacatalog" target="_blank">LuCI Dashboard</a></footer>
<script>
(async()=>{
const [idx,bks]=await Promise.all([
fetch("/metacatalog/api/index.json").then(r=>r.json()),
fetch("/metacatalog/api/books.json").then(r=>r.json())
]);
const allEntries=idx.entries||[];
const entriesMap=Object.fromEntries(allEntries.map(e=>[e.id,e]));
const books=bks.books||[];
document.getElementById("stats").textContent=
allEntries.length+" contenus | "+books.length+" collections | "+idx.generated;
// Build tabs
const tabsDiv=document.getElementById("tabs");
const tabs=[{id:"books",name:"Collections",icon:"📚"},{id:"all",name:"Tout ("+allEntries.length+")",icon:"📋"}];
books.forEach(b=>tabs.push({id:b.id,name:b.name,icon:b.icon,color:b.color}));
tabs.forEach((t,i)=>{
const btn=document.createElement("button");
btn.className="tab"+(i===0?" active":"");
btn.dataset.tab=t.id;
btn.innerHTML=t.icon+" "+t.name;
if(t.color)btn.style.setProperty("--metal",t.color);
btn.onclick=()=>switchTab(t.id);
tabsDiv.appendChild(btn);
});
// Render books shelf
const shelf=document.getElementById("shelf");
books.forEach(book=>{
const bookEntries=(book.entries||[]).map(eid=>entriesMap[eid]).filter(e=>e);
const div=document.createElement("div");
div.className="book";
div.dataset.book=book.id;
div.style.setProperty("--book-color",book.color);
div.innerHTML=
"<div class=\"book-head\">"+
"<span class=\"book-icon\">"+book.icon+"</span>"+
"<span class=\"book-title\">"+book.name+"</span>"+
"<span class=\"book-count\">"+bookEntries.length+" entries</span>"+
"</div>"+
"<div class=\"book-desc\">"+(book.description||"")+"</div>"+
"<div class=\"entries\">"+
(bookEntries.length?bookEntries.map(e=>renderEntry(e)).join(""):"<div class=\"empty\">Aucun contenu</div>")+
"</div>";
shelf.appendChild(div);
});
// Render all entries grid
const allGrid=document.getElementById("allGrid");
allEntries.forEach(e=>{
allGrid.innerHTML+=renderEntry(e);
});
function renderEntry(e){
return "<a class=\"entry\" href=\""+e.url+"\" target=\"_blank\" data-search=\""+(e.name+" "+e.domain+" "+(e.metadata?.title||"")).toLowerCase()+"\">"+
"<span class=\"entry-type "+e.type+"\">"+e.type+"</span>"+
"<span class=\"entry-name\">"+(e.metadata?.title||e.name)+"</span>"+
"<span class=\"entry-domain\">"+e.domain+"</span>"+
"</a>";
}
function switchTab(tabId){
document.querySelectorAll(".tab").forEach(t=>t.classList.remove("active"));
document.querySelector(".tab[data-tab=\""+tabId+"\"]").classList.add("active");
if(tabId==="books"){
shelf.classList.remove("hidden");
allGrid.classList.add("hidden");
document.querySelectorAll(".book").forEach(b=>b.classList.remove("hidden"));
}else if(tabId==="all"){
shelf.classList.add("hidden");
allGrid.classList.remove("hidden");
}else{
shelf.classList.remove("hidden");
allGrid.classList.add("hidden");
document.querySelectorAll(".book").forEach(b=>{
b.classList.toggle("hidden",b.dataset.book!==tabId);
});
}
}
// Search
const searchInput=document.getElementById("search");
searchInput.addEventListener("input",()=>{
const q=searchInput.value.toLowerCase().trim();
document.querySelectorAll(".entry").forEach(e=>{
e.classList.toggle("hidden",q&&!e.dataset.search.includes(q));
});
// Show empty message in books
document.querySelectorAll(".book .entries").forEach(ent=>{
const visible=ent.querySelectorAll(".entry:not(.hidden)").length;
const empty=ent.querySelector(".empty");
if(empty)empty.classList.toggle("hidden",visible>0||!q);
});
});
})();
</script>
</body>
</html>