BREAKING CHANGE: Complete refactor of architecture to prepare BJORN V2 release, APIs, assets, and UI, webapp, logics, attacks, a lot of new features...
0
web/__init__.py
Normal file
939
web/actions_launcher.html
Normal file
@@ -0,0 +1,939 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Bjorn - Actions Launcher</title>
|
||||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon" />
|
||||
<link rel="stylesheet" href="web/css/global.css" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="web/images/icon-192x192.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#0b0d10" />
|
||||
<script src="web/js/global.js" defer></script>
|
||||
|
||||
<style>
|
||||
/* =========================================================
|
||||
Actions Launcher — scoped CSS (plays nice with global.css)
|
||||
---------------------------------------------------------
|
||||
• No layout override of .sidebar/.main positions.
|
||||
• Uses global tokens (colors, radii, borders, shadows).
|
||||
• Mobile = single console, toolbar hidden.
|
||||
========================================================= */
|
||||
|
||||
/* Page container (center column only) */
|
||||
#actionsLauncher{
|
||||
min-height:100%;
|
||||
display:grid;
|
||||
grid-template-columns:1fr;
|
||||
gap:var(--gap-3, 10px);
|
||||
}
|
||||
|
||||
/* Panel look consistent with cards/console surfaces */
|
||||
.panel{
|
||||
background: var(--grad-card, var(--c-panel));
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius, 14px);
|
||||
box-shadow: var(--elev, 0 10px 30px var(--acid-1a, #00ff9a1a), inset 0 0 0 1px var(--acid-22, #00ff9a22));
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
/* ---------- Sidebar (only inner content styles) ---------- */
|
||||
.sidebar .sideheader{ padding:10px 10px 6px; border-bottom:1px dashed var(--c-border); }
|
||||
.tabs-container{ display:flex; gap:8px; flex-wrap:wrap; }
|
||||
.tab-btn{
|
||||
all:unset; cursor:pointer; padding:6px 10px; border-radius:10px;
|
||||
background:var(--c-pill-bg); border:1px solid var(--c-border); color:var(--muted);
|
||||
}
|
||||
.tab-btn.active{
|
||||
background:var(--grad-chip-selected);
|
||||
outline:2px solid color-mix(in oklab, var(--acid) 55%, transparent);
|
||||
outline-offset:0;
|
||||
}
|
||||
.sidebar .search{ display:flex; gap:10px; padding:10px; }
|
||||
.sidebar .input{
|
||||
flex:1; background:var(--c-panel); border:1px solid var(--c-border-strong);
|
||||
color:var(--ink); padding:10px 12px; border-radius:var(--control-r,10px); font:inherit;
|
||||
}
|
||||
.sidebar .input:focus{ outline:none; box-shadow:0 0 0 2px color-mix(in oklab, var(--acid) 55%, transparent) inset; }
|
||||
.sidecontent{ padding:8px; overflow:auto; }
|
||||
|
||||
/* Action list */
|
||||
.sidebar .list{ display:flex; flex-direction:column; gap:10px; padding-right:4px; }
|
||||
.sidebar .row{
|
||||
position:relative; display:grid; grid-template-columns:84px 1fr;
|
||||
gap:12px; padding:10px; background:var(--c-panel-2); border-radius:12px; cursor:pointer;
|
||||
transition:transform .15s ease, border-color .15s ease, box-shadow .15s ease;
|
||||
}
|
||||
.sidebar .row:hover{
|
||||
transform:translateY(-1px);
|
||||
border-color:color-mix(in oklab, var(--accent) 25%, var(--c-border));
|
||||
box-shadow:0 10px 26px var(--glow-weak);
|
||||
}
|
||||
.sidebar .row .ic{
|
||||
width:84px; height:84px; display:grid; place-items:center;
|
||||
border-radius:12px; background:var(--c-panel); overflow:hidden;
|
||||
}
|
||||
.ic-img{ width:70px; height:70px; object-fit:cover; display:block; }
|
||||
.sidebar .row > div:nth-child(2){ min-width:0; display:flex; flex-direction:column; gap:4px; }
|
||||
.name{ font-weight:800; color:var(--acid-2); font-size:14px; line-height:1.2; }
|
||||
.desc{ color:var(--muted); font-size:13px; line-height:1.25; }
|
||||
.sidebar .row .chip{
|
||||
position:absolute; top:6px; left:calc(84px/2 + 10px); transform:translateX(-50%);
|
||||
padding:2px 8px; border-radius:999px; border:1px solid var(--c-border);
|
||||
background:var(--c-chip-bg); color:var(--muted); font-size:11px; line-height:1; pointer-events:none;
|
||||
}
|
||||
.chip.ok{ color:var(--ok); border-color:color-mix(in oklab, var(--ok) 60%, transparent); }
|
||||
.chip.err{ color:var(--danger); border-color:color-mix(in oklab, var(--danger) 60%, transparent); }
|
||||
.chip.run{ color:var(--acid); border-color:color-mix(in oklab, var(--acid) 60%, transparent); }
|
||||
|
||||
/* ---------- Center area ---------- */
|
||||
.center{ display:flex; flex-direction:column; min-height:50vh; }
|
||||
|
||||
/* Secondary toolbar (split controls). Hidden on mobile. */
|
||||
.toolbar2{
|
||||
display:flex; align-items:center; gap:10px; padding:10px;
|
||||
border-bottom:1px solid var(--c-border);
|
||||
background:linear-gradient(180deg, color-mix(in oklab, var(--acid-2) 12%, transparent), transparent);
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.seg{ display:flex; border-radius:10px; overflow:hidden; border:1px solid var(--c-border); }
|
||||
.seg button{
|
||||
background:var(--c-panel); color:var(--muted);
|
||||
padding:8px 10px; border:none; border-right:1px solid var(--c-border);
|
||||
cursor:pointer; font:inherit;
|
||||
}
|
||||
.seg button:last-child{ border-right:none; }
|
||||
.seg button.active{
|
||||
color:var(--ink-invert);
|
||||
background:linear-gradient(90deg, var(--acid-2), color-mix(in oklab, var(--acid-2) 60%, white));
|
||||
}
|
||||
|
||||
.btn{
|
||||
background:var(--c-btn); color:var(--ink); border:1px solid var(--c-border-strong);
|
||||
border-radius:var(--control-r,10px); padding:8px 12px;
|
||||
display:inline-flex; align-items:center; gap:8px; cursor:pointer; transition:.18s;
|
||||
box-shadow:var(--elev); font:inherit;
|
||||
}
|
||||
.btn:hover{ transform:translateY(-1px); box-shadow:var(--shadow-hover); }
|
||||
.btn.warn{
|
||||
background:linear-gradient(180deg, color-mix(in oklab, var(--warning) 28%, var(--c-btn)), var(--c-btn));
|
||||
color:var(--warning); border-color:color-mix(in oklab, var(--warning) 55%, var(--c-border));
|
||||
}
|
||||
|
||||
/* Multi-console grid */
|
||||
.multiConsole{
|
||||
flex:1; padding:10px; display:grid; gap:10px; height:100%;
|
||||
grid-auto-flow:row; grid-auto-rows:1fr;
|
||||
grid-template-rows:repeat(var(--rows,1), 1fr);
|
||||
}
|
||||
.split-1{ grid-template-columns:1fr; }
|
||||
.split-2{ grid-template-columns:1fr 1fr; }
|
||||
.split-3{ grid-template-columns:1fr 1fr 1fr; }
|
||||
.split-4{ grid-template-columns:1fr 1fr; } /* 2x2 */
|
||||
|
||||
/* Console pane */
|
||||
.pane{
|
||||
position:relative; border:1px solid var(--c-border);
|
||||
border-radius:12px; background:var(--grad-console);
|
||||
display:flex; flex-direction:column;
|
||||
box-shadow:inset 0 0 0 1px var(--c-border-muted);
|
||||
}
|
||||
|
||||
/* Clean two-column header: title/meta | actions */
|
||||
.paneHeader{
|
||||
display:grid; grid-template-columns:1fr auto; align-items:center;
|
||||
gap:10px; padding:8px 10px; border-bottom:1px solid var(--c-border);
|
||||
background:linear-gradient(180deg, color-mix(in oklab, var(--acid-2) 8%, transparent), transparent);
|
||||
}
|
||||
|
||||
/* Left side: dot + icon + stacked title/meta */
|
||||
.paneTitle{
|
||||
display:grid; grid-template-columns:auto auto 1fr; align-items:center; gap:10px; min-width:0;
|
||||
}
|
||||
.paneTitle .dot{ width:8px; height:8px; border-radius:50%; flex:0 0 auto; }
|
||||
.paneIcon{ width:70px; height:70px; border-radius:6px; object-fit:cover; opacity:.95; }
|
||||
.titleBlock{ display:flex; flex-direction:column; gap:4px; min-width:0; }
|
||||
.titleLine strong{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.metaLine{ display:flex; flex-wrap:wrap; gap:6px; }
|
||||
.metaLine .chip{ border:1px solid var(--c-border-strong); background:var(--c-chip-bg); color:var(--muted); padding:3px 8px; border-radius:999px; }
|
||||
|
||||
/* Right side: actions wrap neatly */
|
||||
.paneBtns{ display:flex; flex-wrap:wrap; gap:8px; justify-content:flex-end; }
|
||||
.paneBtns .btn{ padding:6px 8px; font-size:.9rem; }
|
||||
|
||||
.paneLog{
|
||||
flex:1; overflow:auto; padding:6px 8px;
|
||||
font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-size:.92rem;
|
||||
}
|
||||
|
||||
/* Log colors */
|
||||
.logline{ white-space:pre-wrap; word-break:break-word; padding:4px 6px; line-height:1.32; color:var(--ink); }
|
||||
.logline.info{ color:#bfefff; }
|
||||
.logline.ok{ color:#9ff7c5; }
|
||||
.logline.warn{ color:#ffd27a; }
|
||||
.logline.err{ color:#ff99b3; }
|
||||
.logline.dim{ color:#6a8596; }
|
||||
|
||||
.paneHighlight{
|
||||
box-shadow:0 0 0 2px var(--acid-2), 0 0 24px color-mix(in oklab, var(--acid-2) 55%, transparent) inset, 0 0 40px color-mix(in oklab, var(--acid-2) 35%, transparent);
|
||||
animation:hi 900ms ease-out 1;
|
||||
}
|
||||
@keyframes hi{ 0%{transform:scale(1)} 50%{transform:scale(1.01)} 100%{transform:scale(1)} }
|
||||
|
||||
/* Arguments section */
|
||||
.section{ padding:12px; border-bottom:1px dashed var(--c-border); }
|
||||
.h{ font-weight:800; letter-spacing:.5px; color:var(--acid-2); }
|
||||
.sub{ color:var(--muted); font-size:.9rem; }
|
||||
.builder{ padding:12px; display:grid; gap:12px; }
|
||||
.field{ display:grid; gap:6px; }
|
||||
.label{ font-size:.85rem; color:var(--muted); }
|
||||
.ctl, .select, .range{
|
||||
background:var(--c-panel); color:var(--ink); border:1px solid var(--c-border-strong);
|
||||
border-radius:var(--control-r,10px); padding:10px 12px; font:inherit;
|
||||
}
|
||||
.ctl:focus, .select:focus{ outline:none; box-shadow:0 0 0 2px color-mix(in oklab, var(--acid) 55%, transparent) inset; }
|
||||
.chips{ display:flex; gap:8px; flex-wrap:wrap; padding:10px; }
|
||||
.chip2{ padding:6px 10px; border-radius:999px; background:var(--c-chip-bg); border:1px solid var(--c-border-hi); cursor:pointer; transition:.18s; }
|
||||
.chip2:hover{ box-shadow:0 0 0 1px var(--c-border-hi) inset, 0 8px 22px var(--glow-weak); }
|
||||
|
||||
/* Mobile tweaks */
|
||||
@media (max-width: 860px){
|
||||
.toolbar2{ display:none !important; } /* hide toolbar in mobile */
|
||||
.paneHeader{ grid-template-columns:1fr; row-gap:8px; }
|
||||
.paneBtns{ justify-content:flex-start; }
|
||||
.paneBtns .btn{ padding:5px 6px; font-size:.85rem; }
|
||||
.main {
|
||||
left: 350px;
|
||||
}
|
||||
.sidebar {
|
||||
width: 350px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- Sidebar content (sidebar frame is from global.css) -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sideheader" id="sideheader">
|
||||
<div class="tabs-container">
|
||||
<button class="tab-btn active" data-page="attacks">Actions</button>
|
||||
<button class="tab-btn" data-page="arguments">Arguments</button>
|
||||
</div>
|
||||
<div class="search">
|
||||
<input class="input" id="searchInput" placeholder="Search actions..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidecontent" id="sidecontent">
|
||||
<!-- Actions tab -->
|
||||
<div id="attacks-sidebar" class="sidebar-page" style="display:block">
|
||||
<div class="list" id="actionsList"></div>
|
||||
</div>
|
||||
|
||||
<!-- Arguments tab -->
|
||||
<div id="arguments-sidebar" class="sidebar-page" style="display:none">
|
||||
<div class="section">
|
||||
<div class="h">Arguments</div>
|
||||
<div class="sub">Auto-generated from action definitions</div>
|
||||
</div>
|
||||
<div class="builder" id="argBuilder"></div>
|
||||
<div class="section">
|
||||
<input id="freeArgs" class="ctl" placeholder="Additional arguments (e.g., --verbose --debug)" />
|
||||
</div>
|
||||
<div class="chips" id="presetChips"></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main area (positioned by global.css) -->
|
||||
<div class="main">
|
||||
<main id="actionsLauncher">
|
||||
<section class="center panel">
|
||||
<div class="toolbar2">
|
||||
<div class="spacer"></div>
|
||||
<div class="seg" id="splitSeg">
|
||||
<button data-split="1" class="active">1</button>
|
||||
<button data-split="2">2</button>
|
||||
<button data-split="3">3</button>
|
||||
<button data-split="4">4</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="multiConsole split-1" id="multiConsole"></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* =========================================================
|
||||
Actions Launcher — JS (queue removed)
|
||||
---------------------------------------------------------
|
||||
• Forces single console on mobile and hides toolbar2.
|
||||
• Clean pane header markup + better responsiveness.
|
||||
• API endpoints kept: /list_scripts, /run_script, /stop_script, /get_script_output/:path
|
||||
========================================================= */
|
||||
|
||||
const isMobile = () => window.matchMedia('(max-width: 860px)').matches;
|
||||
|
||||
class ActionsLauncher{
|
||||
constructor(){
|
||||
this.actions = [];
|
||||
this.activeAction = null;
|
||||
this.runningActions = new Map();
|
||||
this.logs = new Map();
|
||||
this.split = 1; // forced to 1 on mobile
|
||||
this.panes = [null, null, null, null];
|
||||
this.assignTargetPaneIndex = null;
|
||||
this.autoClearPane = {0:false,1:false,2:false,3:false};
|
||||
this.filter = 'all';
|
||||
this.pollingIntervals = new Map();
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init(){
|
||||
await this.loadActions();
|
||||
this.setupEventListeners();
|
||||
this.enforceMobileOnePane(); // always one pane on mobile
|
||||
this.renderActions();
|
||||
this.renderConsoles();
|
||||
window.addEventListener('resize', this.onResizeDebounced.bind(this));
|
||||
}
|
||||
|
||||
onResizeDebounced(){
|
||||
clearTimeout(this._rz_t);
|
||||
this._rz_t = setTimeout(()=>{
|
||||
this.enforceMobileOnePane();
|
||||
this.renderConsoles();
|
||||
}, 120);
|
||||
}
|
||||
|
||||
enforceMobileOnePane(){
|
||||
if(isMobile()){
|
||||
this.split = 1;
|
||||
if(!this.panes[0] && this.activeAction){ this.panes[0] = this.activeAction.id; }
|
||||
for(let i=1;i<this.panes.length;i++) this.panes[i] = null;
|
||||
// Disable split buttons visually
|
||||
document.querySelectorAll('#splitSeg button').forEach(b=>{
|
||||
b.classList.toggle('active', b.dataset.split === '1');
|
||||
b.disabled = true; b.style.opacity = .6; b.style.pointerEvents = 'none';
|
||||
});
|
||||
}else{
|
||||
document.querySelectorAll('#splitSeg button').forEach(b=>{
|
||||
b.disabled = false; b.style.opacity = ''; b.style.pointerEvents = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async loadActions(){
|
||||
try{
|
||||
const response = await fetch('/list_scripts');
|
||||
const { status, data } = await response.json();
|
||||
|
||||
if(status === 'success' && Array.isArray(data)){
|
||||
this.actions = data.map(action=>{
|
||||
const raw = action.b_args ?? {};
|
||||
let args = raw;
|
||||
if(typeof raw === 'string'){ try{ args = JSON.parse(raw);}catch{ args = {}; } }
|
||||
|
||||
const id = action.b_module || (action.name ? action.name.replace(/\.py$/,'') : 'unknown');
|
||||
const className = action.b_class || id;
|
||||
const icon = action.b_icon || `/actions_icons/${className}.png`;
|
||||
|
||||
return {
|
||||
id,
|
||||
name: action.name || action.b_class || action.b_module || 'Unnamed',
|
||||
module: action.b_module || action.module,
|
||||
category: action.b_action || action.category || 'normal',
|
||||
description: action.description || 'No description',
|
||||
args,
|
||||
status: 'ready',
|
||||
path: action.path || action.module_path,
|
||||
icon,
|
||||
version: action.b_version,
|
||||
author: action.b_author,
|
||||
docsUrl: action.b_docs_url,
|
||||
examples: action.b_examples
|
||||
};
|
||||
});
|
||||
}else{
|
||||
this.actions = [];
|
||||
}
|
||||
}catch(err){
|
||||
console.error('Failed to load actions:', err);
|
||||
this.actions = [];
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners(){
|
||||
// Search
|
||||
const search = document.getElementById('searchInput');
|
||||
if(search) search.addEventListener('input', ()=>this.renderActions());
|
||||
|
||||
// Sidebar tabs
|
||||
document.querySelectorAll('.tab-btn').forEach(btn=>{
|
||||
btn.addEventListener('click', ()=>{
|
||||
const page = btn.dataset.page;
|
||||
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
document.querySelectorAll('.sidebar-page').forEach(sp=>sp.style.display='none');
|
||||
document.getElementById(page+'-sidebar').style.display='block';
|
||||
});
|
||||
});
|
||||
|
||||
// Split buttons (ignored on mobile)
|
||||
document.querySelectorAll('#splitSeg button').forEach(btn=>{
|
||||
btn.addEventListener('click', ()=>{
|
||||
if(isMobile()){ this.enforceMobileOnePane(); return; }
|
||||
document.querySelectorAll('#splitSeg button').forEach(b=>b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
this.split = parseInt(btn.dataset.split, 10);
|
||||
this.renderConsoles();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Sidebar: actions ---------- */
|
||||
renderActions(){
|
||||
const q = (document.getElementById('searchInput')?.value || '').trim().toLowerCase();
|
||||
const terms = q ? q.split(/\s+/).filter(Boolean) : [];
|
||||
|
||||
const filtered = this.actions.filter(action=>{
|
||||
const matchesFilter = this.filter==='all' || (action.category||'').toLowerCase()===this.filter;
|
||||
if(!matchesFilter) return false;
|
||||
if(terms.length===0) return true;
|
||||
const hay = [
|
||||
action.name, action.description, action.module, action.id, action.author
|
||||
].concat(Array.isArray(action.tags)?action.tags:[])
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
return terms.every(t=>hay.includes(t));
|
||||
});
|
||||
|
||||
const list = document.getElementById('actionsList');
|
||||
if(!list) return;
|
||||
list.innerHTML = filtered.map(a=>this.createActionRow(a)).join('');
|
||||
this.attachActionHandlers('actionsList');
|
||||
}
|
||||
|
||||
createActionRow(action){
|
||||
const statusChip = this.getStatusChip(action.status);
|
||||
const iconHtml = `
|
||||
<img src="${action.icon}" class="ic-img" alt=""
|
||||
onerror="this.onerror=null; this.src='/actions/actions_icons/default.png';" />
|
||||
`;
|
||||
return `
|
||||
<div class="row" data-action="${action.id}">
|
||||
<div class="ic">${iconHtml}</div>
|
||||
<div>
|
||||
<div class="name">${action.name}</div>
|
||||
<div class="desc">${action.description}</div>
|
||||
</div>
|
||||
<div class="chip ${statusChip.class}">${statusChip.text}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
attachActionHandlers(containerId){
|
||||
const container = document.getElementById(containerId);
|
||||
if(!container) return;
|
||||
container.querySelectorAll('.row').forEach(row=>{
|
||||
const actionId = row.dataset.action;
|
||||
row.draggable = true;
|
||||
|
||||
row.addEventListener('dragstart', e=>{
|
||||
e.dataTransfer.setData('text/plain', actionId);
|
||||
});
|
||||
|
||||
row.addEventListener('click', ()=>{
|
||||
if(this.assignTargetPaneIndex!==null){
|
||||
this.panes[this.assignTargetPaneIndex] = actionId;
|
||||
this.clearAssignTarget();
|
||||
this.renderConsoles();
|
||||
return;
|
||||
}
|
||||
this.selectAction(actionId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getStatusChip(status){
|
||||
switch(status){
|
||||
case 'running': return { class:'run', text:'Running' };
|
||||
case 'success': return { class:'ok', text:'Success' };
|
||||
case 'error': return { class:'err', text:'Error' };
|
||||
default: return { class:'', text:'Ready' };
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Arguments ---------- */
|
||||
selectAction(actionId){
|
||||
const action = this.actions.find(a=>a.id===actionId);
|
||||
if(!action) return;
|
||||
|
||||
this.activeAction = action;
|
||||
this.renderArguments(action);
|
||||
this.renderPresets(action);
|
||||
|
||||
if(this.assignTargetPaneIndex!==null){
|
||||
this.panes[this.assignTargetPaneIndex] = actionId;
|
||||
this.clearAssignTarget();
|
||||
this.renderConsoles();
|
||||
return;
|
||||
}
|
||||
const paneIndex = this.panes.findIndex(p=>p===actionId);
|
||||
if(paneIndex===-1){
|
||||
const emptyIndex = this.panes.slice(0, this.split).findIndex(p=>!p);
|
||||
this.panes[emptyIndex !== -1 ? emptyIndex : 0] = actionId;
|
||||
this.renderConsoles();
|
||||
}else{
|
||||
this.highlightPane(paneIndex);
|
||||
}
|
||||
}
|
||||
|
||||
renderArguments(action){
|
||||
const builder = document.getElementById('argBuilder');
|
||||
if(!builder) return;
|
||||
|
||||
builder.innerHTML = '';
|
||||
|
||||
// Meta + Docs
|
||||
const header = document.createElement('div');
|
||||
header.style.display = 'flex';
|
||||
header.style.alignItems = 'center';
|
||||
header.style.justifyContent = 'space-between';
|
||||
header.style.gap = '10px';
|
||||
header.style.marginBottom = '8px';
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'sub';
|
||||
const bits = [];
|
||||
if(action?.version) bits.push(`v${action.version}`);
|
||||
if(action?.author) bits.push(`by ${action.author}`);
|
||||
meta.textContent = bits.join(' · ') || '';
|
||||
header.appendChild(meta);
|
||||
|
||||
const right = document.createElement('div');
|
||||
if(action?.docsUrl){
|
||||
const a = document.createElement('a');
|
||||
a.href = action.docsUrl;
|
||||
a.target = '_blank'; a.rel='noopener noreferrer';
|
||||
a.className = 'btn';
|
||||
a.textContent = '📖 Docs';
|
||||
right.appendChild(a);
|
||||
}
|
||||
header.appendChild(right);
|
||||
builder.appendChild(header);
|
||||
|
||||
// Presets
|
||||
const hasPresets = Array.isArray(action?.examples) && action.examples.length>0;
|
||||
if(hasPresets){
|
||||
const chipbar = document.createElement('div');
|
||||
chipbar.style.display = 'flex';
|
||||
chipbar.style.flexWrap = 'wrap';
|
||||
chipbar.style.gap = '8px';
|
||||
chipbar.style.margin = '2px 0 10px 0';
|
||||
|
||||
action.examples.forEach((preset, idx)=>{
|
||||
const b = document.createElement('button');
|
||||
b.className = 'chip2';
|
||||
b.textContent = preset.name || preset.title || `Preset ${idx+1}`;
|
||||
b.title = 'Apply this preset';
|
||||
b.addEventListener('click', ()=>this.applyPreset(preset));
|
||||
chipbar.appendChild(b);
|
||||
});
|
||||
builder.appendChild(chipbar);
|
||||
}
|
||||
|
||||
// Fields
|
||||
if(!action.args || Object.keys(action.args).length===0){
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'sub';
|
||||
empty.textContent = 'No configurable arguments';
|
||||
builder.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(action.args).forEach(([key, config])=>{
|
||||
const field = document.createElement('div');
|
||||
field.className = 'field';
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.className = 'label';
|
||||
label.textContent = config.label || key;
|
||||
field.appendChild(label);
|
||||
|
||||
const input = this.createInput(key, config);
|
||||
field.appendChild(input);
|
||||
|
||||
if(config.help){
|
||||
const help = document.createElement('div');
|
||||
help.className = 'sub';
|
||||
help.textContent = config.help;
|
||||
field.appendChild(help);
|
||||
}
|
||||
|
||||
builder.appendChild(field);
|
||||
});
|
||||
}
|
||||
|
||||
createInput(key, config){
|
||||
const type = config.type || 'text';
|
||||
let input;
|
||||
switch(type){
|
||||
case 'select':
|
||||
input = document.createElement('select');
|
||||
input.className = 'select';
|
||||
(config.choices || []).forEach(choice=>{
|
||||
const option = document.createElement('option');
|
||||
option.value = choice; option.textContent = choice;
|
||||
if(choice === config.default) option.selected = true;
|
||||
input.appendChild(option);
|
||||
});
|
||||
break;
|
||||
case 'checkbox':
|
||||
input = document.createElement('input');
|
||||
input.type='checkbox'; input.className='ctl';
|
||||
input.checked = config.default || false;
|
||||
break;
|
||||
case 'number':
|
||||
input = document.createElement('input');
|
||||
input.type='number'; input.className='ctl';
|
||||
if(config.min !== undefined) input.min = config.min;
|
||||
if(config.max !== undefined) input.max = config.max;
|
||||
if(config.step !== undefined) input.step = config.step;
|
||||
input.value = config.default || '';
|
||||
break;
|
||||
case 'slider':
|
||||
case 'range':
|
||||
input = document.createElement('input');
|
||||
input.type='range'; input.className='range';
|
||||
input.min = config.min || 0;
|
||||
input.max = config.max || 100;
|
||||
input.step = config.step || 1;
|
||||
input.value = config.default || input.min;
|
||||
break;
|
||||
default:
|
||||
input = document.createElement('input');
|
||||
input.type='text'; input.className='ctl';
|
||||
input.value = config.default || '';
|
||||
input.placeholder = config.placeholder || '';
|
||||
}
|
||||
input.dataset.arg = key;
|
||||
return input;
|
||||
}
|
||||
|
||||
applyPreset(preset){
|
||||
Object.entries(preset).forEach(([key,val])=>{
|
||||
if(key==='name'||key==='title') return;
|
||||
const container = document.getElementById('argBuilder');
|
||||
if(!container) return;
|
||||
const input = container.querySelector(`[data-arg="${key}"]`);
|
||||
if(!input) return;
|
||||
if(input.type==='checkbox') input.checked = !!val;
|
||||
else input.value = val;
|
||||
});
|
||||
}
|
||||
|
||||
collectArguments(){
|
||||
if(!this.activeAction) return '';
|
||||
const args = [];
|
||||
const builder = document.getElementById('argBuilder');
|
||||
if(builder){
|
||||
builder.querySelectorAll('[data-arg]').forEach(input=>{
|
||||
const key = input.dataset.arg;
|
||||
const flag = `--${key.replace(/_/g,'-')}`;
|
||||
if(input.type==='checkbox'){
|
||||
if(input.checked) args.push(flag);
|
||||
}else{
|
||||
const value = input.value.trim();
|
||||
if(value){ args.push(flag); args.push(value); }
|
||||
}
|
||||
});
|
||||
}
|
||||
const freeArgs = document.getElementById('freeArgs')?.value || '';
|
||||
if(freeArgs.trim()) args.push(...freeArgs.trim().split(' '));
|
||||
return args.join(' ');
|
||||
}
|
||||
|
||||
/* ---------- Consoles ---------- */
|
||||
renderConsoles(){
|
||||
const container = document.getElementById('multiConsole');
|
||||
const effectiveSplit = isMobile() ? 1 : this.split;
|
||||
container.className = `multiConsole split-${effectiveSplit}`;
|
||||
container.innerHTML = '';
|
||||
|
||||
const rows = (effectiveSplit===4) ? 2 : 1;
|
||||
container.style.setProperty('--rows', rows);
|
||||
|
||||
for(let i=0;i<effectiveSplit;i++){
|
||||
const actionId = this.panes[i];
|
||||
const action = actionId ? this.actions.find(a=>a.id===actionId) : null;
|
||||
|
||||
const pane = document.createElement('div');
|
||||
pane.className='pane'; pane.dataset.index=i;
|
||||
|
||||
const statusColor = this.getStatusColor(action?.status);
|
||||
const iconUrl = action?.icon || '/web/images/attack.png';
|
||||
const hasMeta = !!(action && (action.docsUrl || action.author || action.version));
|
||||
|
||||
pane.innerHTML = `
|
||||
<div class="paneHeader">
|
||||
<!-- Left: status dot + icon + title/meta -->
|
||||
<div class="paneTitle" title="${action ? (action.description||'') : ''}">
|
||||
<span class="dot" style="background:${statusColor}"></span>
|
||||
${action ? `<img class="paneIcon" src="${iconUrl}" alt="" onerror="this.style.display='none'">` : ''}
|
||||
<div class="titleBlock">
|
||||
<div class="titleLine">
|
||||
<strong>${action ? action.name : '— Empty Pane —'}</strong>
|
||||
</div>
|
||||
${
|
||||
hasMeta
|
||||
? `<div class="metaLine">
|
||||
${action?.version ? `<span class="chip">v${action.version}</span>` : ''}
|
||||
${action?.author ? `<span class="chip">by ${action.author}</span>` : ''}
|
||||
</div>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: actions -->
|
||||
<div class="paneBtns">
|
||||
${
|
||||
action
|
||||
? `
|
||||
${action?.docsUrl ? `<a class="btn" href="${action.docsUrl}" target="_blank" rel="noopener">Docs</a>` : ''}
|
||||
<button class="btn" data-action="run" data-idx="${i}">Run</button>
|
||||
<button class="btn warn" data-action="stop" data-idx="${i}">Stop</button>
|
||||
<button class="btn" data-action="clear" data-idx="${i}">Clear</button>
|
||||
<button class="btn" data-action="export" data-idx="${i}">⬇ Export</button>
|
||||
<button class="btn ${this.autoClearPane[i] ? 'on':''}" data-action="toggleAuto" data-idx="${i}">
|
||||
${this.autoClearPane[i] ? 'Auto-clear ON' : 'Auto-clear OFF'}
|
||||
</button>
|
||||
`
|
||||
: `<button class="btn" data-action="assign" data-idx="${i}">Assign</button>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paneLog" id="paneLog-${i}">
|
||||
${this.renderLogs(actionId)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
pane.querySelectorAll('[data-action]').forEach(btn=>{
|
||||
btn.addEventListener('click', ()=>{
|
||||
const idx = parseInt(btn.dataset.idx ?? i, 10);
|
||||
switch(btn.dataset.action){
|
||||
case 'run': this.activeAction = action; this.runAction(idx); break;
|
||||
case 'stop': this.stopAction(actionId); break;
|
||||
case 'clear': this.clearActionLogs(actionId); break;
|
||||
case 'export':this.exportPaneLogs(idx); break;
|
||||
case 'toggleAuto':
|
||||
this.autoClearPane[idx] = !this.autoClearPane[idx];
|
||||
btn.classList.toggle('on', this.autoClearPane[idx]);
|
||||
btn.textContent = this.autoClearPane[idx] ? 'Auto-clear ON' : 'Auto-clear OFF';
|
||||
break;
|
||||
case 'assign': this.assignActionToPane(idx); break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Drag-to-assign
|
||||
pane.addEventListener('dragover', e=>{ e.preventDefault(); pane.classList.add('paneHighlight'); });
|
||||
pane.addEventListener('dragleave', ()=>pane.classList.remove('paneHighlight'));
|
||||
pane.addEventListener('drop', e=>{
|
||||
e.preventDefault(); pane.classList.remove('paneHighlight');
|
||||
const droppedId = e.dataTransfer.getData('text/plain'); if(!droppedId) return;
|
||||
this.panes[i] = droppedId; this.renderConsoles();
|
||||
});
|
||||
|
||||
container.appendChild(pane);
|
||||
pane.querySelector(`#paneLog-${i}`)?.addEventListener('click', ()=>this.setAssignTarget(i));
|
||||
}
|
||||
}
|
||||
|
||||
renderLogs(actionId){
|
||||
if(!actionId) return '<div class="logline dim">Select an action to see logs</div>';
|
||||
const logs = this.logs.get(actionId) || [];
|
||||
if(logs.length===0) return '<div class="logline dim">Waiting for logs...</div>';
|
||||
return logs.map(log=>this.formatLogLine(log)).join('');
|
||||
}
|
||||
formatLogLine(log){
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const cssClass = this.getLogClass(log);
|
||||
return `<div class="logline ${cssClass}">[${timestamp}] ${this.escapeHtml(log)}</div>`;
|
||||
}
|
||||
getLogClass(log){
|
||||
const lower = String(log).toLowerCase();
|
||||
if(lower.includes('error') || lower.includes('failed')) return 'err';
|
||||
if(lower.includes('warning') || lower.includes('warn')) return 'warn';
|
||||
if(lower.includes('success') || lower.includes('complete')) return 'ok';
|
||||
if(lower.includes('info')) return 'info';
|
||||
return 'dim';
|
||||
}
|
||||
escapeHtml(text){
|
||||
const map = { '&':'&','<':'<','>':'>','"':'"',"'":''' };
|
||||
return String(text).replace(/[&<>"']/g, m=>map[m]);
|
||||
}
|
||||
getStatusColor(status){
|
||||
switch(status){
|
||||
case 'running': return 'var(--acid)';
|
||||
case 'success': return 'var(--ok)';
|
||||
case 'error': return 'var(--danger)';
|
||||
default: return 'var(--acid-2)';
|
||||
}
|
||||
}
|
||||
|
||||
clearActionLogs(actionId){
|
||||
this.logs.set(actionId, []); this.renderConsoles();
|
||||
}
|
||||
|
||||
exportPaneLogs(paneIndex){
|
||||
const actionId = this.panes[paneIndex]; if(!actionId) return;
|
||||
const action = this.actions.find(a=>a.id===actionId);
|
||||
const logs = this.logs.get(actionId) || [];
|
||||
const blob = new Blob([logs.join('\n')], {type:'text/plain'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${(action?.name || 'pane')}_logs_${Date.now()}.txt`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
highlightPane(index){
|
||||
const pane = document.querySelector(`.pane[data-index="${index}"]`);
|
||||
if(pane){ pane.classList.add('paneHighlight'); setTimeout(()=>pane.classList.remove('paneHighlight'), 900); }
|
||||
}
|
||||
|
||||
setAssignTarget(paneIndex){
|
||||
this.assignTargetPaneIndex = paneIndex;
|
||||
document.querySelectorAll('.pane').forEach(el=>el.classList.remove('paneHighlight'));
|
||||
const pane = document.querySelector(`.pane[data-index="${paneIndex}"]`);
|
||||
if(pane) pane.classList.add('paneHighlight');
|
||||
}
|
||||
clearAssignTarget(){
|
||||
this.assignTargetPaneIndex = null;
|
||||
document.querySelectorAll('.pane').forEach(el=>el.classList.remove('paneHighlight'));
|
||||
}
|
||||
|
||||
/* ---------- Run / Stop / Poll ---------- */
|
||||
async runAction(paneIndex=null){
|
||||
if(!this.activeAction) return;
|
||||
const args = this.collectArguments();
|
||||
const action = this.activeAction;
|
||||
|
||||
if(paneIndex!==null && this.autoClearPane[paneIndex]){
|
||||
this.logs.set(action.id, []);
|
||||
const logEl = document.getElementById(`paneLog-${paneIndex}`);
|
||||
if(logEl) logEl.innerHTML = '';
|
||||
}
|
||||
|
||||
action.status = 'running';
|
||||
this.runningActions.set(action.id, {status:'running', process:null});
|
||||
this.renderActions(); this.renderConsoles();
|
||||
|
||||
if(!this.logs.has(action.id)) this.logs.set(action.id, []);
|
||||
this.logs.get(action.id).push(`Starting ${action.name}...`);
|
||||
|
||||
try{
|
||||
const response = await fetch('/run_script', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ script_name: action.module || action.id, args })
|
||||
});
|
||||
const data = await response.json();
|
||||
if(data.status==='success'){ this.startOutputPolling(action.id); }
|
||||
else{ throw new Error(data.message || 'Run failed'); }
|
||||
}catch(err){
|
||||
action.status = 'error';
|
||||
this.logs.get(action.id).push(`Error: ${err.message}`);
|
||||
this.runningActions.delete(action.id);
|
||||
this.renderActions(); this.renderConsoles();
|
||||
}
|
||||
}
|
||||
|
||||
async stopAction(actionId){
|
||||
const id = actionId || this.activeAction?.id; if(!id) return;
|
||||
try{
|
||||
const action = this.actions.find(a=>a.id===id); if(!action) return;
|
||||
const response = await fetch('/stop_script', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ script_name: action.path })
|
||||
});
|
||||
const data = await response.json();
|
||||
if(data.status==='success'){
|
||||
action.status = 'ready';
|
||||
this.runningActions.delete(id);
|
||||
this.stopOutputPolling(id);
|
||||
this.logs.get(id)?.push('Script stopped by user');
|
||||
this.renderActions(); this.renderConsoles();
|
||||
}
|
||||
}catch(err){ console.error('Failed to stop action:', err); }
|
||||
}
|
||||
|
||||
startOutputPolling(actionId){
|
||||
const poll = async ()=>{
|
||||
try{
|
||||
const action = this.actions.find(a=>a.id===actionId); if(!action) return;
|
||||
const response = await fetch(`/get_script_output/${encodeURIComponent(action.path)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if(data.status==='success'){
|
||||
const output = data.data.output || [];
|
||||
if(output.length>0){
|
||||
if(!this.logs.has(actionId)) this.logs.set(actionId, []);
|
||||
this.logs.set(actionId, output);
|
||||
this.updatePaneLog(actionId);
|
||||
}
|
||||
if(data.data.is_running){
|
||||
this.pollingIntervals.set(actionId, setTimeout(poll, 1000));
|
||||
}else{
|
||||
action.status = 'success';
|
||||
this.runningActions.delete(actionId);
|
||||
this.stopOutputPolling(actionId);
|
||||
this.logs.get(actionId)?.push('Script completed successfully');
|
||||
this.renderActions(); this.renderConsoles();
|
||||
}
|
||||
}
|
||||
}catch(err){
|
||||
console.error('Polling error:', err);
|
||||
this.stopOutputPolling(actionId);
|
||||
}
|
||||
};
|
||||
poll();
|
||||
}
|
||||
|
||||
stopOutputPolling(actionId){
|
||||
const t = this.pollingIntervals.get(actionId);
|
||||
if(t){ clearTimeout(t); this.pollingIntervals.delete(actionId); }
|
||||
}
|
||||
|
||||
updatePaneLog(actionId){
|
||||
const paneIndex = this.panes.findIndex(p=>p===actionId);
|
||||
if(paneIndex===-1) return;
|
||||
const logEl = document.getElementById(`paneLog-${paneIndex}`);
|
||||
if(logEl){
|
||||
logEl.innerHTML = this.renderLogs(actionId);
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Boot */
|
||||
document.addEventListener('DOMContentLoaded', ()=>{
|
||||
window.actionsLauncher = new ActionsLauncher();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1371
web/actions_studio.html
Normal file
1492
web/attacks.html
Normal file
376
web/backup_update.html
Normal file
@@ -0,0 +1,376 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Bjorn Cyberviking - Update and Backup Management</title>
|
||||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="web/css/global.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="web/images/icon-192x192.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="theme-color" content="#333">
|
||||
<script src="web/js/global.js"></script>
|
||||
|
||||
<style>
|
||||
/* ======================================================================
|
||||
Page-scoped CSS (kept minimal) — uses your global tokens
|
||||
====================================================================== */
|
||||
.main-container{display:flex;height:calc(100vh - 60px);width:100%;position:relative}
|
||||
.section-list{list-style-type:none;padding:0;margin:0;flex-grow:1}
|
||||
.list-item{display:flex;align-items:center;padding:12px;cursor:pointer;border-radius:var(--radius);margin-bottom:12px;transition:box-shadow .3s, background-color .3s, border-color .3s;background:var(--grad-card);border:1px solid var(--c-border);box-shadow:var(--shadow)}
|
||||
.list-item:hover{box-shadow:var(--shadow-hover)}
|
||||
.list-item.selected{border:1px solid #00e764}
|
||||
.list-item img{margin-right:10px}
|
||||
@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
|
||||
.right-panel{flex:1;display:flex;flex-direction:column;padding:20px;overflow-y:auto;box-sizing:border-box;background-color:#1e1e1e}
|
||||
.content-section{display:none}
|
||||
.content-section.active{display:block}
|
||||
form{margin-top:20px}
|
||||
form label{display:block;margin-bottom:5px;color:white}
|
||||
form input[type="text"]{width:100%;padding:8px;margin-bottom:10px;border:1px solid #555;border-radius:4px;background-color:#07422f40;color:#fff;cursor:text;pointer-events:auto}
|
||||
form input[type="text"]:focus{outline:none;border-color:#007acc;background-color:#3d3d3d}
|
||||
form input[type="text"]:hover{border-color:#666}
|
||||
.default-badge{display:inline-block;padding:2px 8px;margin-left:8px;background-color:#007acc;color:white;border-radius:12px;font-size:.85em;font-weight:700}
|
||||
/* ===== Bjorn-scoped Modal (avoid conflicts with global.js) ===== */
|
||||
.bj-modal{display:none;position:fixed;z-index:1000;inset:0;overflow:auto;background-color:rgba(0,0,0,.5)}
|
||||
.bj-modal__content{background-color:#2d2d2d;margin:10% auto;padding:20px;border:1px solid #888;width:80%;max-width:fit-content;border-radius:8px;z-index:1001;color:#fff}
|
||||
.bj-modal__close{color:#aaa;float:right;font-size:28px;font-weight:700;cursor:pointer}
|
||||
.bj-modal__close:hover,.bj-modal__close:focus{color:#fff;text-decoration:none}
|
||||
/* ===== Bjorn-scoped Loading Overlay ===== */
|
||||
.bj-loading-overlay{display:none;position:fixed;z-index:1100;inset:0;background-color:rgba(0,0,0,.7);justify-content:center;align-items:center}
|
||||
.bj-rotating-arrow{width:50px;height:50px;border:5px solid transparent;border-top:5px solid #007acc;border-right:5px solid #007acc;border-radius:50%;animation:spin 1.5s linear infinite,bj-pulse 1.5s ease-in-out infinite}
|
||||
@keyframes bj-pulse{0%{box-shadow:0 0 0 0 rgba(0,122,204,.7)}70%{box-shadow:0 0 0 20px rgba(0,122,204,0)}100%{box-shadow:0 0 0 0 rgba(0,122,204,0)}}
|
||||
/* Update message bubble */
|
||||
#bj-update-message{background-color:#28a745;color:#fff;padding:12px 20px;border-radius:25px;display:inline-block;margin-bottom:15px;box-shadow:0 4px 6px rgba(0,0,0,.1);font-size:16px;max-width:100%;word-wrap:break-word}
|
||||
#bj-update-message.fade-in{animation:bjFadeIn .5s ease-in-out}
|
||||
@keyframes bjFadeIn{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}
|
||||
/* Responsive */
|
||||
@media (max-width:768px){.main-container{flex-direction:column}}
|
||||
@media (min-width:769px){.menu-icon{display:none}.side-menu{transform:translateX(0);position:relative;height:98%;z-index:10000}}
|
||||
.form-control{cursor:text;pointer-events:auto;background-color:#2d2d2d;color:#ffffff}
|
||||
.backups-table button.loading{position:relative;pointer-events:none;opacity:.6;background-color:#2d2d2d;color:#fff;border:#007acc}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidehead">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" id="hideSidebar"><span class="icon">⟵</span><span class="label">Hide</span></button>
|
||||
</div>
|
||||
|
||||
<li class="list-item" data-section="backup-section"><img src="/web/images/backuprestore.png" alt="Icon_backup" style="height:72px;"><span>Backup / Restore</span></li>
|
||||
<li class="list-item" data-section="update-section"><img src="/web/images/update.png" alt="Icon_update" style="height:72px;"><span>Update</span></li>
|
||||
|
||||
<div class="sidecontent" id="sidecontent">
|
||||
<div class="content-section" id="logs-section-content">
|
||||
<h2>Clear Logs</h2>
|
||||
<button class="btn danger" onclick="clear_logs()">Clear Logs</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="empty-list-hint" style="display:none;opacity:.8;margin-top:8px;font-size:.95em">No attacks found. Import a .py attack with “Add Attack”.</div>
|
||||
</aside>
|
||||
|
||||
<div class="main" id="main">
|
||||
<!-- Backup and Restore Section -->
|
||||
<div class="content-section" id="backup-section-content">
|
||||
<h2>Backup and Restore</h2>
|
||||
<form id="backup-form">
|
||||
<label for="backup-description">Backup Description:</label>
|
||||
<input type="text" id="backup-description" class="form-control" name="description" required>
|
||||
<button type="submit" class="btn">Create Backup</button>
|
||||
</form>
|
||||
|
||||
<h3>Backup List</h3>
|
||||
<table id="backups-table" class="backups-table">
|
||||
<thead><tr><th>Date</th><th>Description</th><th>Actions</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Update Application Section -->
|
||||
<div class="content-section" id="update-section-content">
|
||||
<div id="bj-update-message" style="margin-bottom:10px;"></div>
|
||||
<h2>Update Application (From Github)</h2>
|
||||
<button class="btn" onclick="bj_checkUpdate()">Check for Updates</button>
|
||||
<button class="btn" onclick="bj_update_application('upgrade')">Upgrade (With options to keep your data)</button>
|
||||
<button class="btn danger" onclick="bj_update_application('fresh_start')">Fresh Start (Replace everything)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bjorn-scoped Loading Overlay -->
|
||||
<div id="bj-loading-overlay" class="bj-loading-overlay"><div class="bj-rotating-arrow"></div></div>
|
||||
|
||||
<!-- Bjorn-scoped Restore/Update Modal -->
|
||||
<div id="bj-restore-modal" class="bj-modal" aria-hidden="true" role="dialog">
|
||||
<div class="bj-modal__content" role="document">
|
||||
<span class="bj-modal__close" id="bj-modal-close" aria-label="Close">×</span>
|
||||
<h2>Restore Options</h2>
|
||||
<form id="bj-restore-form">
|
||||
<p>Please select the folders to keep during restoration:</p>
|
||||
<label><input type="checkbox" name="keep" value="data"> Keep the <strong>data</strong> folder (/home/bjorn/Bjorn/data)</label><br>
|
||||
<label><input type="checkbox" name="keep" value="resources"> Keep the <strong>resources</strong> folder (/home/bjorn/Bjorn/resources)</label><br>
|
||||
<label><input type="checkbox" name="keep" value="actions"> Keep the <strong>actions</strong> folder (/home/bjorn/Bjorn/actions)</label><br>
|
||||
<label><input type="checkbox" name="keep" value="config"> Keep the <strong>config</strong> folder (/home/bjorn/Bjorn/config)</label><br><br>
|
||||
<button type="submit" class="btn">Restore Backup</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ======================================================================
|
||||
// Bjorn UI helpers — non-blocking toast + confirm over your global.js
|
||||
// ======================================================================
|
||||
const t = (msg, ms=2600) => (typeof window.toast === 'function' ? window.toast(msg, ms) : console.log(msg));
|
||||
|
||||
// Lightweight, non-blocking confirmation; auto-cleans DOM; returns Promise<boolean>
|
||||
async function toastConfirm(message, { okText='Proceed', cancelText='Cancel', timeout=0 } = {}) {
|
||||
return new Promise(resolve => {
|
||||
const box = document.createElement('div');
|
||||
Object.assign(box.style, { position:'fixed', right:'16px', bottom:'16px', zIndex:99999, maxWidth:'460px', background:'rgba(10,16,16,.96)', color:'#eafff6', border:'1px solid rgba(0,255,154,.35)', borderRadius:'12px', padding:'12px 14px', boxShadow:'0 10px 24px rgba(0,0,0,.35)', font:'14px/1.45 system-ui' });
|
||||
box.innerHTML = `
|
||||
<div style="margin-bottom:10px">${message}</div>
|
||||
<div style="display:flex; gap:8px; justify-content:flex-end">
|
||||
<button data-x style="background:#333;border:1px solid #555;color:#fff;padding:6px 10px;border-radius:8px;cursor:pointer">${cancelText}</button>
|
||||
<button data-ok style="background:#00ff9a;border:1px solid #00ff9a33;color:#001b11;padding:6px 10px;border-radius:8px;cursor:pointer;font-weight:700">${okText}</button>
|
||||
</div>`;
|
||||
document.body.appendChild(box);
|
||||
const done = v => { try { box.remove(); } catch {} resolve(v); };
|
||||
box.querySelector('[data-ok]').addEventListener('click', () => done(true));
|
||||
box.querySelector('[data-x]').addEventListener('click', () => done(false));
|
||||
if (timeout > 0) setTimeout(() => done(false), timeout);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
/* ===== Sections wiring ===== */
|
||||
const sections = {
|
||||
'logs-section': document.getElementById('logs-section-content'),
|
||||
'backup-section': document.getElementById('backup-section-content'),
|
||||
'update-section': document.getElementById('update-section-content'),
|
||||
};
|
||||
|
||||
const defaultSection = 'backup-section';
|
||||
const defaultSectionElement = document.querySelector(`[data-section="${defaultSection}"]`);
|
||||
if (defaultSectionElement) {
|
||||
defaultSectionElement.classList.add('selected');
|
||||
if (sections[defaultSection]) {
|
||||
sections[defaultSection].classList.add('active');
|
||||
loadBackups();
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Scoped modal/overlay helpers ===== */
|
||||
const bjModal = document.getElementById('bj-restore-modal');
|
||||
const bjModalClose = document.getElementById('bj-modal-close');
|
||||
|
||||
function bj_showLoading(){const overlay=document.getElementById('bj-loading-overlay'); if(overlay) overlay.style.display='flex'}
|
||||
function bj_hideLoading(){const overlay=document.getElementById('bj-loading-overlay'); if(overlay) overlay.style.display='none'}
|
||||
|
||||
function hideAllContents(){for (let key in sections) sections[key].classList.remove('active'); document.querySelectorAll('.list-item').forEach(item => item.classList.remove('selected'))}
|
||||
|
||||
function selectSection(event){
|
||||
const clickedItem = event.currentTarget;
|
||||
const section = clickedItem.getAttribute('data-section');
|
||||
hideAllContents();
|
||||
if (sections[section]) {
|
||||
sections[section].classList.add('active');
|
||||
clickedItem.classList.add('selected');
|
||||
if (section === 'backup-section') { loadBackups(); }
|
||||
else if (section === 'update-section') { bj_checkUpdate(); }
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('.list-item').forEach(item => item.addEventListener('click', selectSection));
|
||||
|
||||
/* ===== API: Update Check (shows status in green bubble + toast) ===== */
|
||||
window.bj_checkUpdate = function(){
|
||||
fetch('/check_update', { method:'GET', headers:{ 'Content-Type':'application/json' } })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const messageDiv = document.getElementById('bj-update-message');
|
||||
if (data.update_available) {
|
||||
messageDiv.innerHTML = `New update available: <strong>${data.latest_version}</strong> (currently on: <strong>${data.current_version}</strong>)`;
|
||||
t('⬆️ Update available');
|
||||
} else {
|
||||
messageDiv.innerHTML = `You are on the latest version: <strong>${data.current_version}</strong>`;
|
||||
t('✅ Latest version already installed');
|
||||
}
|
||||
messageDiv.classList.remove('fade-in'); void messageDiv.offsetWidth; messageDiv.classList.add('fade-in');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error checking updates:', err);
|
||||
const messageDiv = document.getElementById('bj-update-message');
|
||||
messageDiv.innerHTML = `Error checking updates.`;
|
||||
messageDiv.classList.remove('fade-in'); void messageDiv.offsetWidth; messageDiv.classList.add('fade-in');
|
||||
t('⛔ Error while checking updates — see console');
|
||||
});
|
||||
};
|
||||
|
||||
/* ===== Backups: list/load (toast feedback) ===== */
|
||||
function loadBackups(){
|
||||
fetch('/list_backups', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify({}) })
|
||||
.then(response => { if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); })
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
const tbody = document.querySelector('#backups-table tbody'); tbody.innerHTML = '';
|
||||
data.backups.forEach(backup => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${backup.date}</td>
|
||||
<td>${backup.description}${backup.is_default ? ' <span class="default-badge">default</span>' : ''}</td>
|
||||
<td style="display:flex;gap:5px;flex-wrap:wrap">
|
||||
<button class="btn" onclick="bj_openRestoreModal('${backup.filename}')">Restore</button>
|
||||
${!backup.is_default ? `
|
||||
<button class="btn danger" onclick="bj_deleteBackup('${backup.filename}', event)">Delete</button>
|
||||
<button class="btn" onclick="bj_setAsDefault('${backup.filename}', event)">Set as Default</button>
|
||||
` : ''}
|
||||
</td>`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
t('✅ Backups loaded');
|
||||
} else {
|
||||
t('⚠️ ' + (data.message || 'Error while loading backups'));
|
||||
}
|
||||
})
|
||||
.catch(error => { console.error('Error loading backups:', error); t('⛔ Failed to load backups — see console'); });
|
||||
}
|
||||
|
||||
/* ===== Create backup (toast feedback, no blocking alert) ===== */
|
||||
const backupForm = document.getElementById('backup-form');
|
||||
backupForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const description = document.getElementById('backup-description').value;
|
||||
const button = backupForm.querySelector('button');
|
||||
bj_showLoading(); button.classList.add('loading');
|
||||
|
||||
fetch('/create_backup', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify({ description }) })
|
||||
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||||
.then(data => {
|
||||
button.classList.remove('loading'); bj_hideLoading();
|
||||
if (data.status === 'success') { t('✅ ' + (data.message || 'Backup created')); loadBackups(); backupForm.reset(); }
|
||||
else { t('⚠️ ' + (data.message || 'Backup failed')); }
|
||||
})
|
||||
.catch(error => { button.classList.remove('loading'); bj_hideLoading(); console.error('Error creating backup:', error); t('⛔ Error while creating the backup — see console'); });
|
||||
});
|
||||
|
||||
/* ===== Set default backup ===== */
|
||||
window.bj_setAsDefault = function(filename, ev){
|
||||
const button = ev?.target; if (button) button.classList.add('loading');
|
||||
fetch('/set_default_backup', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify({ filename }) })
|
||||
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||||
.then(data => {
|
||||
if (button) button.classList.remove('loading');
|
||||
if (data.status === 'success') { t('✅ Default backup set'); loadBackups(); }
|
||||
else { t('⚠️ ' + (data.message || 'Could not set default')); }
|
||||
})
|
||||
.catch(error => { if (button) button.classList.remove('loading'); console.error('Error setting default backup:', error); t('⛔ Failed to set default — see console'); });
|
||||
};
|
||||
|
||||
/* ===== Open modal for restore ===== */
|
||||
window.bj_openRestoreModal = function (filename){
|
||||
window.bj_filenameToRestore = filename;
|
||||
window.bj_updateMode = null;
|
||||
document.querySelector('#bj-restore-modal .bj-modal__content h2').textContent = 'Restore Options';
|
||||
document.querySelector('#bj-restore-modal .bj-modal__content button').textContent = 'Restore Backup';
|
||||
bjModal.style.display = 'block';
|
||||
bjModal.setAttribute('aria-hidden', 'false');
|
||||
};
|
||||
|
||||
/* ===== Update (shared) ===== */
|
||||
function proceedWithUpdate(mode, keeps, ev){
|
||||
const button = ev?.target; if (button) button.classList.add('loading');
|
||||
const bodyData = { mode, keeps };
|
||||
fetch('/update_application', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify(bodyData) })
|
||||
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||||
.then(data => {
|
||||
if (button) button.classList.remove('loading');
|
||||
if (data.status === 'success') { t('✅ ' + (data.message || 'Application updated')); bjModal.style.display='none'; bjModal.setAttribute('aria-hidden','true'); }
|
||||
else { t('⚠️ ' + (data.message || 'Update failed')); }
|
||||
})
|
||||
.catch(error => { if (button) button.classList.remove('loading'); console.error('Error updating application:', error); t('⛔ Update failed — see console'); });
|
||||
}
|
||||
|
||||
/* ===== Update application (toastConfirm instead of confirm) ===== */
|
||||
window.bj_update_application = async function (mode){
|
||||
const msg = mode === 'upgrade' ? 'Proceed with application upgrade?' : 'Fresh Start will delete all data. Continue?';
|
||||
const ok = await toastConfirm(msg, { okText:'Proceed', cancelText:'Cancel' });
|
||||
if (!ok) { t('ℹ️ Cancelled'); return; }
|
||||
|
||||
if (mode === 'upgrade') {
|
||||
window.bj_updateMode = mode;
|
||||
window.bj_filenameToRestore = null;
|
||||
document.querySelector('#bj-restore-modal .bj-modal__content h2').textContent = 'Update Options';
|
||||
document.querySelector('#bj-restore-modal .bj-modal__content button').textContent = 'Update Application';
|
||||
document.getElementById('bj-restore-form').reset();
|
||||
bjModal.style.display = 'block';
|
||||
bjModal.setAttribute('aria-hidden', 'false');
|
||||
} else {
|
||||
bj_showLoading();
|
||||
proceedWithUpdate(mode, []);
|
||||
bj_hideLoading();
|
||||
}
|
||||
};
|
||||
|
||||
/* ===== Restore form submit (handles restore or upgrade) ===== */
|
||||
document.getElementById('bj-restore-form').addEventListener('submit', function(e){
|
||||
e.preventDefault();
|
||||
const keeps = Array.from(this.querySelectorAll('input[name="keep"]:checked')).map(cb => cb.value);
|
||||
|
||||
if (window.bj_updateMode === 'upgrade') {
|
||||
proceedWithUpdate('upgrade', keeps, e);
|
||||
} else {
|
||||
const filename = window.bj_filenameToRestore;
|
||||
if (!filename) { t('⚠️ No backup file selected'); return; }
|
||||
|
||||
const mode = keeps.length > 0 ? 'selective_restore' : 'full_restore';
|
||||
const bodyData = { filename, mode, keeps };
|
||||
|
||||
const button = this.querySelector('button');
|
||||
button.classList.add('loading'); bj_showLoading();
|
||||
|
||||
fetch('/restore_backup', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify(bodyData) })
|
||||
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||||
.then(data => {
|
||||
button.classList.remove('loading'); bj_hideLoading();
|
||||
if (data.status === 'success') { t('✅ ' + (data.message || 'Backup restored')); loadBackups(); this.reset(); bjModal.style.display='none'; bjModal.setAttribute('aria-hidden','true'); }
|
||||
else { t('⚠️ ' + (data.message || 'Restore failed')); }
|
||||
})
|
||||
.catch(error => { button.classList.remove('loading'); bj_hideLoading(); console.error('Error restoring backup:', error); t('⛔ Restore failed — see console'); });
|
||||
}
|
||||
});
|
||||
|
||||
/* ===== Delete backup (toastConfirm instead of confirm) ===== */
|
||||
window.bj_deleteBackup = async function (filename, ev){
|
||||
const ok = await toastConfirm('Delete this backup?', { okText:'Delete', cancelText:'Cancel' });
|
||||
if (!ok) { t('ℹ️ Deletion cancelled'); return; }
|
||||
|
||||
const button = ev?.target; if (button) button.classList.add('loading');
|
||||
bj_showLoading();
|
||||
|
||||
fetch('/delete_backup', { method:'POST', headers:{ 'Content-Type':'application/json' }, body:JSON.stringify({ filename }) })
|
||||
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||||
.then(data => {
|
||||
if (button) button.classList.remove('loading'); bj_hideLoading();
|
||||
if (data.status === 'success') { t('✅ ' + (data.message || 'Backup deleted')); loadBackups(); }
|
||||
else { t('⚠️ ' + (data.message || 'Delete failed')); }
|
||||
})
|
||||
.catch(error => { if (button) button.classList.remove('loading'); bj_hideLoading(); console.error('Error deleting backup:', error); t('⛔ Delete failed — see console'); });
|
||||
};
|
||||
|
||||
/* ===== Modal close handlers (scoped) ===== */
|
||||
bjModalClose.addEventListener('click', function(){
|
||||
bjModal.style.display = 'none'; bjModal.setAttribute('aria-hidden', 'true'); window.bj_updateMode = null; window.bj_filenameToRestore = null;
|
||||
});
|
||||
window.addEventListener('click', function(event){
|
||||
if (event.target === bjModal) {
|
||||
bjModal.style.display = 'none'; bjModal.setAttribute('aria-hidden','true'); window.bj_updateMode = null; window.bj_filenameToRestore = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
100
web/bjorn.html
@@ -2,12 +2,57 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Bjorn Cyberviking - Bjorn</title>
|
||||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="web/css/styles.css">
|
||||
<link rel="stylesheet" href="web/css/global.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/web/images/icon-192x192.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="theme-color" content="#333">
|
||||
<script src="web/js/global.js"></script>
|
||||
<style>
|
||||
|
||||
.image-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: calc(100vh - 70px); /* Adjust height to fit with mobile */
|
||||
}
|
||||
|
||||
.image-container img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
height: -webkit-fill-available;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.image-container img:active {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.topbar.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.image-container.fullscreen img {
|
||||
height: 100vh;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
|
||||
.image-container {
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<script src="web/js/global.js"></script>
|
||||
|
||||
<script defer>
|
||||
var delay = 5000; // Default value in case the fetch fails
|
||||
var intervalId;
|
||||
@@ -61,15 +106,32 @@
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
var toolbar = document.querySelector('.toolbar');
|
||||
var topbar = document.querySelector('.topbar');
|
||||
var bottombar = document.querySelector('.bottombar');
|
||||
var console = document.querySelector('.console');
|
||||
var imageContainer = document.querySelector('.image-container');
|
||||
if (toolbar.style.display === 'flex') {
|
||||
toolbar.style.display = 'none';
|
||||
if (topbar.style.display === 'flex') {
|
||||
topbar.style.display = 'none';
|
||||
imageContainer.style.width = '100%'; // Adjust width when toolbar is hidden
|
||||
} else {
|
||||
toolbar.style.display = 'flex';
|
||||
topbar.style.display = 'flex';
|
||||
imageContainer.style.width = 'calc(100%)'; // Adjust width when toolbar is visible
|
||||
}
|
||||
if (bottombar) {
|
||||
if (bottombar.style.display === 'grid') {
|
||||
bottombar.style.display = 'none';
|
||||
} else {
|
||||
bottombar.style.display = 'grid';
|
||||
}
|
||||
}
|
||||
if (console) {
|
||||
if (console.style.display === 'grid') {
|
||||
console.style.display = 'none';
|
||||
} else {
|
||||
console.style.display = 'grid';
|
||||
imageContainer.style.width = 'calc(100%)'; // Adjust width when toolbar is visible
|
||||
}
|
||||
}
|
||||
adjustImageHeight(); // Adjust image height after toggling toolbar
|
||||
}
|
||||
|
||||
@@ -91,30 +153,12 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toolbar" id="mainToolbar">
|
||||
<button type="button" onclick="window.location.href='/index.html'" title="Playground">
|
||||
<img src="/web/images/console_icon.png" alt="Bjorn" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/config.html'" title="Config">
|
||||
<img src="/web/images/config_icon.png" alt="Icon_config" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/network.html'" title="Network">
|
||||
<img src="/web/images/network_icon.png" alt="Icon_network" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/netkb.html'" title="NetKB">
|
||||
<img src="/web/images/netkb_icon.png" alt="Icon_netkb" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/credentials.html'" title="Credentials">
|
||||
<img src="/web/images/cred_icon.png" alt="Icon_cred" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/loot.html'" title="Loot">
|
||||
<img src="/web/images/loot_icon.png" alt="Icon_loot" style="height: 50px;">
|
||||
</button>
|
||||
</div>
|
||||
<main>
|
||||
<div class="image-container">
|
||||
<img id="screenImage_Home" src="screen.png" onclick="toggleMenu()" alt="Bjorn">
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bjorn Cyberviking - Config</title>
|
||||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="web/css/styles.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<script src="web/scripts/config.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toolbar" id="mainToolbar">
|
||||
<button type="button" onclick="window.location.href='/index.html'" title="Playground">
|
||||
<img src="/web/images/console_icon.png" alt="Bjorn" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/config.html'" title="Config">
|
||||
<img src="/web/images/config_icon.png" alt="Icon_config" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/network.html'" title="Network">
|
||||
<img src="/web/images/network_icon.png" alt="Icon_network" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/netkb.html'" title="NetKB">
|
||||
<img src="/web/images/netkb_icon.png" alt="Icon_netkb" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/credentials.html'" title="Credentials">
|
||||
<img src="/web/images/cred_icon.png" alt="Icon_cred" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/loot.html'" title="Loot">
|
||||
<img src="/web/images/loot_icon.png" alt="Icon_loot" style="height: 50px;">
|
||||
</button>
|
||||
</div>
|
||||
<div class="console-toolbar">
|
||||
<button type="button" class="toolbar-button" onclick="adjustConfigFontSize(-1)" title="-">
|
||||
<img src="/web/images/less.png" alt="Icon_less" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" class="toolbar-button" onclick="saveConfig()" title="Save">
|
||||
<img src="/web/images/save.png" alt="Icon_plus" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" class="toolbar-button" onclick="restoreDefault()" title="Restore Default">
|
||||
<img src="/web/images/restore.png" alt="Icon_plus" style="height: 50px;">
|
||||
</button>
|
||||
<button id="toggle-toolbar" type="button" class="toolbar-button" onclick="toggleConfigToolbar()" data-open="false">
|
||||
<img id="toggle-icon" src="/web/images/hide.png" alt="Toggle Toolbar" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" class="toolbar-button" onclick="toggleWifiPanel()">
|
||||
<img src="/web/images/wifi.png" alt="wifi" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" class="toolbar-button" onclick="adjustConfigFontSize(1)" title="+">
|
||||
<img src="/web/images/plus.png" alt="Icon_plus" style="height: 50px;">
|
||||
</button>
|
||||
</div>
|
||||
<div class="config-container">
|
||||
<form class="config-form"></form>
|
||||
</div>
|
||||
<div id="wifi-panel" class="wifi-panel">
|
||||
<div class="wifi-panel-header">
|
||||
<h3>Available Wi-Fi Networks</h3>
|
||||
<button class="close-btn" onclick="closeWifiPanel()">✖</button>
|
||||
</div>
|
||||
<ul id="wifi-list"></ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,53 +1,726 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bjorn Cyberviking - Credentials</title>
|
||||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="web/css/styles.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<script src="web/scripts/credentials.js" defer></script>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Bjorn Cyberviking - Credentials</title>
|
||||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="web/css/global.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/web/images/icon-192x192.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="theme-color" content="#333">
|
||||
<script src="web/js/global.js"></script>
|
||||
|
||||
<style>
|
||||
/* ========= Styles alignés sur global.css (pas de nouvelle palette) ========= */
|
||||
:root{
|
||||
/* aucun override agressif : seulement des fallbacks doux */
|
||||
--_bg: var(--bg, #0b0c0f);
|
||||
--_panel: var(--c-panel-2, rgba(16,22,22,.55));
|
||||
--_border: var(--c-border, rgba(255,255,255,.08));
|
||||
--_ink: var(--ink, #e9ecef);
|
||||
--_muted: var(--muted, #a5adb6);
|
||||
--_acid1: var(--acid, #00ff9a);
|
||||
--_acid2: var(--acid-2, #18f0ff);
|
||||
--_shadow: var(--shadow, 0 10px 26px rgba(0,0,0,.35));
|
||||
}
|
||||
|
||||
/* fond + typographie harmonisés */
|
||||
body{
|
||||
background: var(--_bg);
|
||||
color: var(--_ink);
|
||||
font-family: -apple-system,BlinkMacSystemFont,'Segoe UI','Inter',system-ui,sans-serif;
|
||||
min-height:100vh; overflow-x:hidden;
|
||||
}
|
||||
|
||||
/* conteneur principal */
|
||||
.main{ padding:16px; }
|
||||
|
||||
/* barre de stats */
|
||||
.stats-bar{
|
||||
display:flex; gap:12px; flex-wrap:wrap;
|
||||
padding:12px;
|
||||
background: color-mix(in oklab, var(--_panel) 88%, transparent);
|
||||
border:1px solid var(--_border);
|
||||
border-radius:12px;
|
||||
box-shadow: var(--_shadow);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
.stat-item{
|
||||
display:flex; align-items:center; gap:8px;
|
||||
padding:8px 12px;
|
||||
border:1px solid var(--_border);
|
||||
border-radius:10px;
|
||||
background: color-mix(in oklab, var(--_panel) 70%, transparent);
|
||||
}
|
||||
.stat-icon{ font-size:1.1rem; opacity:.9 }
|
||||
.stat-value{
|
||||
font-weight:800;
|
||||
background: linear-gradient(135deg, var(--_acid1), var(--_acid2));
|
||||
-webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
|
||||
}
|
||||
.stat-label{ color: var(--_muted); font-size:.8rem }
|
||||
|
||||
/* recherche globale */
|
||||
.global-search-container{ position:relative }
|
||||
.global-search-input{
|
||||
width:100%; padding:10px 14px; border-radius:12px;
|
||||
border:1px solid var(--_border);
|
||||
background: color-mix(in oklab, var(--_panel) 90%, transparent);
|
||||
color: var(--_ink);
|
||||
}
|
||||
.global-search-input:focus{
|
||||
outline:none;
|
||||
border-color: color-mix(in oklab, var(--_acid2) 40%, var(--_border));
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--_acid2) 18%, transparent);
|
||||
}
|
||||
.clear-global-button{
|
||||
position:absolute; right:10px; top:50%; transform:translateY(-50%);
|
||||
background:none; border:1px solid var(--_border);
|
||||
color:#ef4444; border-radius:8px; padding:2px 6px; display:none;
|
||||
}
|
||||
.clear-global-button.show{ display:block }
|
||||
|
||||
/* tabs collants */
|
||||
.tabs-container{
|
||||
position:sticky; top:0; z-index:20;
|
||||
display:flex; align-items:center; gap:8px;
|
||||
padding:8px 12px; min-height:44px;
|
||||
overflow-x:auto; -webkit-overflow-scrolling:touch;
|
||||
background: color-mix(in oklab, var(--_panel) 92%, transparent);
|
||||
border:1px solid var(--_border); border-radius:12px;
|
||||
box-shadow: var(--_shadow);
|
||||
}
|
||||
.tabs-container::-webkit-scrollbar{ height:0 }
|
||||
.tab{
|
||||
padding:10px 18px; border-radius:10px; cursor:pointer;
|
||||
color: var(--_muted); font-weight:700; font-size:.9rem;
|
||||
border:1px solid transparent; white-space:nowrap; flex:0 0 auto;
|
||||
}
|
||||
.tab:hover{ background: rgba(255,255,255,.05); color: var(--_ink); border-color: var(--_border) }
|
||||
.tab.active{
|
||||
color: var(--_ink);
|
||||
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid2) 18%, transparent), color-mix(in oklab, var(--_acid1) 14%, transparent));
|
||||
border-color: color-mix(in oklab, var(--_acid2) 28%, var(--_border));
|
||||
}
|
||||
.tab-badge{
|
||||
margin-left:8px; padding:2px 6px; border-radius:999px;
|
||||
background: rgba(255,255,255,.1); border:1px solid var(--_border);
|
||||
font-size:.75rem;
|
||||
}
|
||||
|
||||
/* grille & cartes services */
|
||||
.credentials-container{ display:flex; flex-direction:column; gap:12px; scroll-padding-top:56px; }
|
||||
.services-grid{ display:flex; flex-direction:column; gap:12px }
|
||||
|
||||
.service-card{
|
||||
background: color-mix(in oklab, var(--_panel) 88%, transparent);
|
||||
border:1px solid var(--_border);
|
||||
border-radius:16px; overflow:hidden;
|
||||
box-shadow: var(--_shadow);
|
||||
}
|
||||
.service-header{
|
||||
display:flex; align-items:center; gap:8px; padding:12px;
|
||||
cursor:pointer; user-select:none;
|
||||
border-bottom:1px solid color-mix(in oklab, var(--_border) 65%, transparent);
|
||||
}
|
||||
.service-header:hover{ background: rgba(255,255,255,.04) }
|
||||
.service-title{
|
||||
flex:1; font-weight:800; letter-spacing:.2px; font-size:.95rem; text-transform:uppercase;
|
||||
background: linear-gradient(135deg, var(--_acid1), var(--_acid2));
|
||||
-webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
|
||||
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
|
||||
}
|
||||
.service-count{
|
||||
font-weight:800; font-size:.8rem; padding:4px 8px; border-radius:10px;
|
||||
background: rgba(255,255,255,.08); color: var(--_ink); border:1px solid var(--_border);
|
||||
}
|
||||
.service-card[data-credentials]:not([data-credentials="0"]) .service-count{
|
||||
background: linear-gradient(135deg,#2e2e2e,#4CAF50);
|
||||
box-shadow: inset 0 0 0 1px rgba(76,175,80,.35);
|
||||
}
|
||||
.search-container{ position:relative }
|
||||
.search-input{
|
||||
padding:6px 24px 6px 8px; border:none; border-radius:10px;
|
||||
background: rgba(255,255,255,.06); color: var(--_ink); font-size:.82rem;
|
||||
}
|
||||
.search-input:focus{ outline:none; background: rgba(255,255,255,.1) }
|
||||
.clear-button{
|
||||
position:absolute; right:4px; top:50%; transform:translateY(-50%);
|
||||
border:none; background:none; color:#ef4444; cursor:pointer; display:none;
|
||||
}
|
||||
.clear-button.show{ display:block }
|
||||
.download-button{
|
||||
border:1px solid var(--_border); background: rgba(255,255,255,.04);
|
||||
color: var(--_muted); border-radius:8px; padding:4px 8px; cursor:pointer;
|
||||
}
|
||||
.download-button:hover{ color:#e99f00; filter:brightness(1.06) }
|
||||
|
||||
.collapse-indicator{ color: var(--_muted) }
|
||||
.service-card.collapsed .service-content{ max-height:0; overflow:hidden }
|
||||
|
||||
.service-content{ padding:8px 12px }
|
||||
|
||||
/* éléments d’identifiants */
|
||||
.credential-item{
|
||||
border:1px solid var(--_border); border-radius:10px; margin-bottom:6px; padding:8px;
|
||||
background: rgba(255,255,255,.02);
|
||||
display:grid; grid-template-columns: repeat(auto-fit, minmax(120px,1fr)); gap:8px;
|
||||
}
|
||||
.credential-field{ display:flex; align-items:center; gap:6px }
|
||||
.field-label{ font-size:.78rem; color: var(--_muted) }
|
||||
.field-value{
|
||||
flex:1; padding:2px 6px; border-radius:8px; cursor:pointer;
|
||||
white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
|
||||
border:1px solid transparent;
|
||||
}
|
||||
.field-value:hover{ background: rgba(255,255,255,.06); border-color: var(--_border) }
|
||||
|
||||
/* bulles */
|
||||
.bubble-blue{ background: linear-gradient(135deg,#1d2a32,#00c4d6); color:#fff }
|
||||
.bubble-green{ background: linear-gradient(135deg,#1e2a24,#00b894); color:#fff }
|
||||
.bubble-orange{ background: linear-gradient(135deg,#3b2f1a,#e7951a); color:#fff }
|
||||
|
||||
/* toast */
|
||||
.copied-feedback{
|
||||
position:fixed; left:50%; bottom:20px; transform:translateX(-50%);
|
||||
padding:8px 12px; background:#4CAF50; color:#fff; border-radius:10px;
|
||||
box-shadow: var(--_shadow); opacity:0; transition:opacity .25s; z-index:9999;
|
||||
}
|
||||
.copied-feedback.show{ opacity:1 }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="toolbar" id="mainToolbar">
|
||||
<button type="button" onclick="window.location.href='/index.html'" title="Playground">
|
||||
<img src="/web/images/console_icon.png" alt="Bjorn" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/config.html'" title="Config">
|
||||
<img src="/web/images/config_icon.png" alt="Icon_config" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/network.html'" title="Network">
|
||||
<img src="/web/images/network_icon.png" alt="Icon_network" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/netkb.html'" title="NetKB">
|
||||
<img src="/web/images/netkb_icon.png" alt="Icon_netkb" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/credentials.html'" title="Credentials">
|
||||
<img src="/web/images/cred_icon.png" alt="Icon_cred" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/loot.html'" title="Loot">
|
||||
<img src="/web/images/loot_icon.png" alt="Icon_loot" style="height: 50px;">
|
||||
</button>
|
||||
</div>
|
||||
<div class="console-toolbar">
|
||||
<button type="button" class="toolbar-button" onclick="adjustCredFontSize(-1)" title="-">
|
||||
<img src="/web/images/less.png" alt="Icon_less" style="height: 50px;">
|
||||
</button>
|
||||
<button id="toggle-toolbar" type="button" class="toolbar-button" onclick="toggleCredToolbar()" data-open="false">
|
||||
<img id="toggle-icon" src="/web/images/hide.png" alt="Toggle Toolbar" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" class="toolbar-button" onclick="adjustCredFontSize(1)" title="+">
|
||||
<img src="/web/images/plus.png" alt="Icon_plus" style="height: 50px;">
|
||||
</button>
|
||||
</div>
|
||||
<div class="credentials-container">
|
||||
<h1 id="cred-title">Credentials</h1>
|
||||
<div id="credentials-table">
|
||||
<!-- Les tableaux seront insérés ici par JavaScript -->
|
||||
</div>
|
||||
<main class="main" id="main">
|
||||
<div class="credentials-container">
|
||||
|
||||
<div class="stats-bar">
|
||||
<div class="stat-item"><span class="stat-icon">🧩</span><span class="stat-value" id="stat-services">0</span><span class="stat-label">services</span></div>
|
||||
<div class="stat-item"><span class="stat-icon">🔐</span><span class="stat-value" id="stat-creds">0</span><span class="stat-label">credentials</span></div>
|
||||
<div class="stat-item"><span class="stat-icon">🖥️</span><span class="stat-value" id="stat-hosts">0</span><span class="stat-label">unique hosts</span></div>
|
||||
</div>
|
||||
|
||||
<div class="global-search-container">
|
||||
<input type="text" id="global-search-input" class="global-search-input" placeholder="Search Credentials..." oninput="filterAllServices()" />
|
||||
<button class="clear-global-button" onclick="clearGlobalSearch()">✖</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs-container" id="cred-tabs"></div>
|
||||
|
||||
<div class="services-grid" id="credentials-grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="copied-feedback">Copied to clipboard!</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
let fontSize = 12;
|
||||
|
||||
/* =========================
|
||||
Global state
|
||||
========================= */
|
||||
let currentCategory = 'all';
|
||||
let searchGlobal = '';
|
||||
let serviceData = []; // [{ service, category, credentials:{headers,rows} }]
|
||||
|
||||
/* =========================
|
||||
Helpers
|
||||
========================= */
|
||||
function toCaps(s){ return (s||'').toUpperCase(); }
|
||||
function slugify(s){ return (s||'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,''); }
|
||||
function normalizeMac(v){
|
||||
if (!v) return null;
|
||||
const raw = String(v).toLowerCase().replace(/[^0-9a-f]/g,'');
|
||||
if (raw.length !== 12) return null;
|
||||
return raw.match(/.{2}/g).join(':');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Persistence (cards & tabs)
|
||||
========================= */
|
||||
const LS_CARD_PREFIX = 'cred:card:collapsed:'; // per service
|
||||
const LS_TAB_PREFIX = 'cred:tab:autoexpand:'; // per category (tab)
|
||||
|
||||
function setCardCollapsed(service, collapsed){
|
||||
try { localStorage.setItem(LS_CARD_PREFIX+service, collapsed ? '1' : '0'); } catch {}
|
||||
}
|
||||
function getCardCollapsed(service){
|
||||
try {
|
||||
const v = localStorage.getItem(LS_CARD_PREFIX+service);
|
||||
return v === null ? null : (v === '1');
|
||||
} catch { return null; }
|
||||
}
|
||||
function setTabAutoExpand(cat, on){ try { localStorage.setItem(LS_TAB_PREFIX+cat, on?'1':'0'); } catch {} }
|
||||
function isTabAutoExpand(cat){ try { return localStorage.getItem(LS_TAB_PREFIX+cat) === '1'; } catch { return false; } }
|
||||
|
||||
/** Apply persisted collapse states (card-by-card) and tab auto-expand. */
|
||||
function applyPersistedCollapse(){
|
||||
document.querySelectorAll('.service-card').forEach(card=>{
|
||||
const svc = card.dataset.service;
|
||||
const st = getCardCollapsed(svc); // null => no explicit user choice yet
|
||||
if (st === true) card.classList.add('collapsed');
|
||||
if (st === false) card.classList.remove('collapsed');
|
||||
});
|
||||
|
||||
// Auto-expand for the active tab: only open cards without an explicit preference
|
||||
if (isTabAutoExpand(currentCategory)){
|
||||
document.querySelectorAll('.service-card').forEach(card=>{
|
||||
const svc = card.dataset.service;
|
||||
if (getCardCollapsed(svc) === null){
|
||||
card.classList.remove('collapsed');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Touch-friendly horizontal drag (no pointer capture)
|
||||
========================= */
|
||||
function enableTabsDragScroll(el){
|
||||
let isDown = false, startX = 0, startLeft = 0, moved = false;
|
||||
|
||||
const down = (e) => {
|
||||
isDown = true;
|
||||
moved = false;
|
||||
startX = e.pageX || (e.touches && e.touches[0].pageX) || 0;
|
||||
startLeft = el.scrollLeft;
|
||||
};
|
||||
const move = (e) => {
|
||||
if (!isDown) return;
|
||||
const x = e.pageX || (e.touches && e.touches[0].pageX) || 0;
|
||||
const dx = x - startX;
|
||||
if (Math.abs(dx) > 3) moved = true; // small threshold to distinguish click
|
||||
el.scrollLeft = startLeft - dx;
|
||||
};
|
||||
const up = () => { isDown = false; };
|
||||
|
||||
el.addEventListener('pointerdown', down, {passive:true});
|
||||
window.addEventListener('pointermove', move, {passive:true});
|
||||
window.addEventListener('pointerup', up, {passive:true});
|
||||
window.addEventListener('pointercancel', up, {passive:true});
|
||||
|
||||
// If a drag happened, swallow the synthetic click
|
||||
el.addEventListener('click', (e) => {
|
||||
if (moved) { e.preventDefault(); e.stopPropagation(); moved = false; }
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tabsEl = document.getElementById('cred-tabs');
|
||||
if (tabsEl) enableTabsDragScroll(tabsEl);
|
||||
});
|
||||
|
||||
/* =========================
|
||||
Categories / Tabs
|
||||
========================= */
|
||||
function getCategories(){
|
||||
const set = new Set();
|
||||
serviceData.forEach(s => set.add(s.category));
|
||||
return Array.from(set);
|
||||
}
|
||||
|
||||
/** Count credentials (rows) that match current global search, per category and total. */
|
||||
function computeBadgeCounts(){
|
||||
const map = { all: 0 };
|
||||
getCategories().forEach(cat => map[cat] = 0);
|
||||
|
||||
const needle = (searchGlobal || '').toLowerCase();
|
||||
|
||||
serviceData.forEach(svc => {
|
||||
const rows = svc.credentials.rows || [];
|
||||
let matchedCount;
|
||||
|
||||
if (!needle) {
|
||||
matchedCount = rows.length;
|
||||
} else {
|
||||
matchedCount = rows.reduce((acc, row) => {
|
||||
const text = Object.values(row).join(' ').toLowerCase();
|
||||
return acc + (text.includes(needle) ? 1 : 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
map.all += matchedCount;
|
||||
map[svc.category] = (map[svc.category] || 0) + matchedCount;
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function renderTabs(){
|
||||
const tabs = document.getElementById('cred-tabs');
|
||||
const counts = computeBadgeCounts();
|
||||
const cats = ['all', ...getCategories()];
|
||||
|
||||
tabs.innerHTML = cats.map(cat=>{
|
||||
const label = (cat==='all'?'All':toCaps(cat));
|
||||
const count = counts[cat] || 0;
|
||||
const active = (cat===currentCategory) ? 'active':'';
|
||||
return `<div class="tab ${active}" data-cat="${cat}">
|
||||
${label} <span class="tab-badge">${count}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// Click via delegation (robust with drag)
|
||||
tabs.onclick = (e) => {
|
||||
const tab = e.target.closest('.tab');
|
||||
if (!tab) return;
|
||||
|
||||
tabs.querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
|
||||
currentCategory = tab.dataset.cat;
|
||||
|
||||
// Keep the active tab centered (mobile nicety)
|
||||
tab.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
|
||||
|
||||
// Auto-expand for the selected tab (persist)
|
||||
setTabAutoExpand(currentCategory, true);
|
||||
|
||||
renderServices(); // rebuild cards for this tab
|
||||
applyPersistedCollapse(); // reapply collapse + tab auto-expand
|
||||
updateBadges();
|
||||
};
|
||||
}
|
||||
|
||||
function updateBadges(){
|
||||
const counts = computeBadgeCounts();
|
||||
document.querySelectorAll('#cred-tabs .tab').forEach(tab=>{
|
||||
const cat = tab.getAttribute('data-cat');
|
||||
const badge = tab.querySelector('.tab-badge');
|
||||
if (badge) badge.textContent = counts[cat] || 0;
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Stats bar (incl. unique hosts by MAC)
|
||||
========================= */
|
||||
function updateStatsBar(){
|
||||
const totalServices = serviceData.length;
|
||||
const totalCreds = serviceData.reduce((a,s)=>a + (s.credentials.rows?.length || 0), 0);
|
||||
|
||||
const macSet = new Set();
|
||||
serviceData.forEach(s=>{
|
||||
(s.credentials.rows||[]).forEach(r=>{
|
||||
// look for a MAC-looking field
|
||||
let macVal = null;
|
||||
for (const [k,v] of Object.entries(r)) {
|
||||
const key = (k||'').toLowerCase();
|
||||
if (key === 'mac' || key === 'mac address' || key === 'mac_address' || key.includes('mac')) {
|
||||
macVal = v; break;
|
||||
}
|
||||
}
|
||||
const norm = normalizeMac(macVal);
|
||||
if (norm) macSet.add(norm);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('stat-services').textContent = totalServices;
|
||||
document.getElementById('stat-creds').textContent = totalCreds;
|
||||
document.getElementById('stat-hosts').textContent = macSet.size;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Cards / Rows
|
||||
========================= */
|
||||
function createCredentialCard(service, credentials) {
|
||||
const credCount = credentials.rows.length;
|
||||
const borderColor = credCount > 0 ? '#4CAF50' : '#d3d3d3';
|
||||
|
||||
return `
|
||||
<div class="service-card collapsed"
|
||||
data-service="${service}"
|
||||
data-credentials="${credCount}"
|
||||
style="border-color: ${borderColor}">
|
||||
<div class="service-header" onclick="toggleServiceCollapse(this)">
|
||||
<span class="service-title">${toCaps(service)}</span>
|
||||
<span class="service-count" style="background:${credCount>0?'linear-gradient(135deg,#2e2e2e,#4CAF50)':'none'};font-weight:bold;">
|
||||
Credentials: ${credCount}
|
||||
</span>
|
||||
<div class="search-container">
|
||||
<input type="text" class="search-input"
|
||||
data-service="${service}"
|
||||
placeholder="Search..."
|
||||
oninput="filterCredentials(this, '${service}')"
|
||||
onclick="event.stopPropagation()"
|
||||
onkeyup="toggleClearButton(this)" />
|
||||
<button class="clear-button" onclick="clearSearch(this)">✖</button>
|
||||
</div>
|
||||
<button class="download-button" onclick='downloadCredentials(event, "${service}", ${JSON.stringify(credentials).replace(/"/g, '"')})'>💾</button>
|
||||
<span class="collapse-indicator">▼</span>
|
||||
</div>
|
||||
<div class="service-content">
|
||||
${createCredentialsContent(credentials)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function createCredentialsContent(credentials) {
|
||||
return credentials.rows.map(row =>
|
||||
`<div class="credential-item">
|
||||
${Object.entries(row).map(([key, value]) => {
|
||||
const bubbleClass = getBubbleClass(key);
|
||||
const val = (value ?? '').toString();
|
||||
return `
|
||||
<div class="credential-field">
|
||||
<span class="field-label">${key}</span>
|
||||
<div class="field-value ${val.trim()?bubbleClass:''}"
|
||||
data-value="${val.replace(/"/g,'"')}" onclick="copyToClipboard(this)"
|
||||
title="Click to copy">${val}</div>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
function getBubbleClass(key) {
|
||||
const k = (key||'').toLowerCase();
|
||||
if (k === 'port') return 'bubble-orange';
|
||||
if (['ip address','ip','map','hostname','mac address','mac'].includes(k)) return 'bubble-blue';
|
||||
return 'bubble-green';
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Parse backend HTML (/list_credentials)
|
||||
========================= */
|
||||
function parseTable(table) {
|
||||
const headers = Array.from(table.querySelectorAll('th')).map(th => th.textContent.trim());
|
||||
const rows = Array.from(table.querySelectorAll('tr')).slice(1).map(row => {
|
||||
const cells = Array.from(row.querySelectorAll('td'));
|
||||
return Object.fromEntries(headers.map((header, index) => [
|
||||
header,
|
||||
(cells[index]?.textContent || '').trim()
|
||||
]));
|
||||
});
|
||||
return { headers, rows };
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Fetch + Render (with sticky tabs)
|
||||
========================= */
|
||||
function fetchCredentials(){
|
||||
const globalSearchValue = document.getElementById('global-search-input').value.toLowerCase();
|
||||
searchGlobal = globalSearchValue;
|
||||
|
||||
fetch('/list_credentials')
|
||||
.then(r=>r.text())
|
||||
.then(html=>{
|
||||
const doc = new DOMParser().parseFromString(html,'text/html');
|
||||
const tables = doc.querySelectorAll('table');
|
||||
|
||||
serviceData = [];
|
||||
tables.forEach(table=>{
|
||||
const titleEl = table.previousElementSibling;
|
||||
if (titleEl && titleEl.textContent) {
|
||||
const raw = titleEl.textContent.toLowerCase().replace('.csv','').trim();
|
||||
const credentials = parseTable(table);
|
||||
serviceData.push({
|
||||
service: raw,
|
||||
category: raw, // category == service name (dynamic)
|
||||
credentials
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// sort by most credentials first
|
||||
serviceData.sort((a,b)=> (b.credentials.rows?.length||0) - (a.credentials.rows?.length||0));
|
||||
|
||||
updateStatsBar();
|
||||
renderTabs();
|
||||
renderServices();
|
||||
applyPersistedCollapse(); // survive periodic refresh
|
||||
attachSearchListeners();
|
||||
})
|
||||
.catch(err=>console.error('Error:',err));
|
||||
}
|
||||
|
||||
function renderServices(){
|
||||
const grid = document.getElementById('credentials-grid');
|
||||
const needle = (searchGlobal||'').toLowerCase();
|
||||
|
||||
// Filter services by global search (title OR any row content)
|
||||
const searched = serviceData.filter(svc=>{
|
||||
if (!needle) return true;
|
||||
const titleMatch = svc.service.includes(needle);
|
||||
const rowMatch = svc.credentials.rows.some(r => Object.values(r).join(' ').toLowerCase().includes(needle));
|
||||
return titleMatch || rowMatch;
|
||||
});
|
||||
|
||||
// Filter by active category (tab)
|
||||
const byCat = searched.filter(svc => currentCategory==='all' || svc.category===currentCategory);
|
||||
|
||||
if (byCat.length === 0) {
|
||||
grid.innerHTML = `<div style="text-align:center;color:var(--_muted);padding:40px;">
|
||||
<div style="font-size:3rem;margin-bottom:16px;opacity:.5;">🔍</div>No credentials</div>`;
|
||||
updateBadges();
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = byCat.map(s => createCredentialCard(s.service, s.credentials)).join('');
|
||||
|
||||
// If global search active, only show matching rows inside cards and auto-open them
|
||||
if (needle) {
|
||||
document.querySelectorAll('.service-card').forEach(card=>{
|
||||
card.classList.remove('collapsed');
|
||||
card.querySelectorAll('.credential-item').forEach(it=>{
|
||||
const t = it.textContent.toLowerCase();
|
||||
it.style.display = t.includes(needle)?'':'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateBadges();
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Global search
|
||||
========================= */
|
||||
function updateGlobalClearButton(){
|
||||
const btn = document.querySelector('.clear-global-button');
|
||||
if (searchGlobal && searchGlobal.length>0) btn.classList.add('show'); else btn.classList.remove('show');
|
||||
}
|
||||
|
||||
function filterAllServices() {
|
||||
searchGlobal = document.getElementById('global-search-input').value.toLowerCase();
|
||||
renderServices();
|
||||
applyPersistedCollapse();
|
||||
updateGlobalClearButton();
|
||||
}
|
||||
function clearGlobalSearch() {
|
||||
document.getElementById('global-search-input').value = '';
|
||||
searchGlobal = '';
|
||||
renderServices();
|
||||
applyPersistedCollapse();
|
||||
updateGlobalClearButton();
|
||||
document.querySelectorAll('.service-card').forEach(card => card.classList.add('collapsed'));
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Per-card search
|
||||
========================= */
|
||||
let searchTerms = {};
|
||||
let initialCollapsedState = {};
|
||||
|
||||
function filterCredentials(input, service) {
|
||||
const filter = input.value.toLowerCase();
|
||||
searchTerms[service] = filter;
|
||||
|
||||
const card = document.querySelector(`.service-card[data-service="${service}"]`);
|
||||
if (!card) return;
|
||||
const items = card.querySelectorAll('.credential-item');
|
||||
|
||||
if (!(service in initialCollapsedState)) {
|
||||
initialCollapsedState[service] = card.classList.contains('collapsed');
|
||||
}
|
||||
if (filter.length > 0) card.classList.remove('collapsed');
|
||||
|
||||
items.forEach(item=>{
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
function reapplySearchFilters(){
|
||||
Object.keys(searchTerms).forEach(service=>{
|
||||
const filter = searchTerms[service] || '';
|
||||
const card = document.querySelector(`.service-card[data-service="${service}"]`);
|
||||
if (!card) return;
|
||||
const items = card.querySelectorAll('.credential-item');
|
||||
items.forEach(item=>{
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(filter) ? '' : 'none';
|
||||
});
|
||||
const searchInput = card.querySelector('.search-input');
|
||||
if (searchInput) searchInput.value = filter;
|
||||
});
|
||||
}
|
||||
function attachSearchListeners(){
|
||||
document.querySelectorAll('.service-card').forEach(card=>{
|
||||
const service = card.dataset.service;
|
||||
const searchInput = card.querySelector('.search-input');
|
||||
if (searchInput) {
|
||||
searchInput.value = searchTerms[service] || '';
|
||||
searchInput.addEventListener('input', ()=>filterCredentials(searchInput, service));
|
||||
}
|
||||
});
|
||||
}
|
||||
function toggleClearButton(input) {
|
||||
const clearButton = input.nextElementSibling;
|
||||
if (input.value.trim().length > 0) clearButton.classList.add('show'); else clearButton.classList.remove('show');
|
||||
}
|
||||
function clearSearch(button) {
|
||||
const input = button.previousElementSibling;
|
||||
const service = input.getAttribute('data-service');
|
||||
input.value = '';
|
||||
filterCredentials(input, service);
|
||||
|
||||
if (service in initialCollapsedState) {
|
||||
const card = document.querySelector(`.service-card[data-service="${service}"]`);
|
||||
if (card) {
|
||||
if (initialCollapsedState[service]) card.classList.add('collapsed');
|
||||
else card.classList.remove('collapsed');
|
||||
}
|
||||
delete initialCollapsedState[service];
|
||||
}
|
||||
toggleClearButton(input);
|
||||
}
|
||||
|
||||
/* =========================
|
||||
UX bits
|
||||
========================= */
|
||||
|
||||
function toggleServiceCollapse(header) {
|
||||
const card = header.closest('.service-card');
|
||||
const nowCollapsed = !card.classList.contains('collapsed');
|
||||
card.classList.toggle('collapsed');
|
||||
const svc = card.dataset.service;
|
||||
if (svc) setCardCollapsed(svc, nowCollapsed); // remember user's choice
|
||||
}
|
||||
function downloadCredentials(event, service, credentials) {
|
||||
event.stopPropagation();
|
||||
if (!credentials.rows || credentials.rows.length===0) return;
|
||||
const headers = Object.keys(credentials.rows[0]);
|
||||
let csv = headers.join(',') + '\n';
|
||||
credentials.rows.forEach(row=>{
|
||||
const values = headers.map(h=>{
|
||||
const v = (row[h] ?? '').toString();
|
||||
return v.includes(',') ? `"${v.replace(/"/g,'""')}"` : v;
|
||||
});
|
||||
csv += values.join(',') + '\n';
|
||||
});
|
||||
const blob = new Blob([csv], {type:'text/csv'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = `${service}_credentials.csv`;
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
function showCopyFeedback() {
|
||||
const feedback = document.querySelector('.copied-feedback');
|
||||
feedback.classList.add('show');
|
||||
setTimeout(() => feedback.classList.remove('show'), 1500);
|
||||
}
|
||||
function copyToClipboard(el) {
|
||||
const text = el.getAttribute('data-value') || '';
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text; document.body.appendChild(ta);
|
||||
ta.select(); document.execCommand('copy'); document.body.removeChild(ta);
|
||||
showCopyFeedback();
|
||||
const bg = el.style.background;
|
||||
el.style.background='#4CAF50'; setTimeout(()=>el.style.background=bg, 500);
|
||||
}
|
||||
|
||||
|
||||
/* =========================
|
||||
Boot
|
||||
========================= */
|
||||
document.addEventListener('DOMContentLoaded', ()=>{
|
||||
document.getElementById('global-search-input').addEventListener('input', filterAllServices);
|
||||
fetchCredentials();
|
||||
setInterval(fetchCredentials, 30000);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
63
web/css/all.min.css
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
/* Font Awesome Base Styles */
|
||||
.fa, .fas {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Icon Definitions */
|
||||
.fa-th-list:before {
|
||||
content: "\f00b"; /* Icon for toggle between list and grid view */
|
||||
}
|
||||
.fa-object-group:before {
|
||||
content: "\f247"; /* Icon for multi-selection */
|
||||
}
|
||||
.fa-folder-plus:before {
|
||||
content: "\f65e"; /* Icon for adding a new folder */
|
||||
}
|
||||
.fa-edit:before {
|
||||
content: "\f044"; /* Icon for renaming */
|
||||
}
|
||||
.fa-arrows-alt:before {
|
||||
content: "\f0b2"; /* Icon for moving */
|
||||
}
|
||||
.fa-trash:before {
|
||||
content: "\f1f8"; /* Icon for deletion */
|
||||
}
|
||||
.fa-folder:before {
|
||||
content: "\f07b"; /* Icon for folder */
|
||||
}
|
||||
.fa-times:before {
|
||||
content: "\f00d"; /* Icon for cancel in modals */
|
||||
}
|
||||
.fa-check:before {
|
||||
content: "\f00c"; /* Icon for confirmation in modals */
|
||||
}
|
||||
|
||||
/* Font Faces */
|
||||
@font-face {
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: block;
|
||||
src: url(../css/fonts/fa-regular-400.woff2) format("woff2"),
|
||||
url(../css/fonts/fa-regular-400.woff) format("woff"),
|
||||
url(../css/fonts/fa-regular-400.ttf) format("truetype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: block;
|
||||
src: url(../css/fonts/fa-solid-900.woff2) format("woff2"),
|
||||
url(../css/fonts/fa-solid-900.woff) format("woff"),
|
||||
url(../css/fonts/fa-solid-900.ttf) format("truetype");
|
||||
}
|
||||
.fas {
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-weight: 900;
|
||||
}
|
||||
BIN
web/css/fonts/Viking.TTF
Normal file
BIN
web/css/fonts/fa-regular-400.ttf
Normal file
BIN
web/css/fonts/fa-regular-400.woff
Normal file
BIN
web/css/fonts/fa-regular-400.woff2
Normal file
BIN
web/css/fonts/fa-solid-900.ttf
Normal file
BIN
web/css/fonts/fa-solid-900.woff
Normal file
BIN
web/css/fonts/fa-solid-900.woff2
Normal file
631
web/css/global.css
Normal file
@@ -0,0 +1,631 @@
|
||||
/* ======================================================================
|
||||
Bjorn Global CSS — Tokens + Components (cleaned, deduped, variabilized)
|
||||
Single source of truth for design tokens & components across pages
|
||||
----------------------------------------------------------------------
|
||||
• All colors come from :root variables. No standalone literals allowed.
|
||||
• Duplicates merged; conflicting blocks unified; comments explain intent.
|
||||
• Safe defaults + mobile-first tweaks; keeps your AcidBurn/Nordic vibe.
|
||||
====================================================================== */
|
||||
|
||||
/* ==============================
|
||||
0) Design Tokens (CSS Vars)
|
||||
============================== */
|
||||
:root{
|
||||
/* ---- Base palette (solids) --------------------------------------- */
|
||||
--black:#000000; /* .black #000000 */
|
||||
--white:#ffffff; /* .white #ffffff */
|
||||
--bg:#050709; /* page background */
|
||||
--bg-2:#0b0f14; /* secondary background */
|
||||
--ink:#e6fff7; /* primary text */
|
||||
--muted:#8affc1cc; /* subdued text */
|
||||
--acid:#00ff9a; /* neon green primary accent */
|
||||
--acid-2:#18f0ff; /* cyan secondary accent */
|
||||
--danger:#ff3b3b; /* error/danger */
|
||||
--warning:#ffd166; /* warning */
|
||||
--ok:#2cff7e; /* success/ok */
|
||||
--ink-invert:#001014; /* text on bright chips */
|
||||
|
||||
/* ---- Extended opacities (avoid raw rgba) ------------------------- */
|
||||
--black-00:rgba(0,0,0,0); /* fully transparent */
|
||||
--black-10:rgba(0,0,0,.10);
|
||||
--black-50:rgba(0,0,0,.5);
|
||||
--white-05:rgba(255,255,255,.05);
|
||||
--white-06:rgba(255,255,255,.06);
|
||||
--white-10:rgba(255,255,255,.10);
|
||||
--white-12:rgba(255,255,255,.12);
|
||||
--white-20:rgba(255,255,255,.20);
|
||||
|
||||
/* ---- Surfaces / panels ------------------------------------------- */
|
||||
--panel:#0e1717; /* base card surface */
|
||||
--panel-2:#101c1c; /* deeper card */
|
||||
--border:#0f2b2b; /* neutral border */
|
||||
--c-btn:#0d151c; /* button surface */
|
||||
--c-panel:#0b1218; /* control surface */
|
||||
--c-panel-2:#0a1118; /* alt control surface */
|
||||
--c-pill-bg:#0c141b; /* pill bg */
|
||||
--c-chip-bg:#07121a; /* chip bg */
|
||||
--c-slot:#0e1a22; /* small slot/bg bars */
|
||||
--neutral-44:#444444; /* status neutral dot */
|
||||
--muted-off:#666666; /* disabled */
|
||||
|
||||
/* ---- Brand accents ------------------------------------------------ */
|
||||
--accent:#22f0b4; /* brand accent A */
|
||||
--accent-2:#18d6ff; /* brand accent B */
|
||||
--ring:color-mix(in oklab, var(--accent) 60%, var(--white) 8%);
|
||||
|
||||
/* ---- Borders (themed) -------------------------------------------- */
|
||||
--c-border:#00ffff22; /* subtle */
|
||||
--c-border-strong:#00ffff33; /* medium */
|
||||
--c-border-hi:#00ffff44; /* high */
|
||||
--c-border-muted:#00ffff11; /* hairline */
|
||||
|
||||
/* ---- Overlays / glass -------------------------------------------- */
|
||||
--overlay-bg:rgba(5,9,15,.55); /* soft overlay */
|
||||
--overlay-solid:rgba(7,16,24,.92); /* dense overlay */
|
||||
--glass-8:#00000088; /* glass tint */
|
||||
|
||||
/* ---- Scrollbars --------------------------------------------------- */
|
||||
--sb-size:10px;
|
||||
--sb-track:#07121a;
|
||||
--sb-thumb:#09372b;
|
||||
--sb-thumb-hi:var(--acid);
|
||||
--sb-outline:#00ff9a33;
|
||||
|
||||
/* ---- Switch / toggles -------------------------------------------- */
|
||||
--switch-track:#111111;
|
||||
--switch-on-bg:#022a1a;
|
||||
--switch-thumb:var(--acid);
|
||||
|
||||
/* ---- Slider (number input) --------------------------------------- */
|
||||
--slider-h:6px;
|
||||
--slider-thumb:var(--acid);
|
||||
--slider-thumb-size:18px;
|
||||
--slider-track:color-mix(in oklab, var(--acid) 18%, var(--c-panel));
|
||||
--slider-track-fill:color-mix(in oklab, var(--acid) 42%, var(--c-panel));
|
||||
--slider-focus:color-mix(in oklab, var(--acid) 60%, transparent);
|
||||
|
||||
/* ---- Effects / glows / shadows ----------------------------------- */
|
||||
--acid-0f:#00ff9a0f;
|
||||
--acid-1a:#00ff9a1a;
|
||||
--acid-2a:#00ff9a2a;
|
||||
--acid-22:#00ff9a22;
|
||||
--acid-33:#00ff9a33;
|
||||
--glow-weak:var(--acid-10, #00ff9a10);
|
||||
--glow-mid:#00ff9a22;
|
||||
--glow-strong:#00ff9a33;
|
||||
--grid:repeating-linear-gradient(0deg, transparent 0 28px, var(--acid-0f) 28px 29px), repeating-linear-gradient(90deg, transparent 0 28px, var(--acid-0f) 28px 29px);
|
||||
--shadow:0 10px 30px var(--acid-1a), inset 0 0 0 1px var(--acid-22);
|
||||
--shadow-hover:0 14px 34px var(--acid-2a), inset 0 0 0 1px var(--acid-33);
|
||||
--resize-stripe:linear-gradient(90deg, transparent 0 40%, var(--glow-strong) 40% 60%, transparent 60% 100%);
|
||||
--text-gradient:linear-gradient(180deg, transparent 0%, #00ff9a07 70%, #01180f8c 100%);
|
||||
|
||||
/* ---- Gradients ---------------------------------------------------- */
|
||||
--grad-bg-1:radial-gradient(1000px 500px at 10% -5%, #0aff9922, transparent 60%);
|
||||
--grad-bg-2:radial-gradient(800px 400px at 110% 10%, #18f0ff22, transparent 60%);
|
||||
--grad-topbar:linear-gradient(#0c1118, #0a0e14);
|
||||
--grad-sidebar:linear-gradient(180deg, #0a1016, #05080c);
|
||||
--grad-card:linear-gradient(180deg, #0b1218, #070b10);
|
||||
--grad-bottombar:linear-gradient(#0a0e14, #091017);
|
||||
--grad-hero-base:#071016;
|
||||
--grad-hero:radial-gradient(800px 200px at 80% -20%, #18f0ff22, transparent 60%), var(--grad-hero-base);
|
||||
--grad-modal:linear-gradient(180deg, #0a1016, #05080c);
|
||||
--grad-quickpanel:linear-gradient(180deg, #09111a, #050a0f);
|
||||
--grad-console:linear-gradient(180deg, #071018, #05090f);
|
||||
--grad-dropdown:linear-gradient(180deg, #0a1116, #05090f);
|
||||
--grad-chip-selected:linear-gradient(180deg, #0b151c, #091219);
|
||||
--grad-qprow:linear-gradient(180deg, #09121a, #080e14);
|
||||
|
||||
/* ---- Console severities ------------------------------------------ */
|
||||
--log-debug-ink:#c9d4df; --log-debug-bg:#2b3a48;
|
||||
--log-info-ink:#ffffff; --log-info-bg:#007b99;
|
||||
--log-warn-ink:#1a1200; --log-warn-bg:#ffc94d;
|
||||
--log-error-ink:#ffffff; --log-error-bg:#cc2b2b;
|
||||
--log-critical-ink:#ffffff; --log-critical-bg:#a00028; --log-critical-glow:#ff004444;
|
||||
--log-success-ink:#002b14; --log-success-bg:var(--ok);
|
||||
--log-failed-ink:#ffffff; --log-failed-bg:var(--danger);
|
||||
--log-connected-ink:#00331a; --log-connected-bg:var(--acid);
|
||||
|
||||
/* ---- Log level badge tokens -------------------------------------- */
|
||||
--lvl-debug-top:#24323a; --lvl-debug-bot:#1a262e; --lvl-debug-ink:#9bd3ff; --lvl-debug-bdr:#2a91ff44;
|
||||
--lvl-info-top:#12343a; --lvl-info-bot:#0e2a30; --lvl-info-ink:#7ee3ff; --lvl-info-bdr:#18f0ff55;
|
||||
--lvl-warn-top:#3a3312; --lvl-warn-bot:#2a240e; --lvl-warn-ink:#ffd166; --lvl-warn-bdr:#ffd16666;
|
||||
--lvl-error-top:#3a1616; --lvl-error-bot:#2a0e0e; --lvl-error-ink:#ff7b7b; --lvl-error-bdr:#ff3b3b66;
|
||||
--lvl-crit-top:#3a1226; --lvl-crit-bot:#2a0e1d; --lvl-crit-ink:#ff8ad6; --lvl-crit-bdr:#ff4fcf66;
|
||||
--lvl-succ-top:#123a22; --lvl-succ-bot:#0e2a1a; --lvl-succ-ink:#7dffb0; --lvl-succ-bdr:#2cff7e66;
|
||||
--lvl-fail-top:#3a1616; --lvl-fail-bot:#2a0e0e; --lvl-fail-ink:#ff7b7b; --lvl-fail-bdr:#ff3b3b66;
|
||||
--lvl-conn-top:#123a26; --lvl-conn-bot:#0e2a1a; --lvl-conn-ink:#7dffb0; --lvl-conn-bdr:#00ff9a66;
|
||||
--lvl-sse-top:#b68b00; --lvl-sse-bot:#6b4601; --lvl-sse-ink:#ac7000; --lvl-sse-bdr:#ca9b0055;
|
||||
|
||||
/* ---- Component specials (former hard-codes) ---------------------- */
|
||||
--btn-bg-solid:#0f1919;
|
||||
--switch-alt-rail:#122121;
|
||||
--switch-alt-thumb:#1b2b2b;
|
||||
--pill-alt-bg:#122121;
|
||||
--pill-alt-bdr:#143030;
|
||||
|
||||
/* ---- Layout / radii / sizes -------------------------------------- */
|
||||
--radius:14px;
|
||||
--h-topbar:56px;
|
||||
--h-bottombar:56px;
|
||||
--control-h:38px;
|
||||
--control-r:10px;
|
||||
--control-pad-x:12px;
|
||||
--gap-1:6px; --gap-2:8px; --gap-3:10px; --gap-4:12px;
|
||||
--elev:var(--shadow);
|
||||
|
||||
/* ---- Typography --------------------------------------------------- */
|
||||
--font-mono:14px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
|
||||
/* ---- QuickPanel sizes -------------------------------------------- */
|
||||
--qp-h:88vh; --qp-overshoot:60px;
|
||||
|
||||
/* ---- Console ------------------------------------------------------ */
|
||||
--console-tab:28px;
|
||||
|
||||
/* ---- Backdrop ----*/
|
||||
--backdrop-dim: rgba(0,0,0,.55);
|
||||
|
||||
/* ---- Scanline helpers (no raw rgba in rules) --------------------- */
|
||||
--scanline-a:var(--black-00);
|
||||
--scanline-b:var(--black-10);
|
||||
|
||||
color-scheme:dark;
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
1) Resets & Base
|
||||
============================== */
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%;overflow:clip}
|
||||
body{background:var(--grad-bg-1), var(--grad-bg-2), var(--bg);color:var(--ink);font:var(--font-mono)}
|
||||
a{color:var(--acid);text-decoration:none}
|
||||
.spacer{flex:1}
|
||||
.icon{width:16px;height:16px;display:inline-block}
|
||||
.scanlines{position:fixed;inset:0;pointer-events:none;opacity:.33;background-image:linear-gradient(var(--scanline-a) 50%, var(--scanline-b) 50%);background-size:100% 2px;mix-blend-mode:overlay}
|
||||
|
||||
/* ==============================
|
||||
2) Scrollbars (WebKit + Firefox)
|
||||
============================== */
|
||||
*::-webkit-scrollbar{width:var(--sb-size);height:var(--sb-size)}
|
||||
*::-webkit-scrollbar-track{background:var(--sb-track);border-left:1px solid var(--c-border);border-right:1px solid var(--c-border)}
|
||||
*::-webkit-scrollbar-thumb{background:linear-gradient(180deg, color-mix(in oklab, var(--sb-thumb) 70%, transparent), var(--sb-thumb));border:2px solid var(--sb-track);border-radius:12px;box-shadow:0 0 14px var(--sb-outline) inset, 0 0 10px var(--sb-outline)}
|
||||
*::-webkit-scrollbar-thumb:hover{background:linear-gradient(180deg, color-mix(in oklab, var(--sb-thumb-hi) 70%, transparent), var(--sb-thumb-hi))}
|
||||
*::-webkit-scrollbar-corner{background:var(--sb-track)}
|
||||
*{scrollbar-width:thin;scrollbar-color:var(--sb-thumb) var(--sb-track)}
|
||||
|
||||
/* ==============================
|
||||
3) Layout: Topbar / Sidebar / Main / Bottombar
|
||||
============================== */
|
||||
.topbar{position:fixed;top:0;left:0;right:0;height:var(--h-topbar);display:flex;align-items:center;gap:var(--gap-3);padding:0 14px;background:var(--grad-topbar);border-bottom:1px solid var(--c-border);z-index:20}
|
||||
.logo{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}
|
||||
.logo .sig{width:42px;height:42px;object-fit:contain;border-radius:6px;background:none!important;box-shadow:none;filter:drop-shadow(0 0 12px color-mix(in oklab, var(--acid) 60%, transparent))}
|
||||
.actions{position:relative}
|
||||
.dropdown{position:absolute;right:0;top:48px;min-width:320px;background:var(--grad-dropdown);border:1px solid var(--c-border-strong);border-radius:12px;box-shadow:0 20px 60px var(--glow-strong);display:none;z-index:30;overflow:hidden}
|
||||
.dropdown.show{display:block}
|
||||
.menuitem{display:flex;align-items:center;gap:10px;padding:10px 12px;cursor:pointer;border-bottom:1px dashed var(--c-border)}
|
||||
.menuitem:last-child{border-bottom:none}
|
||||
.menuitem:hover{background:var(--c-panel)}
|
||||
.menuitem .mi-icon{width:16px}
|
||||
@media (max-width:700px){.dropdown{top:44px;min-width:320px}}
|
||||
|
||||
body:not(:has(#sidebar)) .main{left:0 !important}
|
||||
.sidebar{position:fixed;left:0;top:var(--h-topbar);bottom:var(--h-bottombar);width:280px;background:var(--grad-sidebar);border-right:1px solid var(--c-border);transform:translateX(0);transition:.28s cubic-bezier(.2,.8,.2,1);z-index:15;display:flex;flex-direction:column}
|
||||
.sidebar.hidden{transform:translateX(-100%)}
|
||||
.sidehead{padding:12px 12px 8px;border-bottom:1px dashed var(--c-border);display:flex;align-items:center;gap:10px}
|
||||
.sidetitle{font-weight:700;color:var(--muted)}
|
||||
.sidecontent{padding:5px;overflow:auto;flex:1}
|
||||
|
||||
.main{position:fixed;left:280px;right:0;top:var(--h-topbar);bottom:var(--h-bottombar);overflow:auto;padding:16px;transition:.25s}
|
||||
.sidebar.hidden + .main{left:0}
|
||||
.hero{min-height:220px;border-radius:16px;background:var(--grid), var(--grad-hero);border:1px solid var(--c-border);box-shadow:var(--shadow);display:grid;align-items:center;justify-items:center;text-align:center;padding:24px}
|
||||
.hero-btn{border-radius:16px;background:var(--grid), var(--grad-hero);border:1px solid var(--c-border);box-shadow:var(--shadow);display:grid;align-items:center;justify-items:center;text-align:center;padding:6px}
|
||||
|
||||
.bottombar{position:fixed;left:0;right:0;bottom:0;height:var(--h-bottombar);background:var(--grad-bottombar);border-top:1px solid var(--c-border);display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:10px;padding:0 10px;z-index:61}
|
||||
.bottombar:hover{box-shadow:0 -10px 30px var(--glow-mid), inset 0 0 0 1px var(--glow-mid)}
|
||||
.bottombar.hidden{transform:translateY(100%)}
|
||||
/* merged duplicate: we keep the grid variant */
|
||||
.status-left{display:grid;grid-template-columns:auto 1fr;align-items:center;column-gap:10px}
|
||||
.status-text{display:grid;grid-auto-rows:min-content;row-gap:2px;min-height:40px;align-content:center}
|
||||
#bjornStatus2:empty{display:none}
|
||||
.status-center{display:flex;align-items:center;justify-content:center;justify-self:center;position:relative}
|
||||
.status-right{display:flex;align-items:center;gap:10px;justify-self:end}
|
||||
.status-character{display:flex;align-items:center;justify-content:center}
|
||||
.status-character .bjorn-dropdown,.status-center .bjorn-dropdown{position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%)}
|
||||
.dock{display:flex;align-items:center;gap:8px;background:var(--c-panel);border:1px solid var(--c-border-strong);border-radius:14px;padding:6px 8px;box-shadow:var(--shadow)}
|
||||
|
||||
/* ==============================
|
||||
4) Helpers & Layout Utilities
|
||||
============================== */
|
||||
.grid-stack{display:grid;gap:14px}
|
||||
.grid-auto-260{display:grid;grid-template-columns:repeat(auto-fit, minmax(260px, 1fr));gap:10px 16px}
|
||||
.grid-auto-320{display:grid;grid-template-columns:repeat(auto-fit, minmax(320px, 1fr));gap:14px}
|
||||
.align-end{justify-self:end}
|
||||
.card-header{display:flex;align-items:center;justify-content:space-between;gap:10px}
|
||||
.card-title{margin:0;font-weight:700;color:var(--acid)}
|
||||
|
||||
/* ==============================
|
||||
5) Buttons / Pills / Chips
|
||||
============================== */
|
||||
.btn{display:inline-flex;align-items:center;gap:var(--gap-2);padding:8px 12px;border-radius:var(--control-r);background:var(--c-btn);border:1px solid var(--c-border-strong);color:var(--ink);cursor:pointer;user-select:none;transition:.2s;box-shadow:var(--shadow)}
|
||||
.btn:hover{transform:translateY(-1px);box-shadow:var(--shadow-hover)}
|
||||
.btn .dot{width:8px;height:8px;border-radius:50%;background:var(--acid);box-shadow:0 0 12px var(--acid)}
|
||||
|
||||
.pill{padding:4px 8px;border-radius:10px;border:1px solid var(--c-border);background:var(--c-pill-bg);color:var(--muted)}
|
||||
|
||||
.chips{display:flex;flex-wrap:wrap;gap:8px}
|
||||
.chips.nowrap{flex-wrap:nowrap;overflow:auto;scrollbar-width:thin}
|
||||
.chips.center{justify-content:center}
|
||||
.chips.end{justify-content:flex-end}
|
||||
.chip{display:inline-flex;align-items:center;gap:8px;padding:6px 10px;border-radius:999px;background:var(--c-chip-bg);border:1px solid var(--c-border-hi);cursor:pointer;user-select:none;transition:.18s}
|
||||
.chip:hover{box-shadow:0 0 0 1px var(--c-border-hi) inset, 0 8px 22px var(--glow-weak)}
|
||||
.chip:active{transform:translateY(1px)}
|
||||
.chip:focus-visible{outline:2px solid color-mix(in oklab, var(--acid) 55%, transparent);outline-offset:2px}
|
||||
.chip .icon{flex:0 0 auto}
|
||||
.chip-close{all:unset;display:inline-grid;place-items:center;cursor:pointer;padding:0 6px;height:22px;min-width:22px;border-radius:6px;border:1px solid var(--c-border);background:var(--c-panel-2)}
|
||||
.chip-close:hover{box-shadow:0 0 0 1px var(--c-border) inset, 0 0 10px var(--glow-strong)}
|
||||
.chip[aria-selected="true"],.chip.is-selected{background:var(--grad-chip-selected);border-color:color-mix(in oklab, var(--acid) 55%, transparent)}
|
||||
.chip.is-ghost{background:transparent;border-style:dashed}
|
||||
.chip.is-ok{border-color:color-mix(in oklab, var(--ok) 65%, transparent)}
|
||||
.chip.is-warn{border-color:color-mix(in oklab, var(--warning) 65%, transparent)}
|
||||
.chip.is-danger{border-color:color-mix(in oklab, var(--danger) 65%, transparent)}
|
||||
.chip.sm{padding:4px 8px;font-size:12px}
|
||||
.chip.lg{padding:8px 12px;font-size:15px}
|
||||
.chip[draggable="true"]{cursor:grab}
|
||||
.chip.dragging{opacity:.7;outline:2px dashed var(--c-border-hi)}
|
||||
|
||||
.chips-input{display:flex;align-items:center;gap:8px;flex-wrap:wrap;border:1px dashed var(--c-border);border-radius:10px;background:var(--c-panel);padding:8px 10px}
|
||||
.chips-input input{flex:1;min-width:120px;background:transparent;border:1px solid var(--c-border-strong);border-radius:8px;padding:6px 8px;color:var(--ink)}
|
||||
.chips-input input::placeholder{color:var(--muted)}
|
||||
.chips-input .chip{margin:0}
|
||||
|
||||
.chip-field{display:grid;gap:8px;padding:8px 10px;border:1px dashed var(--c-border);border-radius:10px;background:var(--c-panel)}
|
||||
.chip-field>label{font-weight:700;color:var(--muted);word-break:break-all}
|
||||
.chip-list{display:flex;flex-wrap:wrap;gap:8px}
|
||||
|
||||
/* ==============================
|
||||
6) Forms (fields, toggles, inputs)
|
||||
============================== */
|
||||
.form-field{display:grid;gap:8px;padding:8px 10px;border:1px dashed var(--c-border);border-radius:10px;background:var(--c-panel)}
|
||||
.form-field>label{font-weight:700;color:var(--muted);word-break:break-all}
|
||||
.form-list{display:grid;gap:8px;padding:8px 10px;border:1px dashed var(--c-border);border-radius:10px;background:var(--c-panel)}
|
||||
.form-list>label{font-weight:700;color:var(--muted);word-break:break-all}
|
||||
.form-addrow{display:flex;gap:8px}
|
||||
.form-addrow input{flex:1;min-width:120px}
|
||||
|
||||
.input,.select{height:var(--control-h);border-radius:var(--control-r);border:1px solid var(--c-border-strong);background:var(--c-panel);color:var(--ink);padding:0 var(--control-pad-x);font:inherit}
|
||||
|
||||
.row-toggle{display:grid;grid-template-columns:1fr auto;align-items:center;gap:10px;padding:8px 10px;border:1px dashed var(--c-border);border-radius:10px;background:var(--c-panel-2)}
|
||||
.row-toggle>label{color:var(--muted);word-break:break-all;font-weight:600}
|
||||
|
||||
/* Toggle (standalone label+input pattern) */
|
||||
.toggle{position:relative;display:inline-block;width:46px;height:26px}
|
||||
.toggle input{opacity:0;width:0;height:0}
|
||||
.toggle .slider{position:absolute;inset:0;cursor:pointer;background:var(--switch-track);border:1px solid var(--c-border-hi);border-radius:99px;box-shadow:inset 0 0 0 1px var(--glow-mid);transition:.18s}
|
||||
.toggle .slider::before{content:"";position:absolute;left:2px;top:2px;width:22px;height:22px;border-radius:50%;background:var(--switch-thumb);box-shadow:0 0 10px var(--acid);transform:translateX(0);transition:.18s}
|
||||
.toggle input:checked + .slider{background:var(--switch-on-bg)}
|
||||
.toggle input:checked + .slider::before{transform:translateX(20px)}
|
||||
|
||||
/* Numeric input */
|
||||
.input-number{display:inline-flex;align-items:center;gap:var(--gap-2);height:var(--control-h);border-radius:var(--control-r);border:1px solid var(--c-border-strong);background:var(--c-panel);color:var(--ink);padding:0 8px}
|
||||
.input-number input[type="number"]{width:120px;height:calc(var(--control-h) - 4px);border:0;outline:0;background:transparent;color:var(--ink);font:inherit;padding:0 6px;appearance:textfield;-moz-appearance:textfield;-webkit-appearance:none}
|
||||
.input-number input[type="number"]::-webkit-outer-spin-button,.input-number input[type="number"]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
||||
.input-number input[type="number"]:-webkit-autofill{-webkit-text-fill-color:var(--ink);-webkit-box-shadow:0 0 0 1000px var(--c-panel) inset;box-shadow:0 0 0 1000px var(--c-panel) inset}
|
||||
.input-number [data-act]{width:30px;height:30px;border-radius:8px;border:1px solid var(--c-border-strong);background:var(--c-btn);cursor:pointer}
|
||||
.input-number [data-act]:hover{box-shadow:0 0 0 1px var(--c-border-strong) inset, 0 8px 22px var(--glow-weak)}
|
||||
.input-number [data-act]:active{transform:translateY(1px)}
|
||||
|
||||
/* Number + range compound */
|
||||
.input-number-w-slider{display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center;height:var(--control-h);padding:0 8px;border:1px solid var(--c-border-strong);border-radius:var(--control-r);background:var(--c-panel);color:var(--ink)}
|
||||
.input-number-w-slider input[type="number"]{width:120px;height:calc(var(--control-h) - 4px);border:0;outline:0;background:transparent;color:var(--ink);font:inherit;padding:0 6px;appearance:textfield;-moz-appearance:textfield;-webkit-appearance:none}
|
||||
.input-number-w-slider input[type="number"]::-webkit-outer-spin-button,.input-number-w-slider input[type="number"]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
||||
.input-number-w-slider input[type="number"]:-webkit-autofill{-webkit-text-fill-color:var(--ink);-webkit-box-shadow:0 0 0 1000px var(--c-panel) inset;box-shadow:0 0 0 1000px var(--c-panel) inset}
|
||||
|
||||
/* Range (base) */
|
||||
.input-number-w-slider input[type="range"]{-webkit-appearance:none;appearance:none;width:100%;height:var(--slider-h);background:transparent;cursor:pointer}
|
||||
.input-number-w-slider input[type="range"]:focus{outline:none}
|
||||
|
||||
/* WebKit track + filled track via gradient */
|
||||
.input-number-w-slider input[type="range"]::-webkit-slider-runnable-track{height:var(--slider-h);border-radius:999px;background:linear-gradient(90deg, var(--slider-track-fill) 0 var(--_fill,0%), var(--slider-track) var(--_fill,0%) 100%)}
|
||||
.input-number-w-slider input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:var(--slider-thumb-size);height:var(--slider-thumb-size);margin-top:calc((var(--slider-h) - var(--slider-thumb-size)) / 2);border-radius:50%;background:var(--slider-thumb);border:1px solid var(--c-border-hi);box-shadow:0 0 10px var(--acid)}
|
||||
|
||||
/* Firefox track + progress */
|
||||
.input-number-w-slider input[type="range"]::-moz-range-track{height:var(--slider-h);border-radius:999px;background:var(--slider-track)}
|
||||
.input-number-w-slider input[type="range"]::-moz-range-progress{height:var(--slider-h);border-radius:999px;background:var(--slider-track-fill)}
|
||||
.input-number-w-slider input[type="range"]::-moz-range-thumb{width:var(--slider-thumb-size);height:var(--slider-thumb-size);border-radius:50%;background:var(--slider-thumb);border:1px solid var(--c-border-hi);box-shadow:0 0 10px var(--acid)}
|
||||
|
||||
/* Focus ring harmonized */
|
||||
.input-number-w-slider:has(input:focus){box-shadow:0 0 0 2px var(--slider-focus) inset}
|
||||
.input-number-w-slider.steppers{grid-template-columns:auto 1fr auto auto}
|
||||
.input-number-w-slider:not(.steppers){grid-template-columns:1fr auto}
|
||||
|
||||
/* ==============================
|
||||
7) Editor
|
||||
============================== */
|
||||
.editor-textarea{width:100%;min-height:300px;resize:vertical;background:var(--c-panel);color:var(--ink);border:1px solid var(--c-border-strong);border-radius:10px;padding:12px;font:var(--font-mono);line-height:1.5;box-shadow:var(--shadow);background-image:var(--text-gradient);background-attachment:local;overflow:auto}
|
||||
.editor-textarea:focus{outline:none;border-color:var(--acid);box-shadow:0 0 12px var(--acid)}
|
||||
.editor-textarea-container{display:flex;flex-direction:column;gap:10px;height:100%}
|
||||
#editor-textarea{flex:1;min-height:300px;width:100%;resize:none;font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;font-size:14px;box-sizing:border-box}
|
||||
|
||||
/* ==============================
|
||||
8) Console
|
||||
============================== */
|
||||
.character-wrap{display:flex;align-items:center;gap:8px;max-width:100%}
|
||||
.status-character img{flex-shrink:0}
|
||||
.bjorn-say,.bjorn-status,.bjorn-status2{white-space:normal;overflow-wrap:break-word;word-break:break-word;font-size:clamp(10px, 1vw, 14px);max-width:200px;text-align:left}
|
||||
#bjornSay,#bjornStatus,#bjornStatus2{flex:1;white-space:normal;overflow-wrap:break-word;word-break:break-word;line-height:1.2;font-size:clamp(10px, 1vw, 14px);max-height:calc(var(--h-bottombar) - 8px);overflow-y:auto}
|
||||
#logout{padding:12px}
|
||||
|
||||
.console{position:fixed;left:1px;right:10px;bottom:var(--h-bottombar);height:48vh;background:var(--grad-console);border:1px solid var(--c-border-hi);border-radius:14px 14px 12px 12px;box-shadow:0 -30px 80px var(--glow-strong), inset 0 0 0 1px var(--glow-mid);z-index:60;display:grid;grid-template-rows:8px auto auto 1fr;transform:translateY(100%);transition:transform .25s ease}
|
||||
.console.open{transform:translateY(0)}
|
||||
.console-head{display:flex;align-items:center;gap:10px;padding:8px 10px}
|
||||
.console-body{overflow:auto}
|
||||
.logline{white-space:pre-wrap;border-bottom:1px dashed var(--c-border-muted);padding:6px 0}
|
||||
.console-resize{position:sticky;top:0;left:0;right:0;height:8px;cursor:ns-resize;background:var(--resize-stripe);border-radius:14px 14px 0 0;z-index:5}
|
||||
|
||||
.console-body .debug{color:var(--log-debug-ink);background:var(--log-debug-bg)}
|
||||
.console-body .info{color:var(--log-info-ink);background:var(--log-info-bg)}
|
||||
.console-body .warning{color:var(--log-warn-ink);background:var(--log-warn-bg)}
|
||||
.console-body .error{color:var(--log-error-ink);background:var(--log-error-bg)}
|
||||
.console-body .critical{color:var(--log-critical-ink);background:var(--log-critical-bg);box-shadow:0 0 6px var(--log-critical-glow)}
|
||||
.console-body .success{color:var(--log-success-ink);background:var(--log-success-bg)}
|
||||
.console-body .failed{color:var(--log-failed-ink);background:var(--log-failed-bg)}
|
||||
.console-body .connected{color:var(--log-connected-ink);background:var(--log-connected-bg)}
|
||||
|
||||
.console-body .loglvl{display:inline-block;padding:2px 8px;border-radius:999px;font-weight:700;font-size:12px;line-height:1;border:1px solid transparent;vertical-align:baseline}
|
||||
.console-body .loglvl.debug{background:linear-gradient(180deg, var(--lvl-debug-top), var(--lvl-debug-bot));color:var(--lvl-debug-ink);border-color:var(--lvl-debug-bdr)}
|
||||
.console-body .loglvl.info{background:linear-gradient(180deg, var(--lvl-info-top), var(--lvl-info-bot));color:var(--lvl-info-ink);border-color:var(--lvl-info-bdr)}
|
||||
.console-body .loglvl.warning{background:linear-gradient(180deg, var(--lvl-warn-top), var(--lvl-warn-bot));color:var(--lvl-warn-ink);border-color:var(--lvl-warn-bdr)}
|
||||
.console-body .loglvl.error{background:linear-gradient(180deg, var(--lvl-error-top), var(--lvl-error-bot));color:var(--lvl-error-ink);border-color:var(--lvl-error-bdr)}
|
||||
.console-body .loglvl.critical{background:linear-gradient(180deg, var(--lvl-crit-top), var(--lvl-crit-bot));color:var(--lvl-crit-ink);border-color:var(--lvl-crit-bdr)}
|
||||
.console-body .loglvl.success{background:linear-gradient(180deg, var(--lvl-succ-top), var(--lvl-succ-bot));color:var(--lvl-succ-ink);border-color:var(--lvl-succ-bdr)}
|
||||
.console-body .loglvl.failed{background:linear-gradient(180deg, var(--lvl-fail-top), var(--lvl-fail-bot));color:var(--lvl-fail-ink);border-color:var(--lvl-fail-bdr)}
|
||||
.console-body .loglvl.connected{background:linear-gradient(180deg, var(--lvl-conn-top), var(--lvl-conn-bot));color:var(--lvl-conn-ink);border-color:var(--lvl-conn-bdr)}
|
||||
.console-body .loglvl.sseclosed{background:linear-gradient(180deg, var(--lvl-sse-top), var(--lvl-sse-bot));color:var(--lvl-sse-ink);border-color:var(--lvl-sse-bdr)}
|
||||
|
||||
/* File-badge uses hue token (--h) injected per-line; still token-driven */
|
||||
.console-body .logfile{display:inline-block;padding:2px 8px;border-radius:999px;background:linear-gradient(180deg, hsla(var(--h), 80%, 25%, .28), hsla(var(--h), 80%, 18%, .38));border:1px solid hsla(var(--h), 95%, 55%, .55);color:hsla(var(--h), 95%, 78%, .95);box-shadow:0 0 0 1px hsla(var(--h), 95%, 55%, .18) inset, 0 8px 22px hsla(var(--h), 95%, 55%, .10);white-space:nowrap}
|
||||
|
||||
/* Attack bar (hidden until .with-attack) */
|
||||
.attackbar{display:wrap;gap:8px;padding:8px 10px;align-items:center;border-bottom:1px dashed var(--c-border-strong);background:var(--overlay-solid);backdrop-filter:blur(4px)}
|
||||
.modal-backdrop#settingsBackdrop{
|
||||
background: var(--backdrop-dim);
|
||||
}
|
||||
|
||||
/* Mode "live" pour l’onglet UI : pas d’assombrissement ni de blur */
|
||||
#settingsBackdrop.live{
|
||||
--backdrop-dim: transparent;
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
}
|
||||
#settingsBackdrop.modal-backdrop {
|
||||
z-index: 90 !important;
|
||||
}
|
||||
|
||||
#settingsBackdrop .modal {
|
||||
z-index: 91 !important;
|
||||
}
|
||||
console.with-attack .attackbar{display:flex}
|
||||
.attackbar select,.attackbar input{height:34px;line-height:34px;background:var(--c-panel);color:var(--ink);border:1px solid var(--c-border-strong);border-radius:10px;padding:0 10px;min-width:120px}
|
||||
.attackbar input{min-width:180px}
|
||||
.attackbar .btn{height:34px;display:inline-flex;align-items:center;justify-content:center;padding:0 12px;border-radius:10px}
|
||||
@media (min-width:1101px){.attackbar{flex-wrap:wrap}}
|
||||
@media (min-width:701px) and (max-width:1100px){.attackbar{flex-wrap:wrap}}
|
||||
@media (max-width:700px){
|
||||
.attackbar{flex-wrap:wrap;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:thin;white-space:nowrap}
|
||||
.attackbar::-webkit-scrollbar{height:6px}
|
||||
.attackbar::-webkit-scrollbar-thumb{background:var(--c-border-strong);border-radius:6px}
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
9) Quick Panel
|
||||
============================== */
|
||||
.grip{position:absolute;left:50%;transform:translateX(-50%);top:6px;width:88px;height:6px;border-radius:99px;background:color-mix(in oklab, var(--acid) 30%, transparent);box-shadow:0 0 12px color-mix(in oklab, var(--acid) 50%, transparent)}
|
||||
/* unified Quickpanel: single definition + state modifiers */
|
||||
.quickpanel{position:fixed;left:0;right:0;top:0;height:var(--qp-h);width:min(720px, 92vw);margin:0 auto;background:var(--grad-quickpanel);border:1px solid var(--c-border-strong);border-top:none;border-radius:0 0 24px 24px;box-shadow:var(--shadow);z-index:65;transform:translateY(calc(-1 * var(--qp-h) - var(--qp-overshoot)));opacity:0;visibility:hidden;pointer-events:none;transition:transform .35s cubic-bezier(.4,0,.2,1), opacity .2s ease, visibility 0s linear .2s}
|
||||
.quickpanel.open{transform:translateY(0);opacity:1;visibility:visible;pointer-events:auto;transition:transform .35s cubic-bezier(.4,0,.2,1), opacity .2s ease, visibility 0s}
|
||||
.quickpanel:not(.open){box-shadow:none !important;border-color:transparent !important;transform:translateY(calc(-1 * var(--qp-h) - var(--qp-overshoot))) !important}
|
||||
@media (max-width:768px){:root{--qp-h:75vh}}
|
||||
|
||||
.qp-header{display:flex;align-items:center;justify-content:space-between}
|
||||
.qp-head-left{display:flex;flex-direction:column;gap:4px}
|
||||
.qp-close{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;cursor:pointer;background:var(--white-06);border:1px solid var(--white-12);color:var(--ink);transition:transform .15s ease, background .15s ease, border-color .15s ease}
|
||||
.qp-close:hover{background:var(--white-10);border-color:var(--white-20)}
|
||||
.qp-close:active{transform:scale(.96)}
|
||||
|
||||
/* Badges — deduped: base + variants */
|
||||
.badge{padding:3px 8px;border-radius:999px;border:1px solid var(--c-border-strong);background:var(--c-chip-bg);color:var(--muted)}
|
||||
.badge.is-accent{background:var(--acid-2);color:var(--ink-invert);font-weight:700;border-color:transparent}
|
||||
.badge.sec-open{border-color:color-mix(in oklab, var(--acid) 55%, transparent)}
|
||||
.badge.sec-wpa{border-color:color-mix(in oklab, var(--acid-2) 60%, transparent)}
|
||||
.badge.sec-wep{border-color:color-mix(in oklab, var(--warning) 60%, transparent)}
|
||||
|
||||
.sig{display:inline-grid;grid-auto-flow:column;gap:2px;align-items:end}
|
||||
.sig i{width:4px;height:6px;display:block;background:var(--c-slot);border:1px solid var(--c-border);border-bottom:none;border-radius:2px 2px 0 0}
|
||||
.sig i.on{background:var(--acid)}
|
||||
|
||||
.btlist .qprow{grid-template-columns:1fr auto}
|
||||
.bt-device{display:flex;align-items:center;gap:10px}
|
||||
.bt-type{color:var(--muted);font-size:12px}
|
||||
|
||||
.state-dot{width:8px;height:8px;border-radius:50%;display:inline-block;background:var(--neutral-44);box-shadow:0 0 10px transparent}
|
||||
.state-on{background:var(--ok);box-shadow:0 0 10px var(--ok)}
|
||||
.state-off{background:var(--muted-off)}
|
||||
.state-err{background:var(--danger);box-shadow:0 0 10px var(--danger)}
|
||||
|
||||
/* ==============================
|
||||
10) Launcher (right rail)
|
||||
============================== */
|
||||
/* Single, authoritative launcher rail (removed conflicting bottom/left rule) */
|
||||
/* .launcher{position:fixed;top:64px;bottom:64px;right:16px;width:96px;border-radius:20px;background:transparent;z-index:70;display:flex;flex-direction:column;padding:0;opacity:0;pointer-events:none;transform:translateX(16px);transition:.2s ease;overflow:visible} */
|
||||
.launcher {
|
||||
position: fixed;
|
||||
top: 64px;
|
||||
bottom: 64px;
|
||||
right: 16px;
|
||||
width: fit-content;
|
||||
border-radius: 16px;
|
||||
background: rgb(5 9 15 / 0%);
|
||||
backdrop-filter: blur(6px);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
box-shadow: 0 20px 60px #00ff9a22;
|
||||
z-index: 70;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(16px);
|
||||
transition: .2s ease;
|
||||
}
|
||||
|
||||
.launcher-scroll {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: none;
|
||||
scroll-behavior: smooth;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.launcher-scroll::-webkit-scrollbar { display: none; }
|
||||
|
||||
.launcher.show {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* --- Boutons du launcher --- */
|
||||
.launcher .lbtn {
|
||||
all: unset;
|
||||
display: flex;
|
||||
flex-direction: column; /* empile icône + titre */
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
width: auto;
|
||||
height: 90px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
transition: .25s;
|
||||
text-align: center;
|
||||
}
|
||||
#actionsBtn { touch-action: manipulation; }
|
||||
|
||||
.launcher .lbtn img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
opacity: .8;
|
||||
transition: .25s;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.launcher .lbtn:hover img {
|
||||
opacity: 1;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* --- Label sous l'image --- */
|
||||
.launcher .lbtn .lbtn-label {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.1;
|
||||
color: var(--c-fg-soft);
|
||||
pointer-events: none;
|
||||
text-shadow: 0 0 2px #000;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.launcher .lbtn:hover .lbtn-label {
|
||||
color: var(--acid);
|
||||
}
|
||||
|
||||
/* Tooltip existant (inchangé) */
|
||||
.launcher .lbtn[data-tooltip]:hover::after,
|
||||
.launcher-scroll .lbtn[data-tooltip]:hover::after {
|
||||
left: 105%;
|
||||
right: auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
/* Hard lock to right */
|
||||
#launcher.launcher {
|
||||
left: auto !important;
|
||||
right: 16px !important;
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: 16px;
|
||||
}
|
||||
|
||||
/* ==============================
|
||||
11) Toasts
|
||||
============================== */
|
||||
.toasts{position:fixed;left:0;right:0;bottom:64px;display:grid;justify-items:center;gap:8px;z-index:80;pointer-events:none}
|
||||
.toast{pointer-events:auto;min-width:220px;max-width:90vw;background:var(--c-panel-2);border:1px solid var(--c-border-strong);border-radius:12px;box-shadow:var(--shadow);padding:10px 12px;animation:rise .28s ease}
|
||||
@keyframes rise{from{transform:translateY(12px);opacity:0}to{transform:translateY(0);opacity:1}}
|
||||
|
||||
/* ==============================
|
||||
12) Modal / Sheet (Wi‑Fi / BT)
|
||||
============================== */
|
||||
.modal-backdrop{position:fixed;inset:0;background:var(--glass-8);display:none;align-items:center;justify-content:center;z-index:60}
|
||||
.modal{width:min(900px, 96vw);max-height:86vh;background:var(--grad-modal);border:1px solid var(--c-border-strong);border-radius:16px;box-shadow:0 40px 120px var(--glow-strong), inset 0 0 0 1px var(--glow-strong);display:grid;grid-template-columns:220px 1fr}
|
||||
.modal.show{animation:pop .2s ease}
|
||||
@keyframes pop{from{transform:scale(.96);opacity:0}to{transform:scale(1);opacity:1}}
|
||||
.tabs{border-right:1px dashed var(--c-border-strong);padding:10px;overflow:auto}
|
||||
.tabbtn{display:block;width:100%;text-align:left;padding:10px 12px;margin:6px 0;border:1px solid var(--c-border);border-radius:10px;background:var(--c-panel);color:var(--ink);cursor:pointer}
|
||||
.tabbtn.active{background:var(--grad-chip-selected);outline:2px solid color-mix(in oklab, var(--acid) 55%, transparent)}
|
||||
.tabpanel{padding:16px;overflow:auto}
|
||||
.row{display:flex;gap:10px;align-items:center;margin:6px 0}
|
||||
/* Inline switch (modal lists) */
|
||||
.switch{position:relative;width:46px;height:26px;background:var(--switch-track);border:1px solid var(--c-border-hi);border-radius:99px;cursor:pointer;box-shadow:inset 0 0 0 1px var(--glow-mid)}
|
||||
.switch::after{content:"";position:absolute;top:2px;left:2px;width:22px;height:22px;background:var(--switch-thumb);border-radius:50%;box-shadow:0 0 10px var(--acid);transform:translateX(0);transition:.18s}
|
||||
.switch.on{background:var(--switch-on-bg)}
|
||||
.switch.on::after{transform:translateX(20px)}
|
||||
|
||||
.sheet-backdrop{position:fixed;inset:0;background:var(--glass-8);display:none;align-items:center;justify-content:center;z-index:75}
|
||||
.sheet{width:min(520px, 94vw);background:var(--grad-modal);border:1px solid var(--c-border-strong);border-radius:14px;box-shadow:0 40px 120px var(--glow-strong);overflow:hidden}
|
||||
.sheet-head{display:flex;align-items:center;gap:10px;padding:12px 14px;border-bottom:1px dashed var(--c-border-strong)}
|
||||
.sheet-body{padding:14px;display:grid;gap:12px}
|
||||
.sheet-foot{display:flex;justify-content:flex-end;gap:8px;padding:12px 14px;border-top:1px dashed var(--c-border)}
|
||||
.field{display:grid;gap:6px}
|
||||
.sheet.show{animation:pop .18s ease}
|
||||
.sheet-backdrop.show{display:flex}
|
||||
|
||||
/* ==============================
|
||||
13) Responsive
|
||||
============================== */
|
||||
@media (max-width:900px){.sidebar{width:240px}.main{left:240px}.modal{grid-template-columns:1fr}.tabs{display:flex;gap:8px;border-right:none;border-bottom:1px dashed var(--c-border-strong)}.tabbtn{flex:1}}
|
||||
@media (max-width:700px){.logo .sig{display:none}.btn .label{display:none}}
|
||||
|
||||
/* ==============================
|
||||
14) Cards / Tiles / Alt buttons (unified)
|
||||
============================== */
|
||||
/* Unified .card: uses gradient surface + themed border */
|
||||
.card{background:var(--grad-card);border:1px solid var(--c-border);border-radius:14px;padding:12px 14px;margin:0 0 12px 0;transition:transform .16s ease, box-shadow .16s ease, border-color .16s ease;box-shadow:var(--shadow)}
|
||||
.card:hover{transform:translateY(-1px);border-color:color-mix(in oklab, var(--accent) 25%, var(--c-border));box-shadow:var(--shadow-hover)}
|
||||
.card .head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
|
||||
.card .title{font-weight:600;color:var(--ink);font-size:14px}
|
||||
.card .meta{color:var(--muted);font-size:12px}
|
||||
|
||||
/* Unified .tile semantics: lighter panel block */
|
||||
.tile{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);padding:16px;box-shadow:var(--shadow)}
|
||||
|
||||
/* Alternative button set (kept for contexts where .btn != desired) */
|
||||
.btn.alt{display:inline-flex;align-items:center;gap:8px;background:var(--btn-bg-solid);color:var(--ink);border:1px solid var(--border);border-radius:12px;padding:8px 12px;transition:background .16s ease, border-color .16s ease, transform .06s ease}
|
||||
.btn.alt:hover{border-color:color-mix(in oklab, var(--accent) 35%, var(--border))}
|
||||
.btn.alt:active{transform:translateY(1px)}
|
||||
.btn.alt.primary{background:linear-gradient(180deg, color-mix(in oklab, var(--accent) 22%, var(--btn-bg-solid)), var(--btn-bg-solid));border-color:color-mix(in oklab, var(--accent) 55%, var(--border))}
|
||||
|
||||
/* ==============================
|
||||
15) Actions Menu (centered dropdown)
|
||||
============================== */
|
||||
.actions{position:relative}
|
||||
#actionsMenu.dropdown{position:absolute;top:calc(100% + 6px) !important;left:50% !important;transform:translateX(-50%);right:auto;min-width:320px;max-width:min(92vw, 920px);--safe-bottom:env(safe-area-inset-bottom, 0px);max-height:calc(100dvh - var(--h-topbar) - var(--h-bottombar) - 16px - var(--safe-bottom)) !important;overflow:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}
|
||||
@media (max-width:700px){#actionsMenu.dropdown{position:fixed !important;top:calc(var(--h-topbar) + 8px) !important;left:50% !important;transform:translateX(-50%);right:auto;min-width:min(92vw, 360px);width:min(92vw, 360px);max-width:92vw;z-index:80;max-height:calc(100dvh - var(--h-topbar) - var(--h-bottombar) - 12px - env(safe-area-inset-bottom, 0px)) !important;overflow:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}}
|
||||
.actions > .btn:focus,.actions > .btn:focus-visible,#actionsMenu .menuitem:focus,#actionsMenu .menuitem:focus-visible{outline:none !important;box-shadow:none !important}
|
||||
#actionsMenu .menuitem:focus-visible{background:var(--c-panel)}
|
||||
@@ -1,590 +0,0 @@
|
||||
/* General styling */
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background-color: #333;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #888 #333;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #333;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 10px;
|
||||
border: 3px solid #333;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* Toolbar styling */
|
||||
|
||||
.toolbar {
|
||||
background-color: #333;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
flex-wrap: wrap;
|
||||
|
||||
}
|
||||
|
||||
.toolbar a, .toolbar button, .toolbar-button {
|
||||
color: rgb(255, 255, 255);
|
||||
text-align: center;
|
||||
padding: 1px 1px;
|
||||
text-decoration: none;
|
||||
margin: 3px;
|
||||
flex: 1 1 auto;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
background-color: #444444;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar a:hover, .toolbar button:hover, .toolbar-button:hover, .toolbar-button:hover button.toolbar-button, .action-button:hover {
|
||||
background-color: #e99f00;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.loot-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding-bottom: 100px;
|
||||
padding-left: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul li {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
ul li a {
|
||||
color: #e99f00;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
ul li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Container styling */
|
||||
.network-container, .netkb-container {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
padding-left: 20px;
|
||||
overflow: auto; /* Add scrollbars if needed */
|
||||
color: white;
|
||||
}
|
||||
|
||||
#netkb-table, #network-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse; /* Collapse borders */
|
||||
|
||||
}
|
||||
|
||||
#action-dropdown, #port-dropdown, #ip-dropdown {
|
||||
color: white;
|
||||
background-color: #444444;
|
||||
border-radius: 15%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.console-toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.console-toolbar button {
|
||||
margin-left: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
#cred-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.credentials-container {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding-bottom: 100px;
|
||||
padding-left: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#credentials-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
color: #ee9025 !important;
|
||||
}
|
||||
|
||||
#credentials-table th {
|
||||
background-color: rgb(99, 99, 99);
|
||||
color: white;
|
||||
}
|
||||
.toobar1-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
background-color: #333;
|
||||
}
|
||||
.config-container {
|
||||
display: flex;
|
||||
flex-direction: column; /* Ensure children are stacked vertically */
|
||||
color: #e0e0e0;
|
||||
background-color: #333;
|
||||
height: calc(100vh - 100px); /* Adjust height considering the toolbar and config buttons */
|
||||
overflow: hidden;
|
||||
padding-left: 10px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
.action-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
color: #e0e0e0;
|
||||
flex: 1 1 auto;
|
||||
background-color: #333;
|
||||
height: calc(100vh - 10px);
|
||||
overflow: hidden;
|
||||
padding-left: 2px;
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.action-panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* Allow items to wrap to the next line */
|
||||
align-items: flex-start;
|
||||
justify-content: center; /* Center align items horizontally */
|
||||
gap: 10px;
|
||||
margin-top: 20px; /* Add some space between text and buttons */
|
||||
}
|
||||
|
||||
.action-button {
|
||||
color: rgb(255, 255, 255);
|
||||
text-align: center;
|
||||
padding: 20px 20px;
|
||||
text-decoration: none;
|
||||
margin: 5px;
|
||||
border-radius: 50px; /* Adjust the value as needed to get the desired roundness */
|
||||
border: none; /* Remove default border */
|
||||
background-color: rgb(19, 109, 0); /* Background color for the buttons */
|
||||
cursor: pointer; /* Add a pointer cursor on hover */
|
||||
}
|
||||
|
||||
.image-container {
|
||||
background-color: #333;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
width: 100%; /* Ensure the container takes full width */
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
/* Image styling */
|
||||
.image-container img {
|
||||
height: 100%; /* Ensure image takes full height */
|
||||
width: auto; /* Ensure aspect ratio is maintained */
|
||||
}
|
||||
|
||||
#screenImage_Home {
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
display: block;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Form styling */
|
||||
.config-form {
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
flex-grow: 1; /* Allow the form to grow and occupy available space */
|
||||
min-width: 275px; /* Set a minimum width */
|
||||
overflow-y: auto;
|
||||
|
||||
padding: 5px;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
align-content: stretch;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
.left-column,
|
||||
.right-column {
|
||||
flex: 1; /* Allow these items to grow and fill available space */
|
||||
padding: 10px; /* Add some padding for better appearance */
|
||||
box-sizing: border-box; /* Include padding in the element's total width and height */
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.label-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgb(255, 255, 255);
|
||||
margin-bottom: 20px; /* Space between switches */
|
||||
}
|
||||
|
||||
.label-switch label {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: rgb(255, 255, 255);
|
||||
margin-bottom: 20px; /* Space below the section title */
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.section-item {
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
color: rgb(255, 255, 255);
|
||||
margin-bottom: 20px; /* Space below each section item */
|
||||
}
|
||||
|
||||
.config-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #333;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Switch styling */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
height: 34px;
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #e99f00;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Wi-Fi panel styling */
|
||||
.wifi-panel {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
right: 20px;
|
||||
background: #333;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#bjorn_home {
|
||||
border-radius: 11% !important;
|
||||
}
|
||||
|
||||
.current-wifi {
|
||||
color: blue !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.wifi-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wifi-panel-header h3 {
|
||||
margin: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.wifi-panel .close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#wifi-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#wifi-list li {
|
||||
background: #e0e0e0;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
#wifi-list li:hover {
|
||||
background-color: #e99f00;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Config buttons styling */
|
||||
.config-buttons {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #333;
|
||||
padding: 10px;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
/* Table and cell styling */
|
||||
table.styled-table {
|
||||
width: 100%;
|
||||
color: rgb(255, 255, 255);
|
||||
border-collapse: collapse;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
table.styled-table th, table.styled-table td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
table.styled-table th {
|
||||
background-color: rgb(10, 9, 9);
|
||||
color: white;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
table.styled-table td.green {
|
||||
color: rgb(0, 255, 0);
|
||||
}
|
||||
|
||||
table.styled-table td.red {
|
||||
background-color: rgb(163, 50, 50);
|
||||
color: white;
|
||||
}
|
||||
|
||||
table.styled-table td.grey {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.blue-row {
|
||||
color: rgb(18, 0, 184);
|
||||
}
|
||||
|
||||
.green {
|
||||
color: rgb(0, 255, 0);
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.scrollable-table {
|
||||
overflow: auto;
|
||||
max-height: 80vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
td:first-child, th:first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background-color: rgb(10, 9, 9);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
/* Dropdown menu styling */
|
||||
#dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #333;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 1;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.dropdown-content button {
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background-color: #333;
|
||||
text-align: left;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-content button:hover {
|
||||
background-color: #e99f00;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown.show .dropdown-content {
|
||||
display: block; /* Afficher le menu déroulant */
|
||||
}
|
||||
|
||||
.action-button img {
|
||||
height: 40px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.action-button span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Additional styles from inline CSS */
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
#log-console {
|
||||
background-color: black;
|
||||
color: white;
|
||||
overflow-y: scroll;
|
||||
font-family: monospace;
|
||||
border: 10px solid #333;
|
||||
font-size: 16px;
|
||||
height: calc(100% - 120px);
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.visible {
|
||||
display: block;
|
||||
height: 50px;
|
||||
background-color: #f4f4f4;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.debug { color: rgb(173, 173, 173); }
|
||||
.info { color: blue; }
|
||||
.warning { color: yellow; }
|
||||
.error { color: red; }
|
||||
.critical { color: magenta; }
|
||||
.success { color: green; }
|
||||
.line-number { color: #888888; } /* Color for line numbers */
|
||||
.number { color: #42ced3; } /* Color for numbers */
|
||||
|
||||
|
||||
.dropdown-content img {
|
||||
display: block;
|
||||
width: 33%; /* 1/3 of the width */
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
770
web/database.html
Normal file
@@ -0,0 +1,770 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Bjorn Cyberviking — DB Manager</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<link rel="icon" href="/web/images/favicon.ico" />
|
||||
<link rel="stylesheet" href="/web/css/global.css" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/web/images/icon-192x192.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#050709" />
|
||||
<style>
|
||||
:root{--topbar-h:var(--h-topbar,56px);--bottombar-h:var(--h-bottombar,56px);--db-row-hover:rgba(0,255,154,.06);--db-row-selected:rgba(0,255,154,.12);--db-cell-edited:rgba(24,240,255,.18);--db-cell-focus:rgba(0,255,154,.22);--sidebar-w:280px}
|
||||
body{padding:0;overflow-x:hidden}
|
||||
.db-header{position:sticky;top:0;z-index:20;background:var(--grad-topbar);border:1px solid var(--c-border);border-radius:12px;padding:12px;box-shadow:var(--shadow);margin-bottom:12px}
|
||||
.sticky-actions{position:sticky;bottom:0;z-index:15;display:flex;gap:8px;justify-content:flex-end;padding:8px;background:linear-gradient(180deg,rgba(0,0,0,0),rgba(0,0,0,.4));border-top:1px solid var(--c-border);border-radius:12px;backdrop-filter:blur(4px)}
|
||||
.db-tree{display:grid;gap:6px}
|
||||
.tree-head{display:flex;gap:8px;align-items:center;margin-bottom:8px}
|
||||
.tree-search{display:flex;gap:6px;align-items:center;background:var(--c-panel);border:1px solid var(--c-border-strong);border-radius:10px;padding:6px 8px}
|
||||
.tree-search input{all:unset;flex:1;color:var(--ink)}
|
||||
.tree-group{margin-top:10px}
|
||||
.tree-item{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--c-border);border-radius:10px;background:var(--c-panel-2);cursor:pointer;transition:.18s}
|
||||
.tree-item:hover{box-shadow:0 0 0 1px var(--c-border-hi) inset,0 8px 22px var(--glow-weak);transform:translateX(2px)}
|
||||
.tree-item.active{background:linear-gradient(180deg,#0b151c,#091219);outline:2px solid color-mix(in oklab,var(--acid) 55%,transparent)}
|
||||
.tree-item .count{margin-left:auto;padding:2px 8px;border-radius:999px;background:var(--c-chip-bg);border:1px solid var(--c-border-hi);font-size:11px;color:var(--muted)}
|
||||
.db-title{display:flex;align-items:center;gap:10px;font-weight:700;color:var(--acid);letter-spacing:.08em}
|
||||
.db-controls{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
|
||||
.db-search{display:flex;align-items:center;gap:8px;background:var(--c-panel);border:1px solid var(--c-border-strong);border-radius:10px;padding:0 10px;min-width:220px;flex:1}
|
||||
.db-search input{all:unset;color:var(--ink);height:34px;flex:1}
|
||||
.db-opts{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.hint{color:var(--muted);font-size:12px}
|
||||
.sep{width:1px;height:24px;background:var(--c-border);margin:0 4px;opacity:.6}
|
||||
.db-container{min-height:100%;display:flex;flex-direction:column}
|
||||
.db-wrap{display:flex;flex-direction:column;gap:12px;min-height:0;flex:1}
|
||||
.db-table-wrap{position:relative;overflow:auto;border:1px solid var(--c-border);border-radius:12px;background:var(--grad-card);box-shadow:var(--shadow);flex:1;min-height:0}
|
||||
table.db{width:100%;border-collapse:separate;border-spacing:0}
|
||||
.db-table-wrap table.db thead th{position:sticky;top:0;z-index:5;background:var(--c-panel);border-bottom:1px solid var(--c-border-strong);text-align:left;padding:10px;font-weight:700;color:var(--acid);user-select:none;cursor:pointer;white-space:nowrap}
|
||||
.db tbody td{padding:8px 10px;border-bottom:1px dashed var(--c-border-muted);vertical-align:middle;background:var(--grad-card)}
|
||||
.db tbody tr:hover{background:var(--db-row-hover)}
|
||||
.db tbody tr.selected{background:var(--db-row-selected);outline:1px solid var(--c-border-hi)}
|
||||
.cell{display:block;min-width:80px;max-width:520px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.cell[contenteditable="true"]{outline:0;border-radius:6px;transition:.12s;padding:2px 6px}
|
||||
.cell[contenteditable="true"]:focus{background:var(--db-cell-focus);box-shadow:0 0 0 1px var(--c-border-hi) inset}
|
||||
.cell.edited{background:var(--db-cell-edited)}
|
||||
.pk{color:var(--muted);font-size:12px}
|
||||
.cols-drawer{display:none}
|
||||
.cols-drawer.open{display:block}
|
||||
.db-page{display:grid;grid-template-columns:1fr}
|
||||
.sidebar{width:auto}
|
||||
.main{position:fixed;left:var(--sidebar-w);right:0;top:var(--topbar-h);bottom:var(--bottombar-h);overflow:auto;padding:16px;transition:.25s}
|
||||
.sidebar.hidden + .main{left:0 !important}@keyframes blinkChange{from{box-shadow:0 0 0 0 var(--acid-22)}to{box-shadow:0 0 0 6px transparent}}
|
||||
.value-changed{animation:blinkChange .66s ease}
|
||||
.sticky-col-cell{position:sticky;z-index:3;background:var(--grad-card);box-shadow:1px 0 0 0 var(--c-border-strong),-1px 0 0 0 var(--c-border)}
|
||||
.sticky-col-head{position:sticky;z-index:3;background:var(--grad-card);box-shadow:1px 0 0 0 var(--c-border-strong),-1px 0 0 0 var(--c-border)}
|
||||
.sticky-check,.sticky-col-head.sticky-check{z-index:4}
|
||||
th.is-sticky .sticky-dot::after{content:"●";margin-left:6px;font-size:10px;color:var(--acid);opacity:.9}
|
||||
th[data-col]{position:relative}
|
||||
th[data-col]::after{content:"⏱️ 1.5s pour fixer";position:absolute;right:8px;top:50%;transform:translateY(-50%);font-size:11px;color:var(--muted);opacity:0;pointer-events:none;transition:opacity .15s ease}
|
||||
@media (hover:hover){th[data-col]:hover::after{opacity:.7}}
|
||||
.main{position:fixed;right:0;top:var(--topbar-h);bottom:var(--bottombar-h);overflow:auto;padding:16px;transition:.25s}
|
||||
.sidebar.hidden + .main{left:0 !important}
|
||||
body:not(:has(#sidebar)) .main{left:0 !important}
|
||||
@media (max-width:1100px){.db-controls{gap:6px}.db-search{min-width:160px}.cell{max-width:60vw}}
|
||||
@media (max-width:1100px){body{padding:0}}
|
||||
@media (max-width:900px){.main{left:240px}}
|
||||
@media (max-width:700px){.main{left:0}}
|
||||
|
||||
</style>
|
||||
<script src="/web/js/global.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar" aria-label="Navigation base de données">
|
||||
<div class="sidehead">
|
||||
<div class="sidetitle">Database</div>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" id="hideSidebar"><span class="icon">⟵</span><span class="label">Hide</span></button>
|
||||
</div>
|
||||
<div class="sidecontent" id="sidecontent">
|
||||
<div class="card">
|
||||
<div class="tree-head">
|
||||
<div class="pill">Tables</div>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" id="refreshTree">Refresh</button>
|
||||
</div>
|
||||
<div class="tree-search">
|
||||
<span aria-hidden="true">🔎</span>
|
||||
<input id="treeFilter" placeholder="Filter tables…" />
|
||||
</div>
|
||||
<div id="dbTree" class="db-tree"></div>
|
||||
<div class="hint" id="treeHint" style="margin-top:8px"></div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="head">
|
||||
<div class="title">Utilities</div>
|
||||
</div>
|
||||
<div class="chips">
|
||||
<button class="chip" id="newTableBtn">➕ New table</button>
|
||||
<button class="chip" id="exportAllBtn">⬇ Export DB</button>
|
||||
<button class="chip is-danger" id="vacuumBtn">🧹 Vacuum</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-btn" style="display:none"></div>
|
||||
<div id="empty-list-hint" style="display:none;opacity:.8;margin-top:8px;font-size:.95em"></div>
|
||||
</aside>
|
||||
|
||||
<!-- Main -->
|
||||
<main class="main" id="main">
|
||||
<div class="db-container">
|
||||
<div class="db-wrap">
|
||||
<!-- Header (collé sous la topbar globale) -->
|
||||
<div class="db-header">
|
||||
<div class="db-title">
|
||||
<img class="sig" src="/web/images/bjornwebicon.png" alt="Bjorn" width="24" height="24" />
|
||||
<span id="titleTable">Select a table</span>
|
||||
<span class="pk" id="titleMeta"></span>
|
||||
</div>
|
||||
<div class="db-controls">
|
||||
<div class="db-search" title="Search in current table">
|
||||
<input id="q" placeholder="Search values, e.g. mac:AA:BB, port>80, text…" />
|
||||
<button class="btn" id="searchBtn">Search</button>
|
||||
</div>
|
||||
<div class="db-opts">
|
||||
<select class="select" id="sortSelect" title="Sort">
|
||||
<option value="">Sort: auto</option>
|
||||
</select>
|
||||
<select class="select" id="limitSelect" title="Rows per page">
|
||||
<option>50</option><option>100</option><option>250</option><option>500</option>
|
||||
</select>
|
||||
<div class="row-toggle" style="padding:6px 10px">
|
||||
<label for="liveToggle" style="margin-right:8px">Live</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="liveToggle" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="input-number">
|
||||
<span>Every</span>
|
||||
<input type="number" id="liveSec" value="5" min="2" max="120" />
|
||||
<span>sec</span>
|
||||
</div>
|
||||
<button class="btn" id="refreshBtn">↻ Refresh</button>
|
||||
<span class="sep" aria-hidden="true"></span>
|
||||
<button class="btn primary" id="saveBtn" disabled>💾 Save edits</button>
|
||||
<button class="btn" id="discardBtn" disabled>⟲ Discard</button>
|
||||
<span class="sep" aria-hidden="true"></span>
|
||||
<button class="btn" id="addRowBtn">➕ Row</button>
|
||||
<button class="btn" id="deleteSelBtn" disabled>🗑 Delete selected</button>
|
||||
<div class="chips">
|
||||
<button class="chip" id="exportCsvBtn">CSV</button>
|
||||
<button class="chip" id="exportJsonBtn">JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="db-table-wrap" id="tableWrap" aria-label="Table data scroller">
|
||||
<table class="db" id="dataTable" aria-live="polite">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sticky-check" style="width:38px; left:0"><input type="checkbox" id="checkAll" aria-label="Select all rows"/></th>
|
||||
<th>—</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbody">
|
||||
<tr><td colspan="99" class="hint" style="padding:14px">Pick a table on the left.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Actions bas (au-dessus de la bottombar) -->
|
||||
<div class="sticky-actions">
|
||||
<div class="hint" id="statusHint">Ready.</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="chips">
|
||||
<button class="chip is-warn" id="truncateBtn" disabled>Danger: Truncate</button>
|
||||
<button class="chip is-danger" id="dropBtn" disabled>Danger: Drop table</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drawer colonnes -->
|
||||
<div class="cols-drawer" id="colsDrawer">
|
||||
<div class="card">
|
||||
<div class="head">
|
||||
<div class="title">Columns</div>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" id="hideCols">Close</button>
|
||||
</div>
|
||||
<div class="chips" id="colsChips"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
const $ = (s, r=document)=> r.querySelector(s);
|
||||
const $$ = (s, r=document)=> [...r.querySelectorAll(s)];
|
||||
const toast = (msg)=> (window.AcidBurn?.toast ? window.AcidBurn.toast(msg) : alert(msg));
|
||||
|
||||
/* ========= API ADAPTER ========= */
|
||||
const API = (function(){
|
||||
const base = '/api/db';
|
||||
const j = (r)=> { if(!r.ok) throw new Error('HTTP '+r.status); return r.json(); };
|
||||
const t = (r)=> { if(!r.ok) throw new Error('HTTP '+r.status); return r.text(); };
|
||||
return {
|
||||
listCatalog: ()=> fetch(`${base}/catalog`).then(j),
|
||||
listTables: ()=> fetch(`${base}/tables`).then(j),
|
||||
getTable: (name, opts={})=>{
|
||||
const p = new URLSearchParams({limit: opts.limit||50, offset: opts.offset||0, q: opts.q||'', sort: opts.sort||''});
|
||||
return fetch(`${base}/table/${encodeURIComponent(name)}?`+p.toString()).then(j);
|
||||
},
|
||||
updateCells: (payload)=> fetch(`${base}/update`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)}).then(j),
|
||||
deleteRows: (payload)=> fetch(`${base}/delete`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)}).then(j),
|
||||
insertRow: (payload)=> fetch(`${base}/insert`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)}).then(j),
|
||||
exportTable: (name, fmt='csv')=> fetch(`${base}/export/${encodeURIComponent(name)}?format=${fmt}`).then(t),
|
||||
exportAll: (fmt='csv')=> fetch(`${base}/export_all?format=${fmt}`),
|
||||
vacuum: ()=> fetch(`${base}/vacuum`, {method:'POST'}).then(j),
|
||||
dropTable: (name)=> fetch(`${base}/drop/${encodeURIComponent(name)}`, {method:'POST'}).then(j),
|
||||
dropView: (name)=> fetch(`${base}/drop_view/${encodeURIComponent(name)}`, {method:'POST'}).then(j),
|
||||
truncateTable: (name)=> fetch(`${base}/truncate/${encodeURIComponent(name)}`, {method:'POST'}).then(j),
|
||||
schema: (name)=> fetch(`${base}/schema/${encodeURIComponent(name)}`).then(j),
|
||||
createTable: (payload)=> fetch(`${base}/create_table`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload)}).then(j),
|
||||
renameTable: (from,to)=> fetch(`${base}/rename_table`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({from,to})}).then(j),
|
||||
addColumn: (table, column)=> fetch(`${base}/add_column`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({table, column})}).then(j),
|
||||
};
|
||||
})();
|
||||
|
||||
/* ========= STATE ========= */
|
||||
const state = {
|
||||
table: null, kind: 'table', // 'table' | 'view'
|
||||
columns: [], pk: null, rows: [],
|
||||
dirty: new Map(),
|
||||
selected: new Set(),
|
||||
timers: { live: null },
|
||||
sort: '', q: '', limit: 50, offset: 0,
|
||||
lastHash: '', hiddenCols: new Set(),
|
||||
stickyCols: new Set(), // ← colonnes “collées”
|
||||
};
|
||||
|
||||
/* ========= SIDEBAR: TABLE + VIEWS TREE ========= */
|
||||
const dbTree = $('#dbTree');
|
||||
const treeHint = $('#treeHint');
|
||||
const treeFilter = $('#treeFilter');
|
||||
|
||||
async function loadTree(){
|
||||
$('#refreshTree')?.setAttribute('disabled','disabled');
|
||||
try{
|
||||
const data = await API.listCatalog();
|
||||
const q = (treeFilter.value||'').toLowerCase().trim();
|
||||
dbTree.innerHTML = '';
|
||||
let shown = 0;
|
||||
|
||||
function section(title, list, icon, kind){
|
||||
if(!list?.length) return 0;
|
||||
const head = document.createElement('div');
|
||||
head.className = 'hint';
|
||||
head.textContent = title;
|
||||
dbTree.appendChild(head);
|
||||
(list||[]).forEach(t=>{
|
||||
if(q && !t.name.toLowerCase().includes(q)) return;
|
||||
shown++;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'tree-item';
|
||||
if(t.name === state.table) item.classList.add('active');
|
||||
item.dataset.name = t.name;
|
||||
item.dataset.kind = kind;
|
||||
item.innerHTML = `<span class="icon">${icon}</span><span>${t.name}</span><span class="count">${t.count ?? '—'}</span>`;
|
||||
item.addEventListener('click', ()=>{
|
||||
// toggle visuel immédiat
|
||||
dbTree.querySelector('.tree-item.active')?.classList.remove('active');
|
||||
item.classList.add('active');
|
||||
// logique
|
||||
selectTable(t.name, t.pk || 'id', kind);
|
||||
});
|
||||
dbTree.appendChild(item);
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
||||
section('Tables', data.tables, '🗂', 'table');
|
||||
section('Views', data.views, '📄', 'view');
|
||||
|
||||
treeHint.textContent = shown ? `${shown} item(s)` : 'No tables/views.';
|
||||
}catch(e){
|
||||
toast('Failed to load catalog');
|
||||
}finally{
|
||||
$('#refreshTree')?.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
/* ========= MAIN: TABLE VIEW ========= */
|
||||
const titleTable = $('#titleTable');
|
||||
const titleMeta = $('#titleMeta');
|
||||
const sortSelect = $('#sortSelect');
|
||||
const limitSelect = $('#limitSelect');
|
||||
const qInput = $('#q');
|
||||
const searchBtn = $('#searchBtn');
|
||||
const tableWrap = $('#tableWrap');
|
||||
const tbody = $('#tbody');
|
||||
const dataTable = $('#dataTable');
|
||||
const saveBtn = $('#saveBtn');
|
||||
const discardBtn = $('#discardBtn');
|
||||
const refreshBtn = $('#refreshBtn');
|
||||
const addRowBtn = $('#addRowBtn');
|
||||
const deleteSelBtn = $('#deleteSelBtn');
|
||||
const exportCsvBtn = $('#exportCsvBtn');
|
||||
const exportJsonBtn = $('#exportJsonBtn');
|
||||
const truncateBtn = $('#truncateBtn');
|
||||
const dropBtn = $('#dropBtn');
|
||||
const statusHint = $('#statusHint');
|
||||
const liveToggle = $('#liveToggle');
|
||||
const liveSec = $('#liveSec');
|
||||
const colsDrawer = $('#colsDrawer');
|
||||
const colsChips = $('#colsChips');
|
||||
const hideCols = $('#hideCols');
|
||||
|
||||
function setStatus(s){ statusHint.textContent = s; }
|
||||
|
||||
function hashRows(rows){
|
||||
try{ return JSON.stringify(rows).slice(0, 1024); }catch{ return String(rows?.length||0) }
|
||||
}
|
||||
|
||||
async function selectTable(name, pkFallback='id', kind='table'){
|
||||
state.table = name;
|
||||
state.kind = kind;
|
||||
state.q = ''; state.sort = ''; state.offset = 0;
|
||||
state.dirty.clear(); state.selected.clear();
|
||||
state.stickyCols.clear();
|
||||
qInput.value = '';
|
||||
titleTable.textContent = name + (kind==='view'?' (view)':'');
|
||||
setButtons();
|
||||
await refresh(true, {pkFallback});
|
||||
}
|
||||
|
||||
function setButtons(){
|
||||
const hasItem = !!state.table;
|
||||
const hasDirty = state.dirty.size > 0;
|
||||
const hasSel = state.selected.size > 0;
|
||||
const readOnly = state.kind === 'view';
|
||||
|
||||
saveBtn.disabled = !hasDirty || readOnly;
|
||||
discardBtn.disabled = !hasDirty;
|
||||
deleteSelBtn.disabled = !hasSel || readOnly;
|
||||
truncateBtn.disabled = !hasItem || readOnly;
|
||||
dropBtn.disabled = !hasItem;
|
||||
addRowBtn.disabled = readOnly;
|
||||
}
|
||||
|
||||
function renderSortOptions(){
|
||||
sortSelect.innerHTML = '<option value="">Sort: auto</option>';
|
||||
state.columns.forEach(c=>{
|
||||
const o1 = document.createElement('option');
|
||||
o1.value = c+':asc'; o1.textContent = `▲ ${c}`;
|
||||
const o2 = document.createElement('option');
|
||||
o2.value = c+':desc'; o2.textContent = `▼ ${c}`;
|
||||
sortSelect.append(o1,o2);
|
||||
});
|
||||
}
|
||||
|
||||
function renderColsChips(){
|
||||
colsChips.innerHTML = '';
|
||||
state.columns.forEach(c=>{
|
||||
const chip = document.createElement('button');
|
||||
chip.className = 'chip'+(state.hiddenCols.has(c)?' is-ghost':'');
|
||||
chip.textContent = state.hiddenCols.has(c) ? `👁️🗨️ ${c}` : `👁️ ${c}`;
|
||||
chip.addEventListener('click', ()=>{
|
||||
if(state.hiddenCols.has(c)) state.hiddenCols.delete(c); else state.hiddenCols.add(c);
|
||||
renderTable();
|
||||
});
|
||||
colsChips.appendChild(chip);
|
||||
});
|
||||
}
|
||||
(function(){
|
||||
const sb=document.getElementById('sidebar');
|
||||
if(!sb) return;
|
||||
const apply=()=>{const w=sb.classList.contains('hidden')?0:Math.round(sb.getBoundingClientRect().width);document.documentElement.style.setProperty('--sidebar-w',w+'px');};
|
||||
const ro=new ResizeObserver(apply);
|
||||
ro.observe(sb);
|
||||
window.addEventListener('resize',apply,{passive:true});
|
||||
new MutationObserver(apply).observe(sb,{attributes:true,attributeFilter:['class']});
|
||||
apply();
|
||||
})();
|
||||
|
||||
function pkOf(row){ return row[state.pk]; }
|
||||
|
||||
/* ===== Long-press + gestion colonnes collées ===== */
|
||||
function onLongPress(el, ms, onFire){
|
||||
let t=null;
|
||||
const clear=()=>{ if(t){ clearTimeout(t); t=null; } };
|
||||
const down=(ev)=>{ t = setTimeout(()=>{ t=null; onFire(ev); }, ms); };
|
||||
const up=()=> clear();
|
||||
const leave=()=> clear();
|
||||
el.addEventListener('mousedown', down);
|
||||
el.addEventListener('touchstart', down, {passive:true});
|
||||
el.addEventListener('mouseup', up);
|
||||
el.addEventListener('mouseleave', leave);
|
||||
el.addEventListener('touchend', up);
|
||||
el.addEventListener('touchcancel', up);
|
||||
}
|
||||
|
||||
function toggleSticky(colName){
|
||||
if(!colName) return;
|
||||
if(state.stickyCols.has(colName)) state.stickyCols.delete(colName);
|
||||
else state.stickyCols.add(colName);
|
||||
renderTable(false);
|
||||
requestAnimationFrame(applyStickyPositions);
|
||||
}
|
||||
|
||||
function applyStickyPositions(){
|
||||
const table = document.getElementById('dataTable');
|
||||
if(!table) return;
|
||||
|
||||
// largeur de la colonne checkbox (left = 0 pour le th)
|
||||
const checkHead = table.querySelector('thead th.sticky-check');
|
||||
let left = (checkHead?.offsetWidth || 38);
|
||||
|
||||
const heads = [...table.querySelectorAll('thead th[data-col]')];
|
||||
|
||||
for(const th of heads){
|
||||
const col = th.dataset.col;
|
||||
const isSticky = th.classList.contains('is-sticky');
|
||||
const cells = [...table.querySelectorAll(`tbody td[data-col="${CSS.escape(col)}"]`)];
|
||||
if(isSticky){
|
||||
th.style.left = left + 'px';
|
||||
th.classList.add('sticky-col-head');
|
||||
cells.forEach(td=>{
|
||||
td.style.left = left + 'px';
|
||||
td.classList.add('sticky-col-cell');
|
||||
});
|
||||
const w = th.offsetWidth || cells[0]?.offsetWidth || 120;
|
||||
left += w;
|
||||
}else{
|
||||
th.style.left = '';
|
||||
th.classList.remove('sticky-col-head');
|
||||
cells.forEach(td=>{
|
||||
td.style.left = '';
|
||||
td.classList.remove('sticky-col-cell');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
window.addEventListener('resize', ()=> requestAnimationFrame(applyStickyPositions));
|
||||
|
||||
function renderTable(diffAnimate=true){
|
||||
// THEAD
|
||||
const thead = dataTable.tHead || dataTable.createTHead();
|
||||
thead.innerHTML = '';
|
||||
const trh = thead.insertRow();
|
||||
|
||||
// Col sélection (checkbox) — sticky à gauche
|
||||
const thSel = document.createElement('th');
|
||||
thSel.style.width='38px';
|
||||
thSel.classList.add('sticky-check');
|
||||
thSel.style.left = '0';
|
||||
thSel.innerHTML = `<input type="checkbox" id="checkAll">`;
|
||||
trh.appendChild(thSel);
|
||||
$('#checkAll', thead)?.addEventListener('change', (e)=>{
|
||||
if(e.target.checked){ state.rows.forEach(r=> state.selected.add(pkOf(r))); }
|
||||
else { state.selected.clear(); }
|
||||
renderTable(false);
|
||||
setButtons();
|
||||
});
|
||||
|
||||
// Colonnes data
|
||||
state.columns.forEach(col=>{
|
||||
if(state.hiddenCols.has(col)) return;
|
||||
const th = document.createElement('th');
|
||||
th.textContent = col + (col===state.pk ? ' (pk)' : '');
|
||||
th.title = 'Click: trier • Long-press: fixer/relâcher';
|
||||
th.dataset.col = col;
|
||||
|
||||
// tri au clic court
|
||||
th.addEventListener('click', ()=>{
|
||||
const cur = state.sort;
|
||||
const asc = `${col}:asc`, desc = `${col}:desc`;
|
||||
state.sort = cur===asc ? desc : asc;
|
||||
sortSelect.value = state.sort;
|
||||
refresh();
|
||||
});
|
||||
|
||||
// long-press (1,5 s) -> toggle sticky
|
||||
onLongPress(th, 1500, ()=> toggleSticky(col));
|
||||
|
||||
// marque visuelle si sticky
|
||||
if(state.stickyCols.has(col)) th.classList.add('is-sticky'); else th.classList.remove('is-sticky');
|
||||
|
||||
// point indicateur
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'sticky-dot';
|
||||
th.appendChild(dot);
|
||||
|
||||
trh.appendChild(th);
|
||||
});
|
||||
|
||||
// TBODY
|
||||
tbody.innerHTML = '';
|
||||
if(!state.rows.length){
|
||||
const tr = document.createElement('tr');
|
||||
const td = document.createElement('td');
|
||||
td.colSpan = 1 + (state.columns.filter(c=>!state.hiddenCols.has(c)).length);
|
||||
td.className = 'hint';
|
||||
td.style.padding = '14px';
|
||||
td.textContent = 'No rows.';
|
||||
tr.appendChild(td);
|
||||
tbody.appendChild(tr);
|
||||
}else{
|
||||
for(const row of state.rows){
|
||||
const tr = document.createElement('tr');
|
||||
const tdSel = document.createElement('td');
|
||||
const pk = pkOf(row);
|
||||
tdSel.classList.add('sticky-check');
|
||||
tdSel.style.left = '0';
|
||||
tdSel.innerHTML = `<input type="checkbox" ${state.selected.has(pk)?'checked':''} data-pk="${String(pk)}">`;
|
||||
tr.appendChild(tdSel);
|
||||
tdSel.querySelector('input').addEventListener('change',(e)=>{
|
||||
if(e.target.checked) state.selected.add(pk);
|
||||
else state.selected.delete(pk);
|
||||
setButtons();
|
||||
});
|
||||
|
||||
for(const col of state.columns){
|
||||
if(state.hiddenCols.has(col)) continue;
|
||||
const td = document.createElement('td');
|
||||
td.dataset.col = col; /* ← nécessaire pour le sticky left */
|
||||
|
||||
const isPK = (col === state.pk);
|
||||
const val = row[col] ?? '';
|
||||
const safe = (val === null || val === undefined) ? '' : String(val);
|
||||
td.innerHTML = `<span class="cell ${isPK?'pk':''}" ${isPK?'':'contenteditable="true"'} data-col="${col}" data-pk="${String(pk)}"></span>`;
|
||||
const span = td.firstElementChild;
|
||||
span.textContent = safe;
|
||||
|
||||
if(!isPK && state.kind !== 'view'){
|
||||
span.addEventListener('input', ()=>{
|
||||
span.classList.add('edited');
|
||||
const changes = state.dirty.get(pk) || {};
|
||||
changes[col] = span.textContent;
|
||||
state.dirty.set(pk, changes);
|
||||
setButtons();
|
||||
});
|
||||
span.addEventListener('keydown', (e)=>{
|
||||
if(e.key==='Enter'){ e.preventDefault(); span.blur(); }
|
||||
});
|
||||
}
|
||||
tr.appendChild(td);
|
||||
}
|
||||
|
||||
if(tr && tr.animate && diffAnimate){ tr.classList.add('value-changed'); setTimeout(()=>tr.classList.remove('value-changed'), 700); }
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
// recalcul des positions “left” des colonnes sticky
|
||||
requestAnimationFrame(applyStickyPositions);
|
||||
}
|
||||
|
||||
async function refresh(initial=false, opts={}){
|
||||
if(!state.table){ return; }
|
||||
setStatus('Loading…');
|
||||
try{
|
||||
const res = await API.getTable(state.table, { limit: state.limit, offset: state.offset, q: state.q, sort: state.sort });
|
||||
state.columns = res.columns || [];
|
||||
state.rows = res.rows || [];
|
||||
state.pk = res.pk || opts.pkFallback || state.pk || 'id';
|
||||
|
||||
if(initial){ renderSortOptions(); renderColsChips(); }
|
||||
|
||||
const curSet = new Set(state.rows.map(r=> pkOf(r)));
|
||||
[...state.selected].forEach(k=>{ if(!curSet.has(k)) state.selected.delete(k); });
|
||||
|
||||
const newHash = hashRows(state.rows);
|
||||
const doAnim = state.lastHash && state.lastHash !== newHash;
|
||||
state.lastHash = newHash;
|
||||
|
||||
titleMeta.textContent = ` • pk: ${state.pk} • ${res.total ?? state.rows.length} rows`;
|
||||
renderTable(doAnim);
|
||||
setButtons();
|
||||
setStatus(`Loaded ${state.rows.length}${res.total ? ' / '+res.total : ''}`);
|
||||
}catch(e){
|
||||
toast('Failed to load data');
|
||||
setStatus('Error.');
|
||||
}
|
||||
}
|
||||
|
||||
/* ========= EDIT / SAVE ========= */
|
||||
async function saveEdits(){
|
||||
if(!state.table || state.dirty.size===0 || state.kind==='view') return;
|
||||
saveBtn.disabled = true;
|
||||
try{
|
||||
const rows = [...state.dirty.entries()].map(([pk, changes])=>({ pk, changes }));
|
||||
await API.updateCells({ table: state.table, pk: state.pk, rows });
|
||||
toast('Saved ✔');
|
||||
state.dirty.clear();
|
||||
setButtons();
|
||||
await refresh();
|
||||
}catch(e){
|
||||
toast('Save failed');
|
||||
}finally{
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
function discardEdits(){
|
||||
state.dirty.clear();
|
||||
$$('[contenteditable].edited', tbody).forEach(el=> el.classList.remove('edited'));
|
||||
setButtons();
|
||||
refresh();
|
||||
}
|
||||
|
||||
/* ========= ROW OPS ========= */
|
||||
async function addRow(){
|
||||
if(!state.table || state.kind==='view') return;
|
||||
const values = {};
|
||||
state.columns.forEach(c=> { if(c!==state.pk) values[c]=''; });
|
||||
try{
|
||||
const res = await API.insertRow({ table: state.table, values });
|
||||
toast('Row added');
|
||||
await refresh();
|
||||
if(res?.pk !== undefined){
|
||||
const el = $(`.cell[data-pk="${String(res.pk)}"][data-col]`, tbody);
|
||||
el?.focus();
|
||||
}
|
||||
}catch(e){ toast('Insert failed'); }
|
||||
}
|
||||
|
||||
async function deleteSelected(){
|
||||
if(!state.table || state.selected.size===0 || state.kind==='view') return;
|
||||
if(!confirm(`Delete ${state.selected.size} selected row(s)?`)) return;
|
||||
try{
|
||||
await API.deleteRows({ table: state.table, pk: state.pk, pks: [...state.selected] });
|
||||
toast('Deleted');
|
||||
state.selected.clear();
|
||||
setButtons();
|
||||
await refresh();
|
||||
}catch(e){ toast('Delete failed'); }
|
||||
}
|
||||
|
||||
/* ========= EXPORT ========= */
|
||||
async function exportTable(fmt){
|
||||
if(!state.table) return;
|
||||
try{
|
||||
const data = await API.exportTable(state.table, fmt);
|
||||
const blob = new Blob([data], {type: fmt==='csv'?'text/csv':'application/json'});
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `${state.table}.${fmt}`;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
toast(`Exported ${fmt.toUpperCase()}`);
|
||||
}catch(e){ toast('Export failed'); }
|
||||
}
|
||||
|
||||
/* ========= DANGEROUS OPS ========= */
|
||||
async function truncateTable(){
|
||||
if(!state.table || state.kind==='view') return;
|
||||
if(!confirm(`TRUNCATE ${state.table}? This removes all data.`)) return;
|
||||
try{ await API.truncateTable(state.table); toast('Table truncated'); await refresh(); }
|
||||
catch(e){ toast('Truncate failed'); }
|
||||
}
|
||||
async function dropCurrent(){
|
||||
if(!state.table) return;
|
||||
const what = state.kind==='view' ? 'VIEW' : 'TABLE';
|
||||
if(!confirm(`DROP ${what} ${state.table}? This removes it.`)) return;
|
||||
try{
|
||||
if(state.kind==='view') await API.dropView(state.table);
|
||||
else await API.dropTable(state.table);
|
||||
toast(`${what} dropped`);
|
||||
state.table = null;
|
||||
titleTable.textContent = 'Select a table';
|
||||
dataTable.tHead.innerHTML = '';
|
||||
tbody.innerHTML = `<tr><td colspan="99" class="hint" style="padding:14px">Pick a table on the left.</td></tr>`;
|
||||
setButtons();
|
||||
await loadTree();
|
||||
}catch(e){ toast('Drop failed'); }
|
||||
}
|
||||
async function vacuum(){ try{ await API.vacuum(); toast('VACUUM done'); }catch(e){ toast('VACUUM failed'); } }
|
||||
|
||||
/* ========= LIVE REFRESH ========= */
|
||||
function startLive(){
|
||||
stopLive();
|
||||
const sec = clamp(parseInt(liveSec.value||'5',10), 2, 120);
|
||||
state.timers.live = setInterval(()=> refresh(), sec*1000);
|
||||
setStatus(`Live: ${sec}s`);
|
||||
}
|
||||
function stopLive(){
|
||||
if(state.timers.live){ clearInterval(state.timers.live); state.timers.live=null; }
|
||||
setStatus('Live: off');
|
||||
}
|
||||
function clamp(n,min,max){ return Math.max(min, Math.min(max,n)); }
|
||||
|
||||
|
||||
/* ========= EVENTS ========= */
|
||||
$('#refreshTree')?.addEventListener('click', loadTree);
|
||||
treeFilter?.addEventListener('input', loadTree);
|
||||
|
||||
qInput?.addEventListener('keydown', e=>{ if(e.key==='Enter') searchBtn.click(); });
|
||||
searchBtn?.addEventListener('click', ()=>{ state.q = qInput.value.trim(); state.offset=0; refresh(); });
|
||||
|
||||
sortSelect?.addEventListener('change', ()=>{ state.sort = sortSelect.value; refresh(); });
|
||||
limitSelect?.addEventListener('change', ()=>{ state.limit = parseInt(limitSelect.value,10)||50; refresh(); });
|
||||
|
||||
refreshBtn?.addEventListener('click', ()=> refresh());
|
||||
saveBtn?.addEventListener('click', saveEdits);
|
||||
discardBtn?.addEventListener('click', discardEdits);
|
||||
addRowBtn?.addEventListener('click', addRow);
|
||||
deleteSelBtn?.addEventListener('click', deleteSelected);
|
||||
exportCsvBtn?.addEventListener('click', ()=> exportTable('csv'));
|
||||
exportJsonBtn?.addEventListener('click', ()=> exportTable('json'));
|
||||
truncateBtn?.addEventListener('click', truncateTable);
|
||||
dropBtn?.addEventListener('click', dropCurrent);
|
||||
|
||||
$('#newTableBtn')?.addEventListener('click', async ()=>{
|
||||
const name = prompt('New table name (letters, digits, underscore):');
|
||||
if(!name) return;
|
||||
try{
|
||||
await API.createTable({
|
||||
name,
|
||||
if_not_exists: true,
|
||||
columns: [{name:'id', type:'INTEGER', pk:true}, {name:'created_at', type:'TEXT'}]
|
||||
});
|
||||
toast('Table created'); await loadTree();
|
||||
}catch{ toast('Create failed'); }
|
||||
});
|
||||
|
||||
$('#exportAllBtn')?.addEventListener('click', async ()=>{
|
||||
try{
|
||||
const res = await API.exportAll('csv');
|
||||
const blob = await res.blob();
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `database_export.zip`;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
toast('Exported all');
|
||||
}catch{ toast('Export all failed'); }
|
||||
});
|
||||
|
||||
$('#vacuumBtn')?.addEventListener('click', vacuum);
|
||||
|
||||
liveToggle?.addEventListener('change', (e)=> e.target.checked ? startLive() : stopLive());
|
||||
liveSec?.addEventListener('change', ()=>{ if(liveToggle.checked) startLive(); });
|
||||
|
||||
// Column drawer toggle with keyboard (Shift+C)
|
||||
document.addEventListener('keydown', (e)=>{
|
||||
if(e.shiftKey && e.key.toLowerCase()==='c'){
|
||||
colsDrawer.classList.toggle('open');
|
||||
if(colsDrawer.classList.contains('open')) renderColsChips();
|
||||
}
|
||||
});
|
||||
hideCols?.addEventListener('click', ()=> colsDrawer.classList.remove('open'));
|
||||
|
||||
// Preserve vertical scroll of table area on refresh
|
||||
let preserveScrollY = 0;
|
||||
tableWrap.addEventListener('scroll', ()=> preserveScrollY = tableWrap.scrollTop);
|
||||
const origRefresh = refresh;
|
||||
refresh = async function(initial=false, opts={}){
|
||||
await origRefresh(initial, opts);
|
||||
tableWrap.scrollTop = preserveScrollY;
|
||||
}
|
||||
|
||||
// Init
|
||||
loadTree();
|
||||
setButtons();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
758
web/files_explorer.html
Normal file
@@ -0,0 +1,758 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<title>Bjorn Cyberviking - Files Explorer</title>
|
||||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon" />
|
||||
<link rel="stylesheet" href="web/css/global.css" />
|
||||
<link rel="stylesheet" href="web/css/all.min.css" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="web/images/icon-192x192.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#333" />
|
||||
<script src="web/js/global.js"></script>
|
||||
|
||||
<!-- Integrated CSS (theming only; no logic changes) -->
|
||||
<style>
|
||||
/* ========= bridge variables to global.css tokens ========= */
|
||||
:root{
|
||||
--_bg: var(--bg, #0b0c0f);
|
||||
--_panel: var(--c-panel-2, rgba(16,22,22,.6));
|
||||
--_border: var(--c-border, rgba(255,255,255,.08));
|
||||
--_ink: var(--ink, #e6fff7);
|
||||
--_muted: var(--muted, #8affc1cc);
|
||||
--_acid: var(--acid, #00ff9a);
|
||||
--_acid2: var(--acid-2, #18f0ff);
|
||||
--_shadow: var(--shadow, 0 10px 26px rgba(0,0,0,.35));
|
||||
}
|
||||
|
||||
body{ background:var(--_bg); color:var(--_ink); }
|
||||
.main{ padding:0 !important; }
|
||||
|
||||
/* ===== Layout ===== */
|
||||
.loot-container{
|
||||
display:flex; flex-direction:column;
|
||||
height:calc(100vh - 120px);
|
||||
padding:12px;
|
||||
gap:12px;
|
||||
}
|
||||
|
||||
.file-explorer{
|
||||
flex:1; display:flex; flex-direction:column; overflow:hidden; padding:10px;
|
||||
color:var(--_ink);
|
||||
background: color-mix(in oklab, var(--_panel) 92%, transparent);
|
||||
border:1px solid var(--_border);
|
||||
border-radius:14px;
|
||||
backdrop-filter: blur(18px);
|
||||
box-shadow: var(--_shadow);
|
||||
}
|
||||
|
||||
/* ===== View containers ===== */
|
||||
.files-grid{
|
||||
overflow-y:auto;
|
||||
display:grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap:8px; padding:8px; border-radius:8px;
|
||||
}
|
||||
.files-list{ overflow-y:auto; padding:4px; }
|
||||
|
||||
/* ===== Upload section ===== */
|
||||
.upload-container{
|
||||
padding:10px; margin-bottom:10px;
|
||||
display:flex; justify-content:center; align-items:center;
|
||||
}
|
||||
.drop-zone{
|
||||
width:100%; max-width:800px;
|
||||
padding:16px;
|
||||
border:2px dashed var(--_border);
|
||||
border-radius:12px; text-align:center; font-size:14px;
|
||||
color: var(--_muted);
|
||||
cursor:pointer; transition:.25s ease;
|
||||
background: color-mix(in oklab, var(--_panel) 88%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.drop-zone:hover{ background: color-mix(in oklab, var(--_panel) 96%, transparent); }
|
||||
.drop-zone.dragover{
|
||||
border-color: color-mix(in oklab, var(--_acid) 50%, var(--_border));
|
||||
background: color-mix(in oklab, var(--_acid) 12%, var(--_panel));
|
||||
color: var(--_ink);
|
||||
}
|
||||
|
||||
/* ===== Items (grid & list) ===== */
|
||||
.grid-item, .list-item{
|
||||
border-radius:10px; padding:8px; cursor:pointer; transition:.15s ease;
|
||||
display:flex; align-items:center; position:relative;
|
||||
border:1px solid transparent;
|
||||
background: color-mix(in oklab, var(--_panel) 86%, transparent);
|
||||
}
|
||||
.grid-item{ flex-direction:column; text-align:center; }
|
||||
.list-item{ flex-direction:row; gap:12px; }
|
||||
|
||||
.grid-item:hover, .list-item:hover{
|
||||
transform: translateY(-2px);
|
||||
border-color: color-mix(in oklab, var(--_acid2) 35%, var(--_border));
|
||||
box-shadow: 0 4px 14px rgba(0,0,0,.25);
|
||||
background: color-mix(in oklab, var(--_panel) 96%, transparent);
|
||||
}
|
||||
|
||||
.grid-item img, .list-item img{ width:28px; height:28px; margin-bottom:4px; }
|
||||
.list-item img{ margin-bottom:0; }
|
||||
|
||||
.item-name{
|
||||
color:var(--_ink); font-size:14px; line-height:1.3;
|
||||
word-break: break-word; pointer-events:none;
|
||||
}
|
||||
.folder .item-name{ color:var(--_ink); font-weight:700; }
|
||||
|
||||
.item-meta{
|
||||
font-size:11px; color:var(--_muted); margin-top:4px; pointer-events:none;
|
||||
}
|
||||
|
||||
/* selected state */
|
||||
.multi-select-mode{ background: color-mix(in oklab, var(--_acid) 6%, transparent); }
|
||||
.item-selected{
|
||||
background: color-mix(in oklab, var(--_acid) 18%, var(--_panel)) !important;
|
||||
border:2px solid color-mix(in oklab, var(--_acid) 55%, var(--_border)) !important;
|
||||
}
|
||||
|
||||
/* ===== Context menu ===== */
|
||||
.context-menu{
|
||||
position:absolute; z-index:1000;
|
||||
background: color-mix(in oklab, var(--_panel) 98%, transparent);
|
||||
border:1px solid var(--_border);
|
||||
border-radius:10px; padding:6px 8px; min-width:160px;
|
||||
color:var(--_ink); box-shadow: var(--_shadow);
|
||||
}
|
||||
.context-menu > div{
|
||||
padding:8px 10px; border-radius:8px; cursor:pointer;
|
||||
}
|
||||
.context-menu > div:hover{
|
||||
background: color-mix(in oklab, var(--_acid2) 12%, transparent);
|
||||
}
|
||||
|
||||
/* ===== Search ===== */
|
||||
.search-container{
|
||||
position:relative; margin-bottom:10px; display:flex; align-items:center;
|
||||
}
|
||||
.search-input{
|
||||
width:100%; padding:10px 40px 10px 12px; font-size:14px;
|
||||
border-radius:10px; border:1px solid var(--_border);
|
||||
background: color-mix(in oklab, var(--_panel) 90%, transparent);
|
||||
color:var(--_ink); box-sizing:border-box; transition:.2s;
|
||||
}
|
||||
.search-input:focus{
|
||||
outline:none;
|
||||
border-color: color-mix(in oklab, var(--_acid2) 35%, var(--_border));
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--_acid2) 18%, transparent);
|
||||
background: color-mix(in oklab, var(--_panel) 96%, transparent);
|
||||
}
|
||||
.search-input::placeholder{ color: color-mix(in oklab, var(--_muted) 70%, transparent); }
|
||||
.clear-button{
|
||||
position:absolute; right:12px; background:none; border:none;
|
||||
color: color-mix(in oklab, var(--_acid) 55%, var(--_ink));
|
||||
font-size:16px; cursor:pointer; display:none;
|
||||
}
|
||||
.clear-button.show{ display:block; }
|
||||
|
||||
/* ===== Toolbar ===== */
|
||||
.toolbar-buttons{ display:flex; gap:8px; margin-bottom:10px; flex-wrap:wrap; }
|
||||
.action-button{
|
||||
background: color-mix(in oklab, var(--_panel) 90%, transparent);
|
||||
border:1px solid var(--_border);
|
||||
color: var(--_muted);
|
||||
padding:8px 10px; border-radius:10px; cursor:pointer; font-size:14px; font-weight:700;
|
||||
display:flex; align-items:center; gap:6px; transition:.2s; backdrop-filter: blur(10px);
|
||||
}
|
||||
.action-button:hover{
|
||||
background: color-mix(in oklab, var(--_panel) 96%, transparent);
|
||||
color: var(--_ink); transform: translateY(-2px);
|
||||
}
|
||||
.action-button.active{
|
||||
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid) 18%, transparent), color-mix(in oklab, var(--_acid2) 10%, transparent));
|
||||
color: var(--_ink);
|
||||
border-color: color-mix(in oklab, var(--_acid2) 28%, var(--_border));
|
||||
}
|
||||
.action-button.delete{
|
||||
background: color-mix(in oklab, var(--_acid) 14%, var(--_panel));
|
||||
color: var(--_ink); display:none; border-color: color-mix(in oklab, var(--_acid) 40%, var(--_border));
|
||||
}
|
||||
.action-button.delete.show{ display:flex; }
|
||||
|
||||
/* ===== Modal ===== */
|
||||
.modal{
|
||||
display:block; position:fixed; inset:0; z-index:1000;
|
||||
background: rgba(0,0,0,.5);
|
||||
}
|
||||
.modal-content{
|
||||
background: color-mix(in oklab, var(--_panel) 98%, transparent);
|
||||
color: var(--_ink);
|
||||
margin: 12vh auto; padding:20px; width: min(500px, 92vw);
|
||||
border:1px solid var(--_border); border-radius:14px; box-shadow: var(--_shadow);
|
||||
}
|
||||
.modal-buttons{ margin-top:18px; text-align:right; display:flex; gap:8px; justify-content:flex-end; }
|
||||
.modal-buttons button{
|
||||
margin-left:0; padding:8px 14px; border-radius:10px; border:1px solid var(--_border);
|
||||
cursor:pointer; background: color-mix(in oklab, var(--_panel) 92%, transparent); color:var(--_ink);
|
||||
}
|
||||
.modal-buttons button:hover{ background: color-mix(in oklab, var(--_panel) 98%, transparent); }
|
||||
.modal-buttons .primary{
|
||||
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid) 18%, transparent), color-mix(in oklab, var(--_acid2) 10%, transparent));
|
||||
border-color: color-mix(in oklab, var(--_acid2) 35%, var(--_border));
|
||||
color: var(--_ink);
|
||||
}
|
||||
|
||||
#folder-tree{
|
||||
border:1px solid var(--_border);
|
||||
border-radius:10px; padding:8px; margin:10px 0; max-height:320px; overflow-y:auto;
|
||||
background: color-mix(in oklab, var(--_panel) 92%, transparent);
|
||||
}
|
||||
.folder-item{
|
||||
padding:8px 10px; cursor:pointer; display:flex; align-items:center; gap:8px; border-radius:8px;
|
||||
}
|
||||
.folder-item:hover{ background: color-mix(in oklab, var(--_panel) 98%, transparent); }
|
||||
.folder-item.selected{
|
||||
background: color-mix(in oklab, var(--_acid2) 16%, transparent);
|
||||
outline: 1px solid color-mix(in oklab, var(--_acid2) 35%, var(--_border));
|
||||
}
|
||||
.folder-item i{ color: var(--_muted); }
|
||||
|
||||
/* ===== Path navigator ===== */
|
||||
.path-navigator{
|
||||
padding:8px; margin-bottom:8px; border-radius:10px;
|
||||
display:flex; align-items:center; gap:8px;
|
||||
background: color-mix(in oklab, var(--_panel) 90%, transparent);
|
||||
border:1px solid var(--_border);
|
||||
}
|
||||
.nav-buttons{ display:flex; gap:8px; }
|
||||
.back-button{
|
||||
background: color-mix(in oklab, var(--_panel) 92%, transparent);
|
||||
border:1px solid var(--_border);
|
||||
color: var(--_muted);
|
||||
padding:8px 12px; border-radius:10px; cursor:pointer; font-weight:700;
|
||||
display:flex; align-items:center; gap:6px; min-width:40px; min-height:40px; justify-content:center;
|
||||
transition:.2s;
|
||||
}
|
||||
.back-button:hover{ background: color-mix(in oklab, var(--_panel) 98%, transparent); color: var(--_ink); }
|
||||
|
||||
.current-path{ display:flex; align-items:center; gap:6px; overflow:hidden; flex-wrap:wrap; }
|
||||
.path-segment{
|
||||
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid) 16%, transparent), color-mix(in oklab, var(--_acid2) 10%, transparent));
|
||||
color: var(--_ink); padding:6px 10px; border-radius:10px; cursor:pointer; transition:.2s;
|
||||
border:1px solid color-mix(in oklab, var(--_acid2) 28%, var(--_border));
|
||||
}
|
||||
.path-segment:hover{ filter: brightness(1.08); }
|
||||
|
||||
/* ===== Responsive ===== */
|
||||
@media (max-width:420px){
|
||||
.loot-container{ height:80vh; }
|
||||
.file-explorer{ max-height:40vh; }
|
||||
.files-grid{ max-height:40vh; }
|
||||
.drop-zone{ padding:18px; font-size:15px; }
|
||||
.toolbar-buttons{ padding:4px; gap:6px; }
|
||||
.search-container, .path-navigator{ padding:4px; }
|
||||
.grid-item{ min-height:74px; font-size:12px; }
|
||||
.item-name{ font-size:13px; margin-top:2px; }
|
||||
.item-meta{ font-size:10px; margin-top:2px; }
|
||||
.grid-item img, .list-item img{ width:28px; height:28px; }
|
||||
}
|
||||
@media (max-width:768px){
|
||||
.files-grid{ grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap:8px; }
|
||||
#file-list{ max-height:fit-content; overflow-y:auto; }
|
||||
.toolbar-buttons{ flex-direction:row; flex-wrap:wrap; gap:8px; }
|
||||
.files-list{ padding:8px; max-height:50vh; overflow-y:auto; }
|
||||
.grid-item{ padding:8px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="main" id="main">
|
||||
<div class="loot-container">
|
||||
<!-- File Explorer Section -->
|
||||
<div class="file-explorer">
|
||||
<div class="toolbar-buttons">
|
||||
<button class="action-button" onclick="toggleView()">
|
||||
<i class="fas fa-th-list"></i>
|
||||
</button>
|
||||
<button class="action-button" id="multiSelectBtn" onclick="toggleMultiSelect()">
|
||||
<i class="fas fa-object-group"></i>Select
|
||||
</button>
|
||||
<button class="action-button" id="newFolderBtn" onclick="createNewFolder()">
|
||||
<i class="fas fa-folder-plus"></i> New Folder
|
||||
</button>
|
||||
<button class="action-button" onclick="renameSelected()" id="renameBtn" style="display: none;">
|
||||
<i class="fas fa-edit"></i> Rename
|
||||
</button>
|
||||
<button class="action-button" onclick="moveSelected()" id="moveBtn" disabled>
|
||||
<i class="fas fa-arrows-alt"></i> Move
|
||||
</button>
|
||||
<button class="action-button delete" id="deleteBtn" onclick="deleteSelectedItems()" disabled>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<input type="text" class="search-input" id="search-input" placeholder="Search files..." oninput="filterFiles()" />
|
||||
<button class="clear-button" id="clear-button" onclick="clearSearch()">✖</button>
|
||||
</div>
|
||||
|
||||
<div class="path-navigator">
|
||||
<div class="nav-buttons">
|
||||
<button class="back-button" onclick="navigateUp()" title="Go to parent directory">
|
||||
← Back
|
||||
</button>
|
||||
</div>
|
||||
<div class="current-path" id="currentPath"></div>
|
||||
</div>
|
||||
|
||||
<div class="files-grid" id="file-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Container Fixed at Bottom -->
|
||||
<div class="upload-container">
|
||||
<input id="file-upload" type="file" multiple style="display: none;" onchange="handle_file_upload(event)" />
|
||||
<div id="drop-zone" class="drop-zone" onclick="document.getElementById('file-upload').click()">
|
||||
Drag files or folders here or click to upload
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Embedded JavaScript (UNCHANGED) -->
|
||||
<script>
|
||||
let currentPath = [];
|
||||
let fontSize = 14;
|
||||
let allFiles = [];
|
||||
let isGridView = true; // Grid view by default
|
||||
|
||||
function formatBytes(bytes, decimals = 1) {
|
||||
if (!bytes) return '0 Bytes';
|
||||
const k = 1024; const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
||||
function toggleView() { isGridView = !isGridView; loadCurrentFolder(); }
|
||||
function navigateUp() { if (currentPath.length > 0) { currentPath.pop(); loadCurrentFolder(); } }
|
||||
function navigateToFolder(folderName) { currentPath.push(folderName); loadCurrentFolder(); }
|
||||
|
||||
function loadAllFiles() {
|
||||
fetch('/list_files')
|
||||
.then(r => r.json())
|
||||
.then(data => { allFiles = data; loadCurrentFolder(); })
|
||||
.catch(err => console.error('Error fetching files:', err));
|
||||
}
|
||||
function loadCurrentFolder() {
|
||||
const currentContent = findFolderContents(allFiles, currentPath);
|
||||
displayFiles(currentContent);
|
||||
updateCurrentPathDisplay();
|
||||
const v = document.getElementById('search-input').value.toLowerCase();
|
||||
if (v) { filterFiles(); }
|
||||
}
|
||||
function findFolderContents(data, path) {
|
||||
if (path.length === 0) return data;
|
||||
let current = data;
|
||||
for (let folder of path) {
|
||||
const found = current.find(item => item.is_directory && item.name === folder);
|
||||
if (!found || !found.children) return [];
|
||||
current = found.children;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
function displayFiles(currentContent) {
|
||||
const container = document.getElementById('file-list');
|
||||
container.innerHTML = '';
|
||||
container.className = isGridView ? 'files-grid' : 'files-list';
|
||||
|
||||
const sorted = [...currentContent].sort((a,b)=>{
|
||||
if (a.is_directory && !b.is_directory) return -1;
|
||||
if (!a.is_directory && b.is_directory) return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
sorted.forEach(item => {
|
||||
const el = document.createElement('div');
|
||||
el.className = (isGridView ? 'grid-item ' : 'list-item ') + (item.is_directory ? 'folder' : 'file');
|
||||
|
||||
item.path = buildCompletePath(item.name);
|
||||
el.dataset.path = item.path;
|
||||
if (selectedItems.has(item.path)) el.classList.add('item-selected');
|
||||
|
||||
el.innerHTML = `
|
||||
<img src="/web/images/${item.is_directory ? 'mainfolder' : 'file'}.png" alt="${item.is_directory ? 'Folder' : 'File'}">
|
||||
<div>
|
||||
<div class="item-name">${item.name}</div>
|
||||
<div class="item-meta">${item.is_directory ? 'Folder' : formatBytes(item.size || 0)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.addEventListener('click', (e)=>{
|
||||
e.preventDefault();
|
||||
if (isMultiSelectMode) {
|
||||
toggleItemSelection(el, item);
|
||||
} else {
|
||||
if (item.is_directory) navigateToFolder(item.name);
|
||||
else window.location.href = `/download_file?path=${encodeURIComponent(item.path)}`;
|
||||
}
|
||||
});
|
||||
|
||||
el.addEventListener('contextmenu', (e)=>{ e.preventDefault(); showContextMenu(e, item); });
|
||||
container.appendChild(el);
|
||||
});
|
||||
|
||||
updateSelectionCount();
|
||||
updateButtonStates();
|
||||
}
|
||||
function navigateToRoot(){ currentPath = []; loadCurrentFolder(); }
|
||||
|
||||
function showMultiContextMenu(event){
|
||||
const existing = document.querySelector('.context-menu'); if (existing) existing.remove();
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'context-menu';
|
||||
menu.style.top = `${event.clientY}px`; menu.style.left = `${event.clientX}px`;
|
||||
const del = document.createElement('div');
|
||||
del.textContent = `Delete (${selectedItems.size} items)`;
|
||||
del.onclick = ()=>{ deleteMultipleItems(Array.from(selectedItems)); menu.remove(); };
|
||||
menu.appendChild(del); document.body.appendChild(menu);
|
||||
document.addEventListener('click', ()=>{ if (menu.parentElement) menu.remove(); }, { once:true });
|
||||
}
|
||||
|
||||
async function deleteSelectedItems(){
|
||||
if (selectedItems.size === 0) return;
|
||||
if (confirm(`Are you sure you want to delete ${selectedItems.size} items?`)){
|
||||
const items = Array.from(selectedItems.keys());
|
||||
for (const path of items) {
|
||||
try{
|
||||
const r = await fetch('/delete_file', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ file_path: path })
|
||||
});
|
||||
if (!r.ok) console.error(`Failed to delete ${path}`);
|
||||
} catch(err){ console.error(`Error deleting ${path}:`, err); }
|
||||
}
|
||||
await loadAllFiles(); clearSelection(); toggleMultiSelect();
|
||||
}
|
||||
}
|
||||
|
||||
function updateCurrentPathDisplay(){
|
||||
const wrap = document.getElementById('currentPath'); wrap.innerHTML='';
|
||||
const root = document.createElement('span'); root.className='path-segment'; root.textContent='/'; root.onclick = ()=>navigateToRoot(); wrap.appendChild(root);
|
||||
currentPath.forEach((folder, idx)=>{
|
||||
const seg = document.createElement('span'); seg.className='path-segment'; seg.textContent=folder;
|
||||
seg.onclick = ()=>{ currentPath = currentPath.slice(0, idx+1); loadCurrentFolder(); };
|
||||
wrap.appendChild(seg);
|
||||
});
|
||||
}
|
||||
|
||||
function buildCompletePath(fileName){
|
||||
const basePath = '/home/bjorn/';
|
||||
return basePath + currentPath.join('/') + '/' + fileName;
|
||||
}
|
||||
|
||||
function deleteFileOrFolder(item){
|
||||
if (!item || !item.path){ console.error('Invalid item or missing path:', item); alert('Error: Invalid file information'); return; }
|
||||
if (confirm(`Are you sure you want to delete "${item.name}"?`)){
|
||||
fetch('/delete_file', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ file_path: item.path })
|
||||
})
|
||||
.then(r=>{ if (!r.ok) return r.json().then(d=>{ throw new Error(d.message || 'Failed to delete the item.');}); return r.json(); })
|
||||
.then(d=>{ if (d.status==='success') loadAllFiles(); else throw new Error(d.message || 'Unknown error occurred'); })
|
||||
.catch(err=>{ console.error('Error in delete operation:', err); alert(`An error occurred: ${err.message}`); });
|
||||
}
|
||||
}
|
||||
|
||||
function showContextMenu(event, item){
|
||||
event.preventDefault();
|
||||
const existing = document.querySelector('.context-menu'); if (existing) existing.remove();
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'context-menu';
|
||||
menu.style.top = `${event.clientY}px`; menu.style.left = `${event.clientX}px`;
|
||||
|
||||
const rename = document.createElement('div'); rename.textContent='Rename';
|
||||
rename.onclick = ()=>{ renameItem(item); menu.remove(); };
|
||||
const dup = document.createElement('div'); dup.textContent='Duplicate';
|
||||
dup.onclick = ()=>{ duplicateItem(item); menu.remove(); };
|
||||
const move = document.createElement('div'); move.textContent='Move to...';
|
||||
move.onclick = ()=>{ showMoveToDialog(item); menu.remove(); };
|
||||
const del = document.createElement('div'); del.textContent='Delete';
|
||||
del.onclick = ()=>{ deleteFileOrFolder({ name:item.name, path:buildCompletePath(item.name), is_directory:item.is_directory }); menu.remove(); };
|
||||
|
||||
[rename, dup, move, del].forEach(x=>menu.appendChild(x));
|
||||
document.body.appendChild(menu);
|
||||
document.addEventListener('click', ()=>{ if (menu.parentElement) menu.remove(); }, { once:true });
|
||||
}
|
||||
|
||||
function filterFiles(){
|
||||
const v = document.getElementById('search-input').value.toLowerCase();
|
||||
const clear = document.getElementById('clear-button');
|
||||
if (v.length > 0){
|
||||
clear.classList.add('show');
|
||||
displayFiles( filterAllFiles(allFiles, v) );
|
||||
} else {
|
||||
clear.classList.remove('show');
|
||||
loadCurrentFolder();
|
||||
}
|
||||
}
|
||||
function filterAllFiles(files, v){
|
||||
let res = [];
|
||||
files.forEach(it=>{
|
||||
if (it.name.toLowerCase().includes(v)) res.push(it);
|
||||
if (it.is_directory && it.children) res = res.concat(filterAllFiles(it.children, v));
|
||||
});
|
||||
return res;
|
||||
}
|
||||
function clearSearch(){ const i = document.getElementById('search-input'); i.value=''; filterFiles(); }
|
||||
|
||||
document.addEventListener('DOMContentLoaded', ()=>{
|
||||
const fi = document.getElementById('file-upload');
|
||||
fi.removeAttribute('webkitdirectory'); fi.removeAttribute('directory'); fi.removeAttribute('mozdirectory'); fi.setAttribute('multiple','');
|
||||
loadAllFiles();
|
||||
if (/Mobi|Android/i.test(navigator.userAgent)){ fontSize = 12; adjustFontSize(0); }
|
||||
const filesGrid = document.querySelector('.files-grid'); if (filesGrid){ filesGrid.addEventListener('contextmenu', showEmptySpaceContextMenu); }
|
||||
});
|
||||
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
dropZone.addEventListener('dragover', (e)=>{ e.preventDefault(); dropZone.classList.add('dragover'); });
|
||||
dropZone.addEventListener('dragleave', ()=>{ dropZone.classList.remove('dragover'); });
|
||||
dropZone.addEventListener('drop', (e)=>{
|
||||
e.preventDefault(); dropZone.classList.remove('dragover');
|
||||
const items = e.dataTransfer.items;
|
||||
if (items){ handleDirectoryUpload(items); } else { handleFiles(e.dataTransfer.files); }
|
||||
});
|
||||
|
||||
function handle_file_upload(e){ handleFiles(e.target.files); }
|
||||
function handleFiles(files){
|
||||
const formData = new FormData();
|
||||
Array.from(files).forEach(file=>{
|
||||
const rel = file.webkitRelativePath || file.name;
|
||||
formData.append('files[]', file, rel);
|
||||
});
|
||||
formData.append('currentPath', JSON.stringify(currentPath));
|
||||
fetch('/upload_files', { method:'POST', body: formData })
|
||||
.then(r=>r.json())
|
||||
.then(()=>{ loadAllFiles(); })
|
||||
.catch(err=>{ alert('Error uploading files: ' + err.message); });
|
||||
}
|
||||
|
||||
let isMultiSelectMode = false;
|
||||
const selectedItems = new Map();
|
||||
function toggleMultiSelect(){
|
||||
isMultiSelectMode = !isMultiSelectMode;
|
||||
const container = document.querySelector('.file-explorer');
|
||||
const btn = document.querySelector('#multiSelectBtn');
|
||||
if (isMultiSelectMode){ container.classList.add('multi-select-mode'); btn.classList.add('active'); }
|
||||
else { container.classList.remove('multi-select-mode'); btn.classList.remove('active'); clearSelection(); }
|
||||
updateButtonStates();
|
||||
}
|
||||
function updateSelectionCount(){
|
||||
const del = document.getElementById('deleteBtn');
|
||||
const n = selectedItems.size;
|
||||
del.innerHTML = n>0 ? `<i class="fas fa-trash"></i> ${n}` : `<i class="fas fa-trash"></i>`;
|
||||
}
|
||||
function clearSelection(){
|
||||
selectedItems.clear();
|
||||
document.querySelectorAll('.grid-item, .list-item').forEach(i=>i.classList.remove('item-selected'));
|
||||
updateButtonStates();
|
||||
}
|
||||
function toggleItemSelection(el, item){
|
||||
if (!isMultiSelectMode) return;
|
||||
const p = item.path;
|
||||
if (selectedItems.has(p)){ selectedItems.delete(p); el.classList.remove('item-selected'); }
|
||||
else { selectedItems.set(p, item); el.classList.add('item-selected'); }
|
||||
updateButtonStates();
|
||||
}
|
||||
|
||||
async function handleDirectoryUpload(items){
|
||||
const files = []; const entries = Array.from(items).map(i=>i.webkitGetAsEntry());
|
||||
async function traverseEntry(entry, path=''){
|
||||
if (entry.isFile){
|
||||
const file = await new Promise(res=>entry.file(res));
|
||||
Object.defineProperty(file, 'webkitRelativePath', { value: path + entry.name });
|
||||
files.push(file);
|
||||
} else if (entry.isDirectory){
|
||||
const reader = entry.createReader();
|
||||
const kids = await new Promise(res=>{
|
||||
const acc=[]; (function read(){ reader.readEntries((ents)=>{ if(ents.length){ acc.push(...ents); read(); } else { res(acc); } }); })();
|
||||
});
|
||||
const next = path + entry.name + '/';
|
||||
for (const k of kids){ await traverseEntry(k, next); }
|
||||
}
|
||||
}
|
||||
await Promise.all(entries.map(e=>traverseEntry(e)));
|
||||
handleFiles(files);
|
||||
}
|
||||
|
||||
function showEmptySpaceContextMenu(event){
|
||||
if (event.target.classList.contains('files-grid')){
|
||||
event.preventDefault();
|
||||
const existing = document.querySelector('.context-menu'); if (existing) existing.remove();
|
||||
const menu = document.createElement('div'); menu.className='context-menu';
|
||||
menu.style.top = `${event.clientY}px`; menu.style.left = `${event.clientX}px`;
|
||||
const nf = document.createElement('div'); nf.textContent='New Folder'; nf.onclick = ()=>{ createNewFolder(); menu.remove(); };
|
||||
menu.appendChild(nf); document.body.appendChild(menu);
|
||||
document.addEventListener('click', ()=>{ if (menu.parentElement) menu.remove(); }, { once:true });
|
||||
}
|
||||
}
|
||||
|
||||
async function renameItem(item){
|
||||
const newName = prompt(`Rename ${item.is_directory ? 'folder' : 'file'} "${item.name}" to:`, item.name);
|
||||
if (newName && newName !== item.name){
|
||||
const oldPath = buildCompletePath(item.name);
|
||||
const newPath = buildCompletePath(newName);
|
||||
try{
|
||||
const r = await fetch('/rename_file', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ old_path: oldPath, new_path: newPath })
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
loadAllFiles();
|
||||
} catch(err){ alert(`Error renaming item: ${err.message}`); }
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateItem(item){
|
||||
const base = item.name;
|
||||
const ext = item.is_directory ? '' : (base.includes('.') ? '.' + base.split('.').pop() : '');
|
||||
const nameNoExt = item.is_directory ? base : base.split('.')[0];
|
||||
const newName = `${nameNoExt} (copy)${ext}`;
|
||||
try{
|
||||
const sourcePath = buildCompletePath(item.name);
|
||||
const targetPath = buildCompletePath(newName);
|
||||
const r = await fetch('/duplicate_file', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ source_path: sourcePath, target_path: targetPath })
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
loadAllFiles();
|
||||
} catch(err){ alert(`Error duplicating item: ${err.message}`); }
|
||||
}
|
||||
|
||||
async function createNewFolder(){
|
||||
const folderName = prompt('Enter new folder name:', 'New Folder');
|
||||
if (folderName){
|
||||
try{
|
||||
const folderPath = buildCompletePath(folderName);
|
||||
const r = await fetch('/create_folder', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ folder_path: folderPath })
|
||||
});
|
||||
if (!r.ok) throw new Error(await r.text());
|
||||
loadAllFiles();
|
||||
} catch(err){ alert(`Error creating folder: ${err.message}`); }
|
||||
}
|
||||
}
|
||||
|
||||
async function showMoveToDialog(items){
|
||||
const itemsArr = Array.isArray(items) ? items : [items];
|
||||
const modal = document.createElement('div');
|
||||
modal.className='modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<h2>Move ${itemsArr.length} ${itemsArr.length>1 ? 'items' : 'item'} to...</h2>
|
||||
<div id="folder-tree"></div>
|
||||
<div class="modal-buttons">
|
||||
<button class="action-button" id="cancelButton"><i class="fas fa-times"></i> Cancel</button>
|
||||
<button class="action-button primary" id="moveConfirmButton"><i class="fas fa-check"></i> Move</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
document.getElementById('cancelButton').addEventListener('click', closeModal);
|
||||
document.getElementById('moveConfirmButton').addEventListener('click', ()=>{ processMove(itemsArr); });
|
||||
modal.addEventListener('click', (e)=>{ if (e.target === modal) closeModal(); });
|
||||
await loadFolderTree();
|
||||
}
|
||||
|
||||
async function loadFolderTree(){
|
||||
try{
|
||||
const r = await fetch('/list_directories'); if (!r.ok) throw new Error('Failed to load directory structure');
|
||||
const dirs = await r.json();
|
||||
const tree = document.getElementById('folder-tree');
|
||||
tree.innerHTML = buildFolderTreeHTML(dirs);
|
||||
addFolderTreeListeners();
|
||||
} catch(err){ alert('Error loading folder structure: ' + err.message); }
|
||||
}
|
||||
|
||||
function buildFolderTreeHTML(directories, level=0){
|
||||
let html=''; const pad = level * 20;
|
||||
directories.forEach(dir=>{
|
||||
if (dir.is_directory){
|
||||
html += `<div class="folder-item" data-path="${dir.path}" style="padding-left:${pad}px"><i class="fas fa-folder"></i><span>${dir.name}</span></div>`;
|
||||
if (dir.children && dir.children.length>0) html += buildFolderTreeHTML(dir.children, level+1);
|
||||
}
|
||||
});
|
||||
return html;
|
||||
}
|
||||
|
||||
let selectedTargetPath = null;
|
||||
function addFolderTreeListeners(){
|
||||
document.querySelectorAll('.folder-item').forEach(it=>{
|
||||
it.addEventListener('click', (e)=>{
|
||||
e.stopPropagation();
|
||||
document.querySelectorAll('.folder-item.selected').forEach(x=>x.classList.remove('selected'));
|
||||
it.classList.add('selected'); selectedTargetPath = it.dataset.path;
|
||||
});
|
||||
});
|
||||
}
|
||||
function closeModal(){ const m = document.querySelector('.modal'); if (m) m.remove(); selectedTargetPath = null; }
|
||||
|
||||
async function processMove(items){
|
||||
if (!selectedTargetPath){ alert('Please select a destination folder'); return; }
|
||||
const errors = []; const arr = Array.isArray(items)?items:[items];
|
||||
for (const item of arr){
|
||||
try{
|
||||
const r = await fetch('/move_file', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ source_path:item.path, target_path:`${selectedTargetPath}/${item.name}` })
|
||||
});
|
||||
if (!r.ok){ const e = await r.text(); errors.push(`Failed to move ${item.name}: ${e}`); }
|
||||
} catch(err){ errors.push(`Error moving ${item.name}: ${err.message}`); }
|
||||
}
|
||||
if (errors.length>0) alert('Some errors occurred:\n' + errors.join('\n'));
|
||||
closeModal(); loadAllFiles();
|
||||
}
|
||||
|
||||
function renameSelected(){
|
||||
const el = document.querySelector('.item-selected'); if (!el){ alert('Please select an item to rename'); return; }
|
||||
const name = el.querySelector('.item-name').textContent; const path = el.dataset.path; const isDir = el.classList.contains('folder');
|
||||
renameItem({ name, path, is_directory:isDir });
|
||||
}
|
||||
|
||||
function moveSelected(){
|
||||
const els = document.querySelectorAll('.item-selected');
|
||||
if (els.length===0){ alert('Please select items to move'); return; }
|
||||
const items = Array.from(els).map(el=>({ name: el.querySelector('.item-name').textContent, path: el.dataset.path, is_directory: el.classList.contains('folder') }));
|
||||
showMoveToDialog(items);
|
||||
}
|
||||
|
||||
function updateButtonStates(){
|
||||
const n = selectedItems.size;
|
||||
const renameBtn = document.getElementById('renameBtn');
|
||||
const moveBtn = document.getElementById('moveBtn');
|
||||
const deleteBtn = document.getElementById('deleteBtn');
|
||||
const newFolderBtn = document.getElementById('newFolderBtn');
|
||||
|
||||
if (renameBtn){
|
||||
if (isMultiSelectMode && n===1){ renameBtn.style.display='inline-block'; renameBtn.disabled=false; }
|
||||
else { renameBtn.style.display='none'; renameBtn.disabled=true; }
|
||||
}
|
||||
if (moveBtn){
|
||||
if (isMultiSelectMode && n>0){ moveBtn.style.display='inline-block'; moveBtn.disabled=false; }
|
||||
else { moveBtn.style.display='none'; moveBtn.disabled=true; }
|
||||
}
|
||||
if (deleteBtn){
|
||||
deleteBtn.style.display = isMultiSelectMode ? 'inline-block' : 'none';
|
||||
deleteBtn.disabled = n===0;
|
||||
deleteBtn.innerHTML = n>0 ? `<i class="fas fa-trash"></i> ${n}` : `<i class="fas fa-trash"></i>`;
|
||||
}
|
||||
if (newFolderBtn){ newFolderBtn.style.display = isMultiSelectMode ? 'none' : 'inline-block'; }
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
web/images/actions_launcher.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
web/images/actions_menu.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
web/images/actions_studio.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 34 KiB |
BIN
web/images/attack.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
web/images/attacks.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
web/images/backup_update.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
web/images/backuprestore.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
web/images/bjornwebicon.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
web/images/bluetooth.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
web/images/boat.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB |
BIN
web/images/credentials.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
web/images/database.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
web/images/default_character_icon.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 2.7 KiB |
BIN
web/images/files_explorer.png
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
web/images/filter_icon.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 24 KiB |
BIN
web/images/import_potfiles.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
web/images/index.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
BIN
web/images/lighthouse.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
web/images/lighthouse_bg.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
web/images/loot.png
Normal file
|
After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 35 KiB |
BIN
web/images/manual_scanning.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
web/images/map_viking.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
web/images/menu_icon.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
web/images/netkb.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 20 KiB |
BIN
web/images/network.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 14 KiB |
BIN
web/images/notalive.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 56 KiB |
BIN
web/images/scanner.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
web/images/scheduler.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
web/images/script_icons/HeimdallGuard.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
web/images/script_icons/arp_spoofer.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
web/images/script_icons/berserker_force.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
web/images/script_icons/default.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
web/images/script_icons/dns_pillager.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
web/images/script_icons/freya_harvest.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
web/images/script_icons/loki_deceiver.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
web/images/script_icons/odin_eye.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
web/images/script_icons/rune_cracker.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
web/images/script_icons/thor_hammer.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
web/images/script_icons/valkyrie_scout.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
web/images/script_icons/wpasec_potfiles.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
web/images/script_icons/yggdrasil_mapper.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
web/images/settings.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
web/images/static_icon.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
web/images/status_image.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 3.7 KiB |
BIN
web/images/switchmode.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
web/images/table_mode.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
web/images/target.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
web/images/target2.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
web/images/treasure.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
web/images/update.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
web/images/vulnerabilities.png
Normal file
|
After Width: | Height: | Size: 159 KiB |
BIN
web/images/web_enum.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 38 KiB |
BIN
web/images/wifi_priority.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
web/images/zombieland.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
1044
web/index.html
2
web/js/d3.v7.min.js
vendored
Normal file
5385
web/js/global.js
Normal file
324
web/login.html
Normal file
@@ -0,0 +1,324 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Login - Bjorn</title>
|
||||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="web/css/global.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="web/images/icon-192x192.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ff0000">
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#ff0000">
|
||||
<style>
|
||||
/* Importation de la police personnalisée */
|
||||
@font-face {
|
||||
font-family: 'Viking';
|
||||
src: url('/web/css/fonts/Viking.TTF') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
body, html {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
font-family: 'Arial', sans-serif;
|
||||
background: #0a0a0a;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rgb-border {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(45deg, #ff0000, #00ff00, #0000ff, #ff0000);
|
||||
background-size: 400% 400%;
|
||||
animation: rgb 10s ease infinite;
|
||||
filter: blur(20px);
|
||||
opacity: 0.5;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@keyframes rgb {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: rgba(10, 10, 10, 0.8);
|
||||
padding: 40px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
text-align: center;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.login-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: conic-gradient(
|
||||
transparent,
|
||||
transparent,
|
||||
transparent,
|
||||
var(--rgb-color)
|
||||
);
|
||||
animation: rotate 4s linear infinite;
|
||||
opacity: 0.1;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #fff;
|
||||
margin-bottom: 20px;
|
||||
font-size: 2em;
|
||||
text-shadow: 0 0 10px rgba(var(--rgb-values), 0.5);
|
||||
font-family: 'Viking', sans-serif; /* Application de la police Viking */
|
||||
}
|
||||
|
||||
.input-group {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s;
|
||||
box-sizing: border-box;
|
||||
min-height: 44px;
|
||||
-webkit-tap-highlight-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 15px rgba(var(--rgb-values), 0.3);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
user-select: none;
|
||||
z-index: 3;
|
||||
padding: 15px;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(45deg, #ff0000, #00ff00, #0000ff);
|
||||
background-size: 200% 200%;
|
||||
animation: rgb 10s ease infinite;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
min-height: 44px;
|
||||
font-family: 'Viking', sans-serif; /* Application de la police Viking */
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow: 0 0 20px rgba(var(--rgb-values), 0.4);
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.auth-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
margin: 20px 0;
|
||||
gap: 10px;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Styles pour le toggle switch */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
background: #ccc;
|
||||
border-radius: 24px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
transition: background 0.4s;
|
||||
}
|
||||
|
||||
.slider::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background: var(--rgb-color);
|
||||
}
|
||||
|
||||
input:checked + .slider::before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
|
||||
/* Optional: Adding a hue animation to the slider */
|
||||
@keyframes hue {
|
||||
from { background: red; }
|
||||
to { background: red; }
|
||||
}
|
||||
|
||||
:root {
|
||||
--rgb-color: #ff0000;
|
||||
--rgb-values: 255, 0, 0;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="rgb-border"></div>
|
||||
<div class="login-container">
|
||||
<h2>Bjorn Login</h2>
|
||||
<form method="POST" action="/login">
|
||||
<div class="input-group">
|
||||
<input type="text" name="username" placeholder="Username" required>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="password" name="password" id="password" placeholder="Password" required>
|
||||
<span class="password-toggle" onclick="togglePassword()">👁️</span>
|
||||
</div>
|
||||
<div class="auth-options">
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="alwaysAuth" name="alwaysAuth">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span>Always require authentication</span>
|
||||
</div>
|
||||
<button type="submit" class="login-button">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Password visibility toggle
|
||||
function togglePassword() {
|
||||
const password = document.getElementById('password');
|
||||
password.type = password.type === 'password' ? 'text' : 'password';
|
||||
}
|
||||
|
||||
// Dynamic RGB color and theme-color update
|
||||
function updateRGBColor() {
|
||||
const r = Math.sin(Date.now() * 0.001) * 127 + 128;
|
||||
const g = Math.sin(Date.now() * 0.001 + 2) * 127 + 128;
|
||||
const b = Math.sin(Date.now() * 0.001 + 4) * 127 + 128;
|
||||
|
||||
const rgbColor = `rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)})`;
|
||||
document.documentElement.style.setProperty('--rgb-color', rgbColor);
|
||||
document.documentElement.style.setProperty('--rgb-values', `${Math.round(r)},${Math.round(g)},${Math.round(b)}`);
|
||||
|
||||
// Update theme-color meta tags
|
||||
const themeColorLight = document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: light)"]');
|
||||
const themeColorDark = document.querySelector('meta[name="theme-color"][media="(prefers-color-scheme: dark)"]');
|
||||
if (themeColorLight) themeColorLight.setAttribute('content', rgbColor);
|
||||
if (themeColorDark) themeColorDark.setAttribute('content', rgbColor);
|
||||
|
||||
requestAnimationFrame(updateRGBColor);
|
||||
}
|
||||
|
||||
// Enhanced mobile touch handling
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const passwordToggle = document.querySelector('.password-toggle');
|
||||
passwordToggle.addEventListener('touchend', function(e) {
|
||||
e.preventDefault();
|
||||
togglePassword();
|
||||
});
|
||||
|
||||
// **Suppression du gestionnaire touchend pour le slider**
|
||||
/*
|
||||
const checkbox = document.getElementById('alwaysAuth');
|
||||
checkbox.parentElement.addEventListener('touchend', function(e) {
|
||||
e.stopPropagation();
|
||||
checkbox.checked = !checkbox.checked;
|
||||
});
|
||||
*/
|
||||
});
|
||||
|
||||
updateRGBColor();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
720
web/loot.html
@@ -1,95 +1,649 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bjorn Cyberviking - Loot</title>
|
||||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="web/css/styles.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<script src="web/scripts/loot.js" defer></script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Bjorn Cyberviking - Loot</title>
|
||||
<link rel="icon" href="web/images/favicon.ico" type="image/x-icon" />
|
||||
<link rel="stylesheet" href="web/css/global.css" />
|
||||
<link rel="stylesheet" href="web/css/all.min.css" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="web/images/icon-192x192.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#0a0a0a" />
|
||||
<script src="web/js/global.js" defer></script>
|
||||
|
||||
<style>
|
||||
/* ====== Respect global.css tokens (fallbacks only) ====== */
|
||||
:root{
|
||||
--_bg: var(--bg, #0b0c0f);
|
||||
--_panel: var(--c-panel-2, rgba(16,22,22,.55));
|
||||
--_border: var(--c-border, rgba(255,255,255,.08));
|
||||
--_ink: var(--ink, #e6fff7);
|
||||
--_muted: var(--muted, #8affc1cc);
|
||||
--_acid: var(--acid, #00ff9a);
|
||||
--_acid2: var(--acid-2, #18f0ff);
|
||||
--_shadow: var(--shadow, 0 10px 26px rgba(0,0,0,.35));
|
||||
}
|
||||
|
||||
*{ margin:0; padding:0; box-sizing:border-box; }
|
||||
|
||||
body{
|
||||
background: var(--_bg);
|
||||
color: var(--_ink);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', system-ui, sans-serif;
|
||||
min-height:100vh; overflow-x:hidden;
|
||||
}
|
||||
|
||||
.main{ padding:16px; }
|
||||
|
||||
/* gentle ambient glow (kept subtle) */
|
||||
body::before{
|
||||
content:'';
|
||||
position:fixed; inset:0;
|
||||
background:
|
||||
radial-gradient(60rem 60rem at 20% 50%, color-mix(in oklab, var(--_acid) 10%, transparent), transparent 60%),
|
||||
radial-gradient(60rem 60rem at 80% 80%, color-mix(in oklab, var(--_acid2) 10%, transparent), transparent 60%);
|
||||
pointer-events:none; z-index:1; animation:breathe 20s ease-in-out infinite;
|
||||
}
|
||||
@keyframes breathe{ 0%,100%{opacity:1;} 50%{opacity:.6;} }
|
||||
|
||||
.loot-container{
|
||||
position:relative; z-index:2;
|
||||
padding:16px; margin-top:5px;
|
||||
min-height:calc(100vh - 60px);
|
||||
display:flex; flex-direction:column; gap:16px;
|
||||
animation:fadeInUp .6s ease-out;
|
||||
}
|
||||
@keyframes fadeInUp{ from{opacity:0; transform:translateY(30px);} to{opacity:1; transform:translateY(0);} }
|
||||
|
||||
/* ===== Stats bar ===== */
|
||||
.stats-bar{
|
||||
display:flex; gap:12px; flex-wrap:wrap;
|
||||
padding:12px;
|
||||
background: color-mix(in oklab, var(--_panel) 88%, transparent);
|
||||
border:1px solid var(--_border);
|
||||
border-radius:12px;
|
||||
box-shadow: var(--_shadow);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
.stat-item{
|
||||
display:flex; align-items:center; gap:8px;
|
||||
padding:8px 16px;
|
||||
background: color-mix(in oklab, var(--_panel) 65%, transparent);
|
||||
border:1px solid var(--_border);
|
||||
border-radius:10px;
|
||||
transition: .2s;
|
||||
}
|
||||
.stat-item:hover{ background: color-mix(in oklab, var(--_panel) 78%, transparent); transform: translateY(-2px); }
|
||||
.stat-icon{ font-size:1.2rem; opacity:.95; }
|
||||
.stat-value{
|
||||
font-size:1.05rem; font-weight:800;
|
||||
background: linear-gradient(135deg, var(--_acid), var(--_acid2));
|
||||
-webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent;
|
||||
}
|
||||
.stat-label{ color: var(--_muted); font-size:.75rem; margin-left:4px; }
|
||||
|
||||
/* ===== Controls ===== */
|
||||
.controls-bar{ display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
|
||||
|
||||
.search-container{ flex:1; min-width:200px; position:relative; }
|
||||
.search-input{
|
||||
width:100%; padding:12px 16px 12px 44px;
|
||||
background: color-mix(in oklab, var(--_panel) 90%, transparent);
|
||||
border:1px solid var(--_border);
|
||||
border-radius:12px;
|
||||
color: var(--_ink); font-size:.95rem;
|
||||
backdrop-filter: blur(10px);
|
||||
transition:.2s;
|
||||
}
|
||||
.search-input:focus{
|
||||
outline:none;
|
||||
border-color: color-mix(in oklab, var(--_acid2) 40%, var(--_border));
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--_acid2) 18%, transparent);
|
||||
background: color-mix(in oklab, var(--_panel) 96%, transparent);
|
||||
}
|
||||
.search-icon{
|
||||
position:absolute; left:16px; top:50%; transform:translateY(-50%);
|
||||
color: var(--_muted); pointer-events:none;
|
||||
}
|
||||
.clear-search{
|
||||
position:absolute; right:12px; top:50%; transform:translateY(-50%);
|
||||
color: var(--_muted); cursor:pointer; font-size:1rem; display:none;
|
||||
}
|
||||
.search-input:not(:placeholder-shown) ~ .clear-search{ display:block; }
|
||||
|
||||
.view-controls{ display:flex; gap:8px; align-items:center; }
|
||||
.view-btn, .sort-btn{
|
||||
padding:10px;
|
||||
background: color-mix(in oklab, var(--_panel) 90%, transparent);
|
||||
border:1px solid var(--_border);
|
||||
border-radius:10px;
|
||||
color: var(--_muted); cursor:pointer;
|
||||
transition:.2s; backdrop-filter: blur(10px); font-size:1.1rem;
|
||||
}
|
||||
.view-btn:hover, .sort-btn:hover{ background: color-mix(in oklab, var(--_panel) 96%, transparent); color: var(--_ink); transform: translateY(-2px); }
|
||||
.view-btn.active{
|
||||
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid) 20%, transparent), color-mix(in oklab, var(--_acid2) 12%, transparent));
|
||||
color: var(--_ink);
|
||||
border-color: color-mix(in oklab, var(--_acid2) 35%, var(--_border));
|
||||
}
|
||||
|
||||
.sort-dropdown{ position:relative; }
|
||||
.sort-menu{
|
||||
position:absolute; top:calc(100% + 8px); right:0;
|
||||
background: color-mix(in oklab, var(--_panel) 98%, transparent);
|
||||
border:1px solid var(--_border); border-radius:12px;
|
||||
padding:8px; min-width:150px;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: var(--_shadow);
|
||||
opacity:0; pointer-events:none; transform: translateY(-10px);
|
||||
transition:.2s; z-index:10;
|
||||
}
|
||||
.sort-dropdown.active .sort-menu{ opacity:1; pointer-events:auto; transform: translateY(0); }
|
||||
.sort-option{
|
||||
padding:10px 12px; border-radius:8px; cursor:pointer; transition:.2s; font-size:.9rem; color: var(--_ink);
|
||||
}
|
||||
.sort-option:hover{ background: rgba(255,255,255,.05); }
|
||||
.sort-option.active{ color: var(--_ink); background: color-mix(in oklab, var(--_acid2) 14%, transparent); }
|
||||
|
||||
/* ===== Tabs ===== */
|
||||
.tabs-container{
|
||||
display:flex; gap:8px; padding:4px;
|
||||
background: color-mix(in oklab, var(--_panel) 88%, transparent);
|
||||
border-radius:12px; border:1px solid var(--_border);
|
||||
backdrop-filter: blur(10px);
|
||||
overflow-x:auto; scrollbar-width:none;
|
||||
}
|
||||
.tabs-container::-webkit-scrollbar{ display:none; }
|
||||
.tab{
|
||||
padding:10px 20px; border-radius:8px; cursor:pointer; transition:.2s;
|
||||
white-space:nowrap; font-size:.9rem; font-weight:700; position:relative;
|
||||
color: var(--_muted); border:1px solid transparent;
|
||||
}
|
||||
.tab:hover{ background: rgba(255,255,255,.05); color: var(--_ink); }
|
||||
.tab.active{
|
||||
background: linear-gradient(135deg, color-mix(in oklab, var(--_acid) 16%, transparent), color-mix(in oklab, var(--_acid2) 10%, transparent));
|
||||
color: var(--_ink);
|
||||
border-color: color-mix(in oklab, var(--_acid2) 28%, var(--_border));
|
||||
}
|
||||
.tab.active::after{
|
||||
content:''; position:absolute; bottom:0; left:10%; right:10%; height:2px;
|
||||
background: linear-gradient(90deg, var(--_acid), var(--_acid2)); border-radius:2px;
|
||||
}
|
||||
.tab-badge{
|
||||
display:inline-block; padding:2px 6px; margin-left:6px;
|
||||
background: rgba(255,255,255,.08); border:1px solid var(--_border);
|
||||
border-radius:10px; font-size:.75rem; font-weight:700; color: var(--_ink);
|
||||
}
|
||||
|
||||
/* ===== Explorer ===== */
|
||||
.explorer{
|
||||
background: color-mix(in oklab, var(--_panel) 88%, transparent);
|
||||
border-radius:20px; border:1px solid var(--_border);
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: var(--_shadow);
|
||||
overflow:hidden; flex:1; display:flex; flex-direction:column;
|
||||
animation:slideIn .6s ease-out;
|
||||
}
|
||||
@keyframes slideIn{ from{opacity:0; transform:translateX(-16px);} to{opacity:1; transform:translateX(0);} }
|
||||
|
||||
.explorer-content{ padding:20px; overflow-y:auto; flex:1; max-height:calc(100vh - 280px); }
|
||||
|
||||
.tree-view{ display:none; }
|
||||
.tree-view.active{ display:block; }
|
||||
.list-view{ display:none; }
|
||||
.list-view.active{ display:grid; gap:8px; }
|
||||
|
||||
.tree-item{ margin-bottom:4px; animation:itemSlide .3s ease-out backwards; }
|
||||
@keyframes itemSlide{ from{opacity:0; transform:translateX(-10px);} to{opacity:1; transform:translateX(0);} }
|
||||
|
||||
.tree-header{
|
||||
display:flex; align-items:center; padding:12px; cursor:pointer;
|
||||
border-radius:10px; transition:.2s; position:relative; overflow:hidden;
|
||||
}
|
||||
.tree-header::before{
|
||||
content:''; position:absolute; inset:0;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,.05), transparent);
|
||||
transform: translateX(-100%); transition: transform .6s;
|
||||
}
|
||||
.tree-header:hover::before{ transform: translateX(100%); }
|
||||
.tree-header:hover{ background: rgba(255,255,255,.04); }
|
||||
|
||||
.tree-icon{
|
||||
width:32px; height:32px; display:flex; align-items:center; justify-content:center;
|
||||
border-radius:8px; margin-right:12px; font-size:1.1rem; flex-shrink:0;
|
||||
background: color-mix(in oklab, var(--_acid) 12%, transparent); color: var(--_ink);
|
||||
}
|
||||
.folder-icon{ background: color-mix(in oklab, var(--_acid) 10%, transparent); color: var(--_ink); }
|
||||
|
||||
.tree-name{ flex:1; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.tree-chevron{ width:20px; height:20px; display:flex; align-items:center; justify-content:center; color: var(--_muted); transition: transform .3s cubic-bezier(.4,0,.2,1); margin-left:8px; }
|
||||
.tree-item.expanded .tree-chevron{ transform: rotate(90deg); }
|
||||
|
||||
.tree-children{
|
||||
max-height:0; overflow:hidden;
|
||||
transition:max-height .3s cubic-bezier(.4,0,.2,1);
|
||||
margin-left:20px; padding-left:20px;
|
||||
border-left:1px solid var(--_border);
|
||||
}
|
||||
.tree-item.expanded .tree-children{ max-height:5000px; }
|
||||
|
||||
.file-item{
|
||||
display:flex; align-items:center; padding:10px 12px; border-radius:10px;
|
||||
cursor:pointer; transition:.2s; margin-bottom:4px;
|
||||
}
|
||||
.file-item:hover{ background: rgba(255,255,255,.04); transform: translateX(4px); }
|
||||
.file-item:active{ transform: translateX(2px) scale(.98); }
|
||||
|
||||
.file-icon{
|
||||
width:28px; height:28px; display:flex; align-items:center; justify-content:center;
|
||||
border-radius:6px; margin-right:12px; font-size:.9rem; flex-shrink:0; color: var(--_ink);
|
||||
background: color-mix(in oklab, var(--_panel) 75%, transparent);
|
||||
}
|
||||
.file-icon.ssh{ background: color-mix(in oklab, var(--_acid) 12%, transparent); }
|
||||
.file-icon.sql{ background: color-mix(in oklab, var(--_acid2) 12%, transparent); }
|
||||
.file-icon.smb{ background: color-mix(in oklab, var(--_acid2) 16%, transparent); }
|
||||
.file-icon.other{ background: color-mix(in oklab, var(--_panel) 75%, transparent); }
|
||||
|
||||
.file-name{ flex:1; font-size:.9rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color: var(--_ink); }
|
||||
.file-type{
|
||||
padding:3px 8px; border-radius:6px; font-size:.7rem; font-weight:800;
|
||||
text-transform:uppercase; letter-spacing:.05em; margin-left:8px;
|
||||
border:1px solid var(--_border); color: var(--_ink);
|
||||
background: color-mix(in oklab, var(--_panel) 80%, transparent);
|
||||
}
|
||||
.file-type.ssh{ background: color-mix(in oklab, var(--_acid) 12%, transparent); }
|
||||
.file-type.sql{ background: color-mix(in oklab, var(--_acid2) 12%, transparent); }
|
||||
.file-type.smb{ background: color-mix(in oklab, var(--_acid2) 16%, transparent); }
|
||||
|
||||
.no-results{ text-align:center; color: var(--_muted); padding:40px; font-size:.95rem; }
|
||||
.no-results-icon{ font-size:3rem; margin-bottom:16px; opacity:.5; }
|
||||
|
||||
.loading{ display:flex; justify-content:center; align-items:center; height:200px; }
|
||||
.loading-spinner{
|
||||
width:40px; height:40px; border:3px solid var(--_border);
|
||||
border-top-color: var(--_acid2); border-radius:50%; animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin{ to{ transform: rotate(360deg); } }
|
||||
|
||||
/* ===== Responsive ===== */
|
||||
@media (max-width:768px){
|
||||
.loot-container{ padding:12px; gap:12px; }
|
||||
.controls-bar{ flex-direction:column; align-items:stretch; }
|
||||
.search-container{ width:100%; }
|
||||
.view-controls{ justify-content:center; }
|
||||
.tabs-container{ padding:2px; }
|
||||
.tab{ padding:8px 14px; font-size:.85rem; }
|
||||
.explorer-content{ padding:12px; max-height:calc(100vh - 320px); }
|
||||
.tree-children{ margin-left:12px; padding-left:12px; }
|
||||
.stat-item{ padding:6px 10px; }
|
||||
.stat-value{ font-size:.95rem; }
|
||||
}
|
||||
@media (hover:none){ .tree-header:active{ background: rgba(255,255,255,.06); } }
|
||||
</style>
|
||||
</head>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const fileList = document.getElementById("file-list");
|
||||
|
||||
let currentPath = "/"; // Start at root
|
||||
|
||||
function fetchFiles(path) {
|
||||
fetch(`/list_files?path=${encodeURIComponent(path)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
displayFiles(data, path);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching files:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function displayFiles(files, path) {
|
||||
currentPath = path;
|
||||
fileList.innerHTML = "";
|
||||
|
||||
files.forEach(file => {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = file.name;
|
||||
div.classList.add(file.is_directory ? "folder" : "file");
|
||||
|
||||
div.addEventListener("click", () => {
|
||||
if (file.is_directory) {
|
||||
fetchFiles(`${path}/${file.name}`);
|
||||
} else {
|
||||
window.open(`${path}/${file.name}`, '_blank');
|
||||
}
|
||||
});
|
||||
|
||||
fileList.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
fetchFiles(currentPath);
|
||||
});
|
||||
</script>
|
||||
<body>
|
||||
<div class="toolbar" id="mainToolbar">
|
||||
<button type="button" onclick="window.location.href='/index.html'" title="Playground">
|
||||
<img src="/web/images/console_icon.png" alt="Bjorn" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/config.html'" title="Config">
|
||||
<img src="/web/images/config_icon.png" alt="Icon_config" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/network.html'" title="Network">
|
||||
<img src="/web/images/network_icon.png" alt="Icon_network" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/netkb.html'" title="NetKB">
|
||||
<img src="/web/images/netkb_icon.png" alt="Icon_netkb" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/credentials.html'" title="Credentials">
|
||||
<img src="/web/images/cred_icon.png" alt="Icon_cred" style="height: 50px;">
|
||||
</button>
|
||||
<button type="button" onclick="window.location.href='/loot.html'" title="Loot">
|
||||
<img src="/web/images/loot_icon.png" alt="Icon_loot" style="height: 50px;">
|
||||
</button>
|
||||
</div>
|
||||
<div class="console-toolbar">
|
||||
<button type="button" class="toolbar-button" onclick="adjustLootFontSize(-1)" title="-">
|
||||
<img src="/web/images/less.png" alt="Icon_less" style="height: 50px;">
|
||||
</button>
|
||||
|
||||
<button id="toggle-toolbar" type="button" class="toolbar-button" onclick="toggleLootToolbar()" data-open="false">
|
||||
<img id="toggle-icon" src="/web/images/hide.png" alt="Toggle Toolbar" style="height: 50px;">
|
||||
</button>
|
||||
|
||||
<button type="button" class="toolbar-button" onclick="adjustLootFontSize(1)" title="+">
|
||||
<img src="/web/images/plus.png" alt="Icon_plus" style="height: 50px;">
|
||||
</button>
|
||||
</div>
|
||||
<div class="main" id="main">
|
||||
<div class="loot-container">
|
||||
<div id="file-list">
|
||||
<!-- The file list will be inserted here by JavaScript -->
|
||||
<!-- Stats bar -->
|
||||
<div class="stats-bar">
|
||||
<div class="stat-item"><span class="stat-icon">👥</span><span class="stat-value" id="stat-victims">0</span><span class="stat-label">victims</span></div>
|
||||
<div class="stat-item"><span class="stat-icon">📄</span><span class="stat-value" id="stat-files">0</span><span class="stat-label">files</span></div>
|
||||
<div class="stat-item"><span class="stat-icon">📁</span><span class="stat-value" id="stat-folders">0</span><span class="stat-label">folders</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls-bar">
|
||||
<div class="search-container">
|
||||
<span class="search-icon">🔍</span>
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Search in all categories..." />
|
||||
<span class="clear-search" id="clearSearch">❌</span>
|
||||
</div>
|
||||
<div class="view-controls">
|
||||
<button class="view-btn active" id="treeViewBtn" title="Tree View">🌳</button>
|
||||
<button class="view-btn" id="listViewBtn" title="List View">📋</button>
|
||||
<div class="sort-dropdown" id="sortDropdown">
|
||||
<button class="sort-btn" id="sortBtn">⬇️</button>
|
||||
<div class="sort-menu">
|
||||
<div class="sort-option active" data-sort="name">Name</div>
|
||||
<div class="sort-option" data-sort="type">Type</div>
|
||||
<div class="sort-option" data-sort="date">Date</div>
|
||||
<div class="sort-option" data-sort="asc">Asc</div>
|
||||
<div class="sort-option" data-sort="desc">Desc</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs-container" id="tabsContainer"></div>
|
||||
|
||||
<div class="explorer">
|
||||
<div class="explorer-content" id="explorerContent">
|
||||
<div class="loading"><div class="loading-spinner"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== JS: unchanged logic ===== -->
|
||||
<script>
|
||||
let fileData = [];
|
||||
let allFiles = [];
|
||||
let currentView = 'tree';
|
||||
let currentCategory = 'all';
|
||||
let currentSort = 'name';
|
||||
let sortDirection = 'asc';
|
||||
let searchTerm = '';
|
||||
let globalStats = {};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadFiles();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
function setupEventListeners(){
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const clearBtn = document.getElementById('clearSearch');
|
||||
let searchTimeout;
|
||||
|
||||
searchInput.addEventListener('input', e => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
searchTerm = e.target.value.toLowerCase();
|
||||
renderContent(true);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
clearBtn.addEventListener("click", () => {
|
||||
searchInput.value = "";
|
||||
searchTerm = "";
|
||||
renderContent();
|
||||
});
|
||||
|
||||
document.getElementById('treeViewBtn').addEventListener('click', () => setView('tree'));
|
||||
document.getElementById('listViewBtn').addEventListener('click', () => setView('list'));
|
||||
|
||||
const sortDropdown = document.getElementById('sortDropdown');
|
||||
const sortBtn = document.getElementById('sortBtn');
|
||||
sortBtn.addEventListener('click', () => { sortDropdown.classList.toggle('active'); });
|
||||
|
||||
document.querySelectorAll('.sort-option').forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
document.querySelectorAll('.sort-option').forEach(opt => opt.classList.remove('active'));
|
||||
option.classList.add('active');
|
||||
if (option.dataset.sort === 'asc' || option.dataset.sort === 'desc') {
|
||||
sortDirection = option.dataset.sort;
|
||||
} else {
|
||||
currentSort = option.dataset.sort;
|
||||
}
|
||||
sortDropdown.classList.remove('active');
|
||||
renderContent();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const sortDropdownEl = document.getElementById('sortDropdown');
|
||||
if (!sortDropdownEl.contains(e.target)) sortDropdownEl.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
function setView(view){
|
||||
currentView = view;
|
||||
document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.getElementById(view === 'tree' ? 'treeViewBtn' : 'listViewBtn').classList.add('active');
|
||||
renderContent();
|
||||
}
|
||||
|
||||
function loadFiles(){
|
||||
fetch('/loot_directories')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
fileData = data.data;
|
||||
processFiles();
|
||||
updateStats();
|
||||
renderContent();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Error loading files:', err);
|
||||
document.getElementById('explorerContent').innerHTML =
|
||||
'<div class="no-results"><div class="no-results-icon">⚠️</div>Failed to load files</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderTabs(categories){
|
||||
const tabs = document.getElementById('tabsContainer');
|
||||
tabs.innerHTML = '';
|
||||
tabs.innerHTML += `<div class="tab active" data-category="all">All <span class="tab-badge" id="badge-all">0</span></div>`;
|
||||
categories.forEach(cat => {
|
||||
tabs.innerHTML += `<div class="tab" data-category="${cat}">${cat.toUpperCase()}<span class="tab-badge" id="badge-${cat}">0</span></div>`;
|
||||
});
|
||||
tabs.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
currentCategory = tab.dataset.category;
|
||||
renderContent();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function processFiles(){
|
||||
allFiles = [];
|
||||
let stats = {};
|
||||
function extractFiles(items, path = ''){
|
||||
items.forEach(item => {
|
||||
if (item.type === 'directory' && item.children) {
|
||||
extractFiles(item.children, path + item.name + '/');
|
||||
} else {
|
||||
const category = getFileCategory(item.name, path);
|
||||
allFiles.push({ ...item, category, fullPath: path + item.name });
|
||||
stats[category] = (stats[category] || 0) + 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
extractFiles(fileData);
|
||||
globalStats = stats;
|
||||
renderTabs(Object.keys(stats));
|
||||
document.getElementById('badge-all').textContent = allFiles.length;
|
||||
for (const cat in stats) {
|
||||
const badge = document.getElementById(`badge-${cat}`);
|
||||
if (badge) badge.textContent = stats[cat];
|
||||
}
|
||||
}
|
||||
|
||||
function getFileCategory(filename, path){
|
||||
const lowerName = filename.toLowerCase();
|
||||
const lowerPath = path.toLowerCase();
|
||||
if (lowerPath.includes('ssh') || lowerName.includes('ssh') || lowerName.includes('key')) return 'ssh';
|
||||
else if (lowerPath.includes('sql') || lowerName.includes('sql') || lowerName.includes('database')) return 'sql';
|
||||
else if (lowerPath.includes('smb') || lowerName.includes('smb') || lowerName.includes('share')) return 'smb';
|
||||
return 'other';
|
||||
}
|
||||
function getDirCategory(path){
|
||||
const lowerPath = path.toLowerCase();
|
||||
if (lowerPath.includes('ssh')) return 'ssh';
|
||||
if (lowerPath.includes('sql')) return 'sql';
|
||||
if (lowerPath.includes('smb')) return 'smb';
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function updateStats(){
|
||||
let victims = new Set(); let totalFiles = 0; let totalFolders = 0;
|
||||
function scan(items){
|
||||
items.forEach(item => {
|
||||
if (item.type === 'directory') {
|
||||
totalFolders++;
|
||||
if (/^[0-9a-f:]{17}_\d+\.\d+\.\d+\.\d+$/i.test(item.name)) { victims.add(item.name); }
|
||||
if (item.children) scan(item.children);
|
||||
} else { totalFiles++; }
|
||||
});
|
||||
}
|
||||
scan(fileData);
|
||||
document.getElementById('stat-victims').textContent = victims.size;
|
||||
document.getElementById('stat-files').textContent = totalFiles;
|
||||
document.getElementById('stat-folders').textContent = totalFolders;
|
||||
}
|
||||
|
||||
/* ========== Search & badges (logic unchanged) ========== */
|
||||
function fileMatchesSearch(f){
|
||||
if (!searchTerm) return true;
|
||||
const n = f.name?.toLowerCase() || "";
|
||||
const p = f.fullPath?.toLowerCase() || "";
|
||||
return n.includes(searchTerm) || p.includes(searchTerm);
|
||||
}
|
||||
function computeSearchFilteredFiles(){
|
||||
return allFiles.filter(fileMatchesSearch);
|
||||
}
|
||||
function updateBadgesFromFiltered(){
|
||||
const filtered = computeSearchFilteredFiles();
|
||||
const allBadge = document.getElementById("badge-all");
|
||||
if (allBadge) allBadge.textContent = filtered.length;
|
||||
|
||||
const byCat = filtered.reduce((acc, f) => {
|
||||
acc[f.category] = (acc[f.category] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
document.querySelectorAll(".tab").forEach(tab => {
|
||||
const cat = tab.dataset.category;
|
||||
if (cat !== "all") {
|
||||
const badge = document.getElementById(`badge-${cat}`);
|
||||
if (badge) badge.textContent = byCat[cat] || 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderContent(autoExpand = false){
|
||||
const container = document.getElementById('explorerContent');
|
||||
if (currentView === 'tree') {
|
||||
renderTreeView(container);
|
||||
} else {
|
||||
renderListView(container);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTreeView(container){
|
||||
const filteredData = filterDataForTree();
|
||||
updateBadgesFromFiltered();
|
||||
|
||||
if (filteredData.length === 0) {
|
||||
container.innerHTML = '<div class="no-results"><div class="no-results-icon">🔍</div>No results</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `<div class="tree-view active">${renderTreeItems(filteredData, 0)}</div>`;
|
||||
container.querySelectorAll('.tree-header').forEach(header => {
|
||||
header.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const treeItem = header.closest('.tree-item');
|
||||
if (treeItem) { treeItem.classList.toggle('expanded'); }
|
||||
});
|
||||
});
|
||||
container.querySelectorAll('.file-item').forEach(fileItem => {
|
||||
fileItem.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
const path = fileItem.dataset.path;
|
||||
if (path) downloadFile(path);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function filterDataForTree(){
|
||||
function filterItems(items, path = '', isRoot = false){
|
||||
return items.map(item => {
|
||||
if (item.type === 'directory') {
|
||||
const dirPath = path + item.name + '/';
|
||||
const dirCategory = getDirCategory(dirPath);
|
||||
const filteredChildren = item.children ? filterItems(item.children, dirPath, false) : [];
|
||||
const nameMatch = item.name.toLowerCase().includes(searchTerm);
|
||||
|
||||
if (isRoot) {
|
||||
if (currentCategory !== 'all' && dirCategory !== currentCategory) return null;
|
||||
if (!searchTerm) return { ...item, children: filteredChildren };
|
||||
if (filteredChildren.length > 0) return { ...item, children: filteredChildren };
|
||||
return null;
|
||||
}
|
||||
|
||||
if (nameMatch || filteredChildren.length > 0) return { ...item, children: filteredChildren };
|
||||
return null;
|
||||
} else {
|
||||
const category = getFileCategory(item.name, path);
|
||||
const temp = { ...item, category, fullPath: path + item.name };
|
||||
const matchesSearch = fileMatchesSearch(temp);
|
||||
const matchesCategory = (currentCategory === 'all' || category === currentCategory);
|
||||
return (matchesSearch && matchesCategory) ? temp : null;
|
||||
}
|
||||
}).filter(Boolean);
|
||||
}
|
||||
return filterItems(fileData, '', true);
|
||||
}
|
||||
|
||||
function renderTreeItems(items, level, path = ''){
|
||||
return items.map((item, index) => {
|
||||
if (item.type === 'directory') {
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const expandedClass = searchTerm ? " expanded" : "";
|
||||
return `<div class="tree-item${expandedClass}" style="animation-delay:${index*0.05}s">
|
||||
<div class="tree-header">
|
||||
<div class="tree-icon folder-icon">📁</div>
|
||||
<div class="tree-name">${item.name}</div>
|
||||
${hasChildren ? '<div class="tree-chevron">▶</div>' : ''}
|
||||
</div>
|
||||
${hasChildren ? `<div class="tree-children">${renderTreeItems(item.children, level + 1, path + item.name + '/')}</div>` : ''}
|
||||
</div>`;
|
||||
} else {
|
||||
const category = getFileCategory(item.name, path);
|
||||
return renderFileItem({ ...item, category, fullPath: path + item.name }, category, index, false);
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderListView(container){
|
||||
let filtered = allFiles.filter(f => fileMatchesSearch(f) && (currentCategory === 'all' || f.category === currentCategory));
|
||||
updateBadgesFromFiltered();
|
||||
|
||||
filtered.sort((a, b) => {
|
||||
let res = 0;
|
||||
switch (currentSort) {
|
||||
case 'type': res = a.category.localeCompare(b.category) || a.name.localeCompare(b.name); break;
|
||||
case 'path': res = a.fullPath.localeCompare(b.fullPath); break;
|
||||
case 'date': res = a.name.localeCompare(b.name); break;
|
||||
case 'name':
|
||||
default: res = a.name.localeCompare(b.name);
|
||||
}
|
||||
return sortDirection === 'desc' ? -res : res;
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = '<div class="no-results"><div class="no-results-icon">🔍</div>No files</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = `<div class="list-view active">${filtered.map((f,i)=>renderFileItem(f, f.category, i, true)).join('')}</div>`;
|
||||
container.querySelectorAll('.file-item').forEach(fi => {
|
||||
fi.addEventListener('click', () => {
|
||||
const path = fi.dataset.path;
|
||||
if (path) downloadFile(path);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderFileItem(file, category, index = 0, showPath = false){
|
||||
const icons = { ssh: '🔐', sql: '🗄️', smb: '🌐', other: '📄' };
|
||||
return `<div class="file-item" data-path="${file.path}" style="animation-delay:${index*0.02}s">
|
||||
<div class="file-icon ${category}">${icons[category]}</div>
|
||||
<div class="file-name">${file.name}${showPath ? ` <span style="color:var(--_muted);font-size:0.75rem">— ${file.fullPath}</span>` : ''}</div>
|
||||
<span class="file-type ${category}">${category}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function downloadFile(path){
|
||||
window.location.href = `/loot_download?path=${encodeURIComponent(path)}`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,52 +3,52 @@
|
||||
"short_name": "Bjorn",
|
||||
"description": "Bjorn Cyberviking",
|
||||
"start_url": "/index.html",
|
||||
"display": "standalone",
|
||||
"display": "standalone",
|
||||
"background_color": "#333",
|
||||
"theme_color": "#333",
|
||||
"icons": [
|
||||
{
|
||||
"src": "images/icon-60x60.png",
|
||||
"src": "web/images/icon-60x60.png",
|
||||
"sizes": "60x60",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icon-72x72.png",
|
||||
"src": "web/images/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icon-96x96.png",
|
||||
"src": "web/images/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icon-128x128.png",
|
||||
"src": "web/images/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icon-144x144.png",
|
||||
"src": "web/images/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icon-152x152.png",
|
||||
"src": "web/images/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icon-192x192.png",
|
||||
"src": "web/images/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icon-384x384.png",
|
||||
"src": "web/images/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "images/icon-512x512.png",
|
||||
"src": "web/images/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
|
||||