BREAKING CHANGE: Complete refactor of architecture to prepare BJORN V2 release, APIs, assets, and UI, webapp, logics, attacks, a lot of new features...

This commit is contained in:
Fabien POLLY
2025-12-10 16:01:03 +01:00
parent a748f523a9
commit c1729756c0
927 changed files with 110752 additions and 9751 deletions

0
web/__init__.py Normal file
View File

939
web/actions_launcher.html Normal file
View 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 = { '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;' };
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

File diff suppressed because it is too large Load Diff

1492
web/attacks.html Normal file

File diff suppressed because it is too large Load Diff

376
web/backup_update.html Normal file
View 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">&times;</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 didentifiants */
.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, '&quot;')})'>💾</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,'&quot;')}" 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
View 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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

631
web/css/global.css Normal file
View 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 longlet UI : pas dassombrissement 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 (WiFi / 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)}

View File

@@ -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
View 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
View 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">
&larr; 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

BIN
web/images/actions_menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 34 KiB

BIN
web/images/attack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
web/images/attacks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
web/images/bjornwebicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
web/images/bluetooth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
web/images/boat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
web/images/credentials.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

BIN
web/images/database.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
web/images/filter_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
web/images/index.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

BIN
web/images/lighthouse.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
web/images/loot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
web/images/map_viking.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
web/images/menu_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
web/images/netkb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

BIN
web/images/network.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

BIN
web/images/notalive.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 56 KiB

BIN
web/images/scanner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
web/images/scheduler.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
web/images/settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

BIN
web/images/static_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
web/images/status_image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
web/images/switchmode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
web/images/table_mode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
web/images/target.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
web/images/target2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
web/images/treasure.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
web/images/update.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

BIN
web/images/web_enum.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
web/images/zombieland.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

File diff suppressed because it is too large Load Diff

2
web/js/d3.v7.min.js vendored Normal file

File diff suppressed because one or more lines are too long

5385
web/js/global.js Normal file

File diff suppressed because it is too large Load Diff

324
web/login.html Normal file
View 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>

View File

@@ -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>

View File

@@ -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"
}

Some files were not shown because too many files have changed in this diff Show More