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:
parent
2eb79b6ebb
commit
eed289850b
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user