A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1633 lines
51 KiB
Python
1633 lines
51 KiB
Python
"""
|
|
Admin DSMS Component - DSMS/IPFS WebUI, Archive Management
|
|
"""
|
|
|
|
def get_admin_dsms_css() -> str:
|
|
"""CSS für DSMS zurückgeben"""
|
|
return """
|
|
/* DSMS Styles */
|
|
.dsms-subtab {
|
|
padding: 6px 14px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--bp-text-muted);
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
border-radius: 4px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.dsms-subtab:hover {
|
|
background: var(--bp-border-subtle);
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.dsms-subtab.active {
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
}
|
|
|
|
.dsms-content {
|
|
display: none;
|
|
}
|
|
|
|
.dsms-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.dsms-status-card {
|
|
background: var(--bp-surface-elevated);
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.dsms-status-card h4 {
|
|
margin: 0 0 8px 0;
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.dsms-status-card .value {
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: var(--bp-text);
|
|
}
|
|
|
|
.dsms-status-card .value.online {
|
|
color: var(--bp-accent);
|
|
}
|
|
|
|
.dsms-status-card .value.offline {
|
|
color: var(--bp-danger);
|
|
}
|
|
|
|
.dsms-verify-success {
|
|
background: var(--bp-accent-soft);
|
|
border: 1px solid var(--bp-accent);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
color: var(--bp-accent);
|
|
}
|
|
|
|
.dsms-verify-error {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border: 1px solid var(--bp-danger);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
color: var(--bp-danger);
|
|
}
|
|
|
|
/* DSMS WebUI Styles */
|
|
.dsms-webui-nav {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--bp-text-muted);
|
|
font-size: 14px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
text-align: left;
|
|
width: 100%;
|
|
transition: all 0.2s;
|
|
}
|
|
.dsms-webui-nav:hover {
|
|
background: var(--bp-surface-elevated);
|
|
color: var(--bp-text);
|
|
}
|
|
.dsms-webui-nav.active {
|
|
background: var(--bp-primary-soft);
|
|
color: var(--bp-primary);
|
|
font-weight: 500;
|
|
}
|
|
.dsms-webui-section {
|
|
display: none;
|
|
}
|
|
.dsms-webui-section.active {
|
|
display: block;
|
|
}
|
|
.dsms-webui-stat-card {
|
|
background: var(--bp-surface-elevated);
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
}
|
|
.dsms-webui-stat-label {
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
margin-bottom: 4px;
|
|
}
|
|
.dsms-webui-stat-value {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--bp-text);
|
|
}
|
|
.dsms-webui-stat-sub {
|
|
font-size: 11px;
|
|
color: var(--bp-text-muted);
|
|
margin-top: 4px;
|
|
}
|
|
.dsms-webui-upload-zone {
|
|
border: 2px dashed var(--bp-border);
|
|
border-radius: 12px;
|
|
padding: 48px 24px;
|
|
background: var(--bp-input-bg);
|
|
transition: all 0.2s;
|
|
}
|
|
.dsms-webui-upload-zone.dragover {
|
|
border-color: var(--bp-primary);
|
|
background: var(--bp-primary-soft);
|
|
}
|
|
.dsms-webui-file-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 16px;
|
|
background: var(--bp-surface-elevated);
|
|
border: 1px solid var(--bp-border);
|
|
border-radius: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.dsms-webui-file-item .cid {
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
word-break: break-all;
|
|
}
|
|
|
|
.main-layout {
|
|
display: grid;
|
|
grid-template-columns: 240px minmax(0, 1fr);
|
|
height: 100%;
|
|
min-height: 0;
|
|
}
|
|
|
|
.sidebar {
|
|
border-right: 1px solid var(--bp-border-subtle);
|
|
background: var(--bp-gradient-sidebar);
|
|
padding: 14px 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
transition: background 0.3s ease, border-color 0.3s ease;
|
|
}
|
|
|
|
.sidebar-section-title {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
color: var(--bp-text);
|
|
padding: 8px 6px 6px 6px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.sidebar-menu {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.sidebar-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 10px;
|
|
border-radius: 9px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.sidebar-item.active {
|
|
background: var(--bp-surface-elevated);
|
|
color: var(--bp-accent);
|
|
border: 1px solid var(--bp-accent-soft);
|
|
}
|
|
|
|
[data-theme="light"] .sidebar-item.active {
|
|
background: var(--bp-primary-soft);
|
|
color: var(--bp-primary);
|
|
border: 1px solid var(--bp-primary);
|
|
}
|
|
|
|
.sidebar-item-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
}
|
|
|
|
.sidebar-item-badge {
|
|
font-size: 10px;
|
|
border-radius: 999px;
|
|
padding: 2px 7px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
}
|
|
|
|
.sidebar-footer {
|
|
margin-top: auto;
|
|
font-size: 11px;
|
|
color: var(--bp-text-muted);
|
|
padding: 0 6px;
|
|
}
|
|
|
|
.content {
|
|
padding: 14px 16px 16px 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
height: 100%;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.panel {
|
|
background: var(--bp-gradient-surface);
|
|
border-radius: 16px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
padding: 12px 14px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
height: 100%;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
transition: background 0.3s ease, border-color 0.3s ease;
|
|
}
|
|
|
|
[data-theme="light"] .panel {
|
|
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.panel-title {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--bp-text);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.panel-subtitle {
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.panel-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
overflow: auto;
|
|
}
|
|
|
|
.small-pill {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
padding: 4px 10px;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
color: var(--bp-text);
|
|
background: var(--bp-surface-elevated);
|
|
}
|
|
|
|
.upload-inline {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
padding: 10px;
|
|
background: var(--bp-surface-elevated);
|
|
border-radius: 8px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
}
|
|
|
|
[data-theme="light"] .upload-inline {
|
|
background: var(--bp-surface);
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.upload-inline input[type=file] {
|
|
font-size: 11px;
|
|
padding: 6px;
|
|
background: var(--bp-input-bg);
|
|
border: 1px solid var(--bp-border-subtle);
|
|
border-radius: 6px;
|
|
color: var(--bp-text);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.upload-inline input[type=file]::file-selector-button {
|
|
background: var(--bp-accent-soft);
|
|
border: 1px solid var(--bp-accent);
|
|
border-radius: 4px;
|
|
padding: 4px 10px;
|
|
color: var(--bp-accent);
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
[data-theme="light"] .upload-inline input[type=file]::file-selector-button {
|
|
background: var(--bp-primary-soft);
|
|
border-color: var(--bp-primary);
|
|
color: var(--bp-primary);
|
|
}
|
|
|
|
.upload-inline input[type=file]::file-selector-button:hover {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.file-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
max-height: 150px;
|
|
overflow-y: auto;
|
|
border-radius: 10px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
background: var(--bp-surface-elevated);
|
|
}
|
|
|
|
[data-theme="light"] .file-list {
|
|
border-color: var(--bp-primary);
|
|
background: var(--bp-surface);
|
|
}
|
|
|
|
.file-item {
|
|
font-size: 12px;
|
|
padding: 8px 10px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
transition: background 0.15s ease;
|
|
}
|
|
|
|
.file-item:nth-child(odd) {
|
|
background: var(--bp-surface-elevated);
|
|
}
|
|
|
|
[data-theme="light"] .file-item:nth-child(odd) {
|
|
background: rgba(108, 27, 27, 0.03);
|
|
}
|
|
|
|
.file-item:hover {
|
|
background: var(--bp-accent-soft);
|
|
}
|
|
|
|
[data-theme="light"] .file-item:hover {
|
|
background: var(--bp-primary-soft);
|
|
}
|
|
|
|
.file-item.active {
|
|
background: var(--bp-accent-soft);
|
|
}
|
|
|
|
.file-item-name {
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.file-item-delete {
|
|
font-size: 14px;
|
|
color: #f97316;
|
|
cursor: pointer;
|
|
padding: 4px 6px;
|
|
border-radius: 4px;
|
|
transition: all 0.15s ease;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.file-item-delete:hover {
|
|
color: #fb923c;
|
|
background: rgba(249,115,22,0.15);
|
|
}
|
|
|
|
.file-empty {
|
|
font-size: 12px;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.inline-process {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 12px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid rgba(148,163,184,0.2);
|
|
}
|
|
|
|
.preview-container {
|
|
flex: 1;
|
|
border-radius: 12px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
background: var(--bp-gradient-sidebar);
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: stretch;
|
|
justify-content: center;
|
|
position: relative;
|
|
min-height: 750px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
[data-theme="light"] .preview-container {
|
|
background: var(--bp-bg);
|
|
border: 2px solid var(--bp-primary);
|
|
box-shadow: 0 4px 20px rgba(108, 27, 27, 0.1);
|
|
}
|
|
|
|
.preview-placeholder {
|
|
font-size: 13px;
|
|
color: var(--bp-text-muted);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
padding: 20px;
|
|
}
|
|
|
|
.compare-wrapper {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) 110px minmax(0, 1fr);
|
|
gap: 8px;
|
|
width: 100%;
|
|
height: 100%;
|
|
position: relative;
|
|
}
|
|
|
|
.compare-section {
|
|
position: relative;
|
|
border-radius: 10px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
background: var(--bp-gradient-sidebar);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
min-height: 0;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
[data-theme="light"] .compare-section {
|
|
border: 2px solid var(--bp-primary);
|
|
box-shadow: 0 4px 20px rgba(108, 27, 27, 0.1);
|
|
}
|
|
|
|
.compare-header {
|
|
padding: 6px 10px;
|
|
font-size: 12px;
|
|
border-bottom: 1px solid var(--bp-border-subtle);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
[data-theme="light"] .compare-header {
|
|
background: var(--bp-primary-soft);
|
|
border-bottom: 1px solid var(--bp-primary);
|
|
}
|
|
|
|
.compare-header span {
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
[data-theme="light"] .compare-header span {
|
|
color: var(--bp-primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.compare-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 6px;
|
|
}
|
|
|
|
.compare-body-inner {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.preview-img {
|
|
max-width: 100%;
|
|
max-height: 100%;
|
|
object-fit: contain;
|
|
box-shadow: 0 18px 40px rgba(0,0,0,0.5);
|
|
border-radius: 10px;
|
|
}
|
|
|
|
[data-theme="light"] .preview-img {
|
|
box-shadow: 0 8px 24px rgba(108, 27, 27, 0.15);
|
|
}
|
|
|
|
.clean-frame {
|
|
width: 100%;
|
|
height: 100%;
|
|
border: none;
|
|
border-radius: 10px;
|
|
background: white;
|
|
}
|
|
|
|
.preview-nav {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
pointer-events: none;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.preview-nav button {
|
|
pointer-events: auto;
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
background: var(--bp-surface-elevated);
|
|
color: var(--bp-text);
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
[data-theme="light"] .preview-nav button {
|
|
border: 2px solid var(--bp-primary);
|
|
background: var(--bp-surface);
|
|
color: var(--bp-primary);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.preview-nav button:hover:not(:disabled) {
|
|
border-color: var(--bp-primary);
|
|
color: var(--bp-primary);
|
|
}
|
|
|
|
[data-theme="light"] .preview-nav button:hover:not(:disabled) {
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
}
|
|
|
|
.preview-nav button:disabled {
|
|
opacity: 0.35;
|
|
cursor: default;
|
|
}
|
|
|
|
.preview-nav span {
|
|
position: absolute;
|
|
bottom: 6px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
font-size: 11px;
|
|
padding: 2px 8px;
|
|
border-radius: 999px;
|
|
background: var(--bp-surface-elevated);
|
|
border: 1px solid var(--bp-border-subtle);
|
|
}
|
|
|
|
[data-theme="light"] .preview-nav span {
|
|
background: var(--bp-surface);
|
|
border: 1px solid var(--bp-primary);
|
|
color: var(--bp-primary);
|
|
}
|
|
|
|
.preview-thumbnails {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
padding: 8px 4px;
|
|
overflow-y: auto;
|
|
align-items: center;
|
|
}
|
|
|
|
.preview-thumb {
|
|
min-width: 90px;
|
|
width: 90px;
|
|
height: 70px;
|
|
border-radius: 8px;
|
|
border: 2px solid rgba(148,163,184,0.25);
|
|
background: rgba(15,23,42,0.5);
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.preview-thumb:hover {
|
|
border-color: var(--bp-accent);
|
|
}
|
|
|
|
.preview-thumb.active {
|
|
border-color: var(--bp-accent);
|
|
border-width: 3px;
|
|
}
|
|
|
|
.preview-thumb img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.preview-thumb-label {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
font-size: 9px;
|
|
padding: 2px;
|
|
background: rgba(0,0,0,0.8);
|
|
color: white;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.pager {
|
|
grid-column: 1 / -1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
padding: 2px 0;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.pager button {
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
background: var(--bp-surface-elevated);
|
|
color: var(--bp-text);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.pager button:hover {
|
|
border-color: var(--bp-primary);
|
|
color: var(--bp-primary);
|
|
}
|
|
|
|
[data-theme="light"] .pager button {
|
|
border: 2px solid var(--bp-primary);
|
|
background: var(--bp-surface);
|
|
color: var(--bp-primary);
|
|
font-weight: 700;
|
|
}
|
|
|
|
[data-theme="light"] .pager button:hover {
|
|
background: var(--bp-primary);
|
|
color: white;
|
|
}
|
|
|
|
.status-bar {
|
|
position: fixed;
|
|
right: 18px;
|
|
bottom: 18px;
|
|
padding: 8px 12px;
|
|
border-radius: 999px;
|
|
background: var(--bp-surface-elevated);
|
|
border: 1px solid var(--bp-border-subtle);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 12px;
|
|
min-width: 230px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
[data-theme="light"] .status-bar {
|
|
background: var(--bp-surface);
|
|
border: 2px solid var(--bp-primary);
|
|
box-shadow: 0 4px 16px rgba(108, 27, 27, 0.15);
|
|
}
|
|
|
|
.status-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 999px;
|
|
background: var(--bp-text-muted);
|
|
}
|
|
|
|
.status-dot.busy {
|
|
background: var(--bp-accent);
|
|
}
|
|
|
|
.status-dot.error {
|
|
background: var(--bp-danger);
|
|
}
|
|
|
|
.status-text-main {
|
|
font-size: 12px;
|
|
}
|
|
|
|
.status-text-sub {
|
|
font-size: 11px;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
.footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 12px;
|
|
font-size: 11px;
|
|
color: var(--bp-text-muted);
|
|
border-top: 1px solid var(--bp-border-subtle);
|
|
background: var(--bp-surface-elevated);
|
|
transition: background 0.3s ease;
|
|
}
|
|
|
|
.footer a {
|
|
color: var(--bp-text-muted);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.footer a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.btn {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
padding: 8px 16px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
background: var(--bp-surface-elevated);
|
|
color: var(--bp-text);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.btn:hover:not(:disabled) {
|
|
border-color: var(--bp-primary);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-primary {
|
|
border-color: var(--bp-accent);
|
|
background: var(--bp-btn-primary-bg);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: var(--bp-btn-primary-hover);
|
|
box-shadow: 0 4px 12px rgba(34,197,94,0.3);
|
|
}
|
|
|
|
[data-theme="light"] .btn-primary:hover:not(:disabled) {
|
|
box-shadow: 0 4px 12px rgba(108, 27, 27, 0.3);
|
|
}
|
|
|
|
.btn-ghost {
|
|
background: transparent;
|
|
}
|
|
|
|
.btn-ghost:hover:not(:disabled) {
|
|
background: var(--bp-surface-elevated);
|
|
}
|
|
|
|
[data-theme="light"] .btn-ghost {
|
|
border-color: var(--bp-primary);
|
|
color: var(--bp-primary);
|
|
}
|
|
|
|
[data-theme="light"] .btn-ghost:hover:not(:disabled) {
|
|
background: var(--bp-primary-soft);
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 6px 12px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.panel-tools-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.card-toggle-bar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.toggle-pill {
|
|
font-size: 11px;
|
|
padding: 4px 8px;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
background: var(--bp-surface-elevated);
|
|
color: var(--bp-text-muted);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.toggle-pill.active {
|
|
border-color: var(--bp-accent);
|
|
color: var(--bp-accent);
|
|
background: var(--bp-accent-soft);
|
|
}
|
|
|
|
[data-theme="light"] .toggle-pill.active {
|
|
border-color: var(--bp-primary);
|
|
color: var(--bp-primary);
|
|
background: var(--bp-primary-soft);
|
|
}
|
|
|
|
.cards-grid {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
grid-auto-rows: 1fr;
|
|
gap: 10px;
|
|
min-height: 0;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.card {
|
|
border-radius: 14px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
background: var(--bp-card-bg);
|
|
padding: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
cursor: pointer;
|
|
min-height: 0;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
[data-theme="light"] .card {
|
|
border: 2px solid var(--bp-primary);
|
|
box-shadow: 0 4px 20px rgba(108, 27, 27, 0.1);
|
|
}
|
|
|
|
[data-theme="light"] .card:hover {
|
|
box-shadow: 0 6px 28px rgba(108, 27, 27, 0.2);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
[data-theme="light"] .card-header {
|
|
border-bottom: 1px solid var(--bp-border-subtle);
|
|
padding-bottom: 8px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
[data-theme="light"] .card-title {
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: var(--bp-text);
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
.card-badge {
|
|
font-size: 10px;
|
|
border-radius: 999px;
|
|
padding: 2px 7px;
|
|
border: 1px solid var(--bp-border-subtle);
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
[data-theme="light"] .card-badge {
|
|
background: linear-gradient(135deg, var(--bp-accent) 0%, #4CAF50 100%);
|
|
color: white;
|
|
border: none;
|
|
font-weight: 600;
|
|
padding: 4px 10px;
|
|
}
|
|
|
|
.card-body {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
font-size: 11px;
|
|
color: var(--bp-text-muted);
|
|
}
|
|
|
|
[data-theme="light"] .card-body {
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.card-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.card-hidden {
|
|
display: none;
|
|
}
|
|
|
|
.card-full {
|
|
grid-column: 1 / -1;
|
|
min-height: 220px;
|
|
}
|
|
"""
|
|
|
|
|
|
def get_admin_dsms_html() -> str:
|
|
"""HTML für DSMS WebUI Modal zurückgeben"""
|
|
return """
|
|
|
|
"""
|
|
|
|
|
|
def get_admin_dsms_js() -> str:
|
|
"""JavaScript für DSMS Functions zurückgeben"""
|
|
return """
|
|
const DSMS_GATEWAY_URL = 'http://localhost:8082';
|
|
let dsmsArchives = [];
|
|
|
|
function switchDsmsTab(tabName) {
|
|
document.querySelectorAll('.dsms-subtab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.dsms-content').forEach(c => c.classList.remove('active'));
|
|
|
|
document.querySelector(`.dsms-subtab[data-dsms-tab="${tabName}"]`)?.classList.add('active');
|
|
document.getElementById(`dsms-${tabName}`)?.classList.add('active');
|
|
|
|
// Load data for specific tabs
|
|
if (tabName === 'settings') {
|
|
loadDsmsNodeInfo();
|
|
}
|
|
}
|
|
|
|
async function loadDsmsData() {
|
|
await Promise.all([
|
|
loadDsmsStatus(),
|
|
loadDsmsArchives(),
|
|
loadDsmsDocumentSelect()
|
|
]);
|
|
}
|
|
|
|
async function loadDsmsStatus() {
|
|
const container = document.getElementById('dsms-status-cards');
|
|
container.innerHTML = '<div class="admin-loading">Lade DSMS Status...</div>';
|
|
|
|
try {
|
|
const [healthRes, nodeRes] = await Promise.all([
|
|
fetch(`${DSMS_GATEWAY_URL}/health`).catch(() => null),
|
|
fetch(`${DSMS_GATEWAY_URL}/api/v1/node/info`).catch(() => null)
|
|
]);
|
|
|
|
const health = healthRes?.ok ? await healthRes.json() : null;
|
|
const nodeInfo = nodeRes?.ok ? await nodeRes.json() : null;
|
|
|
|
const isOnline = health?.ipfs_connected === true;
|
|
const repoSize = nodeInfo?.repo_size ? formatBytes(nodeInfo.repo_size) : '-';
|
|
const storageMax = nodeInfo?.storage_max ? formatBytes(nodeInfo.storage_max) : '-';
|
|
const numObjects = nodeInfo?.num_objects ?? '-';
|
|
|
|
container.innerHTML = `
|
|
<div class="dsms-status-card">
|
|
<h4>Status</h4>
|
|
<div class="value ${isOnline ? 'online' : 'offline'}">${isOnline ? 'Online' : 'Offline'}</div>
|
|
</div>
|
|
<div class="dsms-status-card">
|
|
<h4>Speicher verwendet</h4>
|
|
<div class="value">${repoSize}</div>
|
|
</div>
|
|
<div class="dsms-status-card">
|
|
<h4>Max. Speicher</h4>
|
|
<div class="value">${storageMax}</div>
|
|
</div>
|
|
<div class="dsms-status-card">
|
|
<h4>Objekte</h4>
|
|
<div class="value">${numObjects}</div>
|
|
</div>
|
|
`;
|
|
} catch(e) {
|
|
container.innerHTML = `
|
|
<div class="dsms-status-card" style="grid-column: 1/-1;">
|
|
<h4>Status</h4>
|
|
<div class="value offline">Nicht erreichbar</div>
|
|
<p style="font-size: 12px; color: var(--bp-text-muted); margin-top: 8px;">
|
|
DSMS Gateway ist nicht verfügbar. Stellen Sie sicher, dass die Container laufen.
|
|
</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
async function loadDsmsArchives() {
|
|
const container = document.getElementById('dsms-archives-table');
|
|
container.innerHTML = '<div class="admin-loading">Lade archivierte Dokumente...</div>';
|
|
|
|
try {
|
|
const token = localStorage.getItem('bp_token') || '';
|
|
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/documents`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error('Fehler beim Laden');
|
|
}
|
|
|
|
const data = await res.json();
|
|
dsmsArchives = data.documents || [];
|
|
|
|
if (dsmsArchives.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="admin-empty">
|
|
<p>Keine archivierten Dokumente vorhanden.</p>
|
|
<p style="font-size: 12px; color: var(--bp-text-muted);">
|
|
Klicken Sie auf "+ Dokument archivieren" um ein Legal Document im DSMS zu speichern.
|
|
</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `
|
|
<table class="admin-table">
|
|
<thead>
|
|
<tr>
|
|
<th>CID</th>
|
|
<th>Dokument</th>
|
|
<th>Version</th>
|
|
<th>Archiviert am</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${dsmsArchives.map(doc => `
|
|
<tr>
|
|
<td>
|
|
<code style="font-size: 11px; background: var(--bp-surface-elevated); padding: 2px 6px; border-radius: 4px;">
|
|
${doc.cid.substring(0, 12)}...
|
|
</code>
|
|
</td>
|
|
<td>${doc.metadata?.document_id || doc.filename || '-'}</td>
|
|
<td>${doc.metadata?.version || '-'}</td>
|
|
<td>${doc.metadata?.created_at ? new Date(doc.metadata.created_at).toLocaleString('de-DE') : '-'}</td>
|
|
<td>
|
|
<button class="btn btn-ghost btn-sm" onclick="verifyDsmsDocumentByCid('${doc.cid}')" title="Verifizieren">
|
|
✓
|
|
</button>
|
|
<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${doc.cid}')" title="CID kopieren">
|
|
📋
|
|
</button>
|
|
<a href="${DSMS_GATEWAY_URL.replace('8082', '8085')}/ipfs/${doc.cid}" target="_blank" class="btn btn-ghost btn-sm" title="Im Gateway öffnen">
|
|
↗
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
} catch(e) {
|
|
container.innerHTML = `<div class="admin-empty">Fehler: ${e.message}</div>`;
|
|
}
|
|
}
|
|
|
|
async function loadDsmsDocumentSelect() {
|
|
const select = document.getElementById('dsms-archive-doc-select');
|
|
if (!select) return;
|
|
|
|
try {
|
|
const res = await fetch('/api/consent/admin/documents');
|
|
if (!res.ok) return;
|
|
|
|
const data = await res.json();
|
|
const docs = data.documents || [];
|
|
|
|
select.innerHTML = '<option value="">-- Dokument wählen --</option>' +
|
|
docs.map(d => `<option value="${d.id}">${d.name} (${d.type})</option>`).join('');
|
|
} catch(e) {
|
|
console.error('Error loading documents:', e);
|
|
}
|
|
}
|
|
|
|
async function loadDsmsVersionSelect() {
|
|
const docSelect = document.getElementById('dsms-archive-doc-select');
|
|
const versionSelect = document.getElementById('dsms-archive-version-select');
|
|
const docId = docSelect?.value;
|
|
|
|
if (!docId) {
|
|
versionSelect.innerHTML = '<option value="">-- Erst Dokument wählen --</option>';
|
|
versionSelect.disabled = true;
|
|
return;
|
|
}
|
|
|
|
versionSelect.disabled = false;
|
|
versionSelect.innerHTML = '<option value="">Lade Versionen...</option>';
|
|
|
|
try {
|
|
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
|
|
if (!res.ok) throw new Error('Fehler');
|
|
|
|
const data = await res.json();
|
|
const versions = data.versions || [];
|
|
|
|
if (versions.length === 0) {
|
|
versionSelect.innerHTML = '<option value="">Keine Versionen vorhanden</option>';
|
|
return;
|
|
}
|
|
|
|
versionSelect.innerHTML = '<option value="">-- Version wählen --</option>' +
|
|
versions.map(v => `<option value="${v.id}" data-content="${encodeURIComponent(v.content || '')}" data-version="${v.version}">
|
|
${v.version} (${v.language}) - ${v.status}
|
|
</option>`).join('');
|
|
} catch(e) {
|
|
versionSelect.innerHTML = '<option value="">Fehler beim Laden</option>';
|
|
}
|
|
}
|
|
|
|
// Attach event listener for doc select change
|
|
document.getElementById('dsms-archive-doc-select')?.addEventListener('change', loadDsmsVersionSelect);
|
|
|
|
function showArchiveForm() {
|
|
document.getElementById('dsms-archive-form').style.display = 'block';
|
|
loadDsmsDocumentSelect();
|
|
}
|
|
|
|
function hideArchiveForm() {
|
|
document.getElementById('dsms-archive-form').style.display = 'none';
|
|
}
|
|
|
|
async function archiveDocumentToDsms() {
|
|
const docSelect = document.getElementById('dsms-archive-doc-select');
|
|
const versionSelect = document.getElementById('dsms-archive-version-select');
|
|
const selectedOption = versionSelect.options[versionSelect.selectedIndex];
|
|
|
|
if (!docSelect.value || !versionSelect.value) {
|
|
alert('Bitte Dokument und Version auswählen');
|
|
return;
|
|
}
|
|
|
|
const content = decodeURIComponent(selectedOption.dataset.content || '');
|
|
const version = selectedOption.dataset.version;
|
|
const docId = docSelect.value;
|
|
|
|
if (!content) {
|
|
alert('Die ausgewählte Version hat keinen Inhalt');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const token = localStorage.getItem('bp_token') || '';
|
|
const params = new URLSearchParams({
|
|
document_id: docId,
|
|
version: version,
|
|
content: content,
|
|
language: 'de'
|
|
});
|
|
|
|
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/legal-documents/archive?${params}`, {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json();
|
|
throw new Error(err.detail || 'Archivierung fehlgeschlagen');
|
|
}
|
|
|
|
const result = await res.json();
|
|
alert(`Dokument erfolgreich archiviert!\\n\\nCID: ${result.cid}\\nChecksum: ${result.checksum}`);
|
|
hideArchiveForm();
|
|
loadDsmsArchives();
|
|
} catch(e) {
|
|
alert('Fehler: ' + e.message);
|
|
}
|
|
}
|
|
|
|
async function verifyDsmsDocument() {
|
|
const cidInput = document.getElementById('dsms-verify-cid');
|
|
const resultDiv = document.getElementById('dsms-verify-result');
|
|
const cid = cidInput?.value?.trim();
|
|
|
|
if (!cid) {
|
|
alert('Bitte CID eingeben');
|
|
return;
|
|
}
|
|
|
|
await verifyDsmsDocumentByCid(cid);
|
|
}
|
|
|
|
async function verifyDsmsDocumentByCid(cid) {
|
|
const resultDiv = document.getElementById('dsms-verify-result');
|
|
resultDiv.style.display = 'block';
|
|
resultDiv.innerHTML = '<div class="admin-loading">Verifiziere...</div>';
|
|
|
|
// Switch to verify tab
|
|
switchDsmsTab('verify');
|
|
document.getElementById('dsms-verify-cid').value = cid;
|
|
|
|
try {
|
|
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/verify/${cid}`);
|
|
const data = await res.json();
|
|
|
|
if (data.exists && data.integrity_valid) {
|
|
resultDiv.innerHTML = `
|
|
<div class="dsms-verify-success">
|
|
<h4 style="margin: 0 0 12px 0;">✓ Dokument verifiziert</h4>
|
|
<div style="display: grid; gap: 8px; font-size: 13px;">
|
|
<div><strong>CID:</strong> <code>${cid}</code></div>
|
|
<div><strong>Integrität:</strong> Gültig</div>
|
|
<div><strong>Typ:</strong> ${data.metadata?.document_type || '-'}</div>
|
|
<div><strong>Dokument-ID:</strong> ${data.metadata?.document_id || '-'}</div>
|
|
<div><strong>Version:</strong> ${data.metadata?.version || '-'}</div>
|
|
<div><strong>Erstellt:</strong> ${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '-'}</div>
|
|
<div><strong>Checksum:</strong> <code style="font-size: 10px;">${data.stored_checksum || '-'}</code></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else if (data.exists && !data.integrity_valid) {
|
|
resultDiv.innerHTML = `
|
|
<div class="dsms-verify-error">
|
|
<h4 style="margin: 0 0 12px 0;">⚠ Integritätsfehler</h4>
|
|
<p>Das Dokument existiert, aber die Prüfsumme stimmt nicht überein.</p>
|
|
<p style="font-size: 12px;">Gespeichert: ${data.stored_checksum}</p>
|
|
<p style="font-size: 12px;">Berechnet: ${data.calculated_checksum}</p>
|
|
</div>
|
|
`;
|
|
} else {
|
|
resultDiv.innerHTML = `
|
|
<div class="dsms-verify-error">
|
|
<h4 style="margin: 0 0 12px 0;">✗ Nicht gefunden</h4>
|
|
<p>Kein Dokument mit diesem CID gefunden.</p>
|
|
${data.error ? `<p style="font-size: 12px;">${data.error}</p>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
} catch(e) {
|
|
resultDiv.innerHTML = `
|
|
<div class="dsms-verify-error">
|
|
<h4 style="margin: 0 0 12px 0;">Fehler</h4>
|
|
<p>${e.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
async function loadDsmsNodeInfo() {
|
|
const container = document.getElementById('dsms-node-info');
|
|
container.innerHTML = '<div class="admin-loading">Lade Node-Info...</div>';
|
|
|
|
try {
|
|
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/node/info`);
|
|
if (!res.ok) throw new Error('Nicht erreichbar');
|
|
|
|
const info = await res.json();
|
|
|
|
container.innerHTML = `
|
|
<div style="display: grid; gap: 12px; font-size: 13px;">
|
|
<div><strong>Node ID:</strong> <code style="font-size: 11px;">${info.node_id || '-'}</code></div>
|
|
<div><strong>Agent:</strong> ${info.agent_version || '-'}</div>
|
|
<div><strong>Repo-Größe:</strong> ${info.repo_size ? formatBytes(info.repo_size) : '-'}</div>
|
|
<div><strong>Max. Speicher:</strong> ${info.storage_max ? formatBytes(info.storage_max) : '-'}</div>
|
|
<div><strong>Objekte:</strong> ${info.num_objects ?? '-'}</div>
|
|
<div>
|
|
<strong>Adressen:</strong>
|
|
<ul style="margin: 4px 0 0 0; padding-left: 20px; font-size: 11px; color: var(--bp-text-muted);">
|
|
${(info.addresses || []).map(a => `<li><code>${a}</code></li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} catch(e) {
|
|
container.innerHTML = `<div class="admin-empty">DSMS nicht erreichbar: ${e.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function copyToClipboard(text) {
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
// Optional: Show toast
|
|
}).catch(err => {
|
|
console.error('Copy failed:', err);
|
|
});
|
|
}
|
|
|
|
// ==========================================
|
|
// DSMS WEBUI FUNCTIONS
|
|
// ==========================================
|
|
function openDsmsWebUI() {
|
|
document.getElementById('dsms-webui-modal').style.display = 'flex';
|
|
loadDsmsWebUIData();
|
|
}
|
|
|
|
function closeDsmsWebUI() {
|
|
document.getElementById('dsms-webui-modal').style.display = 'none';
|
|
}
|
|
|
|
function switchDsmsWebUISection(section) {
|
|
// Update nav buttons
|
|
document.querySelectorAll('.dsms-webui-nav').forEach(btn => {
|
|
btn.classList.toggle('active', btn.dataset.section === section);
|
|
});
|
|
// Update sections
|
|
document.querySelectorAll('.dsms-webui-section').forEach(sec => {
|
|
sec.classList.remove('active');
|
|
sec.style.display = 'none';
|
|
});
|
|
const activeSection = document.getElementById('dsms-webui-' + section);
|
|
if (activeSection) {
|
|
activeSection.classList.add('active');
|
|
activeSection.style.display = 'block';
|
|
}
|
|
// Load section-specific data
|
|
if (section === 'peers') loadDsmsPeers();
|
|
}
|
|
|
|
async function loadDsmsWebUIData() {
|
|
try {
|
|
// Load node info
|
|
const infoRes = await fetch(DSMS_GATEWAY_URL + '/api/v1/node/info');
|
|
const info = await infoRes.json();
|
|
|
|
document.getElementById('webui-status').innerHTML = '<span style="color: var(--bp-accent);">Online</span>';
|
|
document.getElementById('webui-node-id').textContent = info.node_id || '--';
|
|
document.getElementById('webui-protocol').textContent = info.protocol_version || '--';
|
|
document.getElementById('webui-agent').textContent = info.agent_version || '--';
|
|
document.getElementById('webui-repo-size').textContent = formatBytes(info.repo_size || 0);
|
|
document.getElementById('webui-storage-info').textContent = 'Max: ' + formatBytes(info.storage_max || 0);
|
|
document.getElementById('webui-num-objects').textContent = (info.num_objects || 0).toLocaleString();
|
|
|
|
// Addresses
|
|
const addresses = info.addresses || [];
|
|
document.getElementById('webui-addresses').innerHTML = addresses.length > 0
|
|
? addresses.map(a => '<div style="margin-bottom: 4px;">' + a + '</div>').join('')
|
|
: '<span style="color: var(--bp-text-muted);">Keine Adressen verfügbar</span>';
|
|
|
|
// Load pinned count
|
|
const token = localStorage.getItem('bp_token') || '';
|
|
const docsRes = await fetch(DSMS_GATEWAY_URL + '/api/v1/documents', {
|
|
headers: { 'Authorization': 'Bearer ' + token }
|
|
});
|
|
if (docsRes.ok) {
|
|
const docs = await docsRes.json();
|
|
document.getElementById('webui-pinned-count').textContent = docs.total || 0;
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load WebUI data:', e);
|
|
document.getElementById('webui-status').innerHTML = '<span style="color: var(--bp-danger);">Offline</span>';
|
|
}
|
|
}
|
|
|
|
async function loadDsmsPeers() {
|
|
const container = document.getElementById('webui-peers-list');
|
|
try {
|
|
// IPFS peers endpoint via proxy would need direct IPFS API access
|
|
// For now, show info that private network has no peers
|
|
container.innerHTML = `
|
|
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; text-align: center;">
|
|
<div style="font-size: 48px; margin-bottom: 16px;">🔒</div>
|
|
<h4 style="margin: 0 0 8px 0;">Privates Netzwerk</h4>
|
|
<p style="color: var(--bp-text-muted); margin: 0;">
|
|
DSMS läuft als isolierter Node. Keine externen Peers verbunden.
|
|
</p>
|
|
</div>
|
|
`;
|
|
} catch (e) {
|
|
container.innerHTML = '<div class="dsms-verify-error">Fehler beim Laden der Peers</div>';
|
|
}
|
|
}
|
|
|
|
// File upload handlers
|
|
function handleDsmsDragOver(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
document.getElementById('dsms-upload-zone').classList.add('dragover');
|
|
}
|
|
|
|
function handleDsmsDragLeave(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
document.getElementById('dsms-upload-zone').classList.remove('dragover');
|
|
}
|
|
|
|
async function handleDsmsFileDrop(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
document.getElementById('dsms-upload-zone').classList.remove('dragover');
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
await uploadDsmsFiles(files);
|
|
}
|
|
}
|
|
|
|
async function handleDsmsFileSelect(e) {
|
|
const files = e.target.files;
|
|
if (files.length > 0) {
|
|
await uploadDsmsFiles(files);
|
|
}
|
|
}
|
|
|
|
async function uploadDsmsFiles(files) {
|
|
const token = localStorage.getItem('bp_token') || '';
|
|
const progressDiv = document.getElementById('dsms-upload-progress');
|
|
const statusDiv = document.getElementById('dsms-upload-status');
|
|
const barDiv = document.getElementById('dsms-upload-bar');
|
|
const resultsDiv = document.getElementById('dsms-upload-results');
|
|
|
|
progressDiv.style.display = 'block';
|
|
resultsDiv.innerHTML = '';
|
|
|
|
const results = [];
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
statusDiv.textContent = 'Lade hoch: ' + file.name + ' (' + (i+1) + '/' + files.length + ')';
|
|
barDiv.style.width = ((i / files.length) * 100) + '%';
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('document_type', 'legal_document');
|
|
|
|
const res = await fetch(DSMS_GATEWAY_URL + '/api/v1/documents', {
|
|
method: 'POST',
|
|
headers: { 'Authorization': 'Bearer ' + token },
|
|
body: formData
|
|
});
|
|
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
results.push({ file: file.name, cid: data.cid, success: true });
|
|
} else {
|
|
results.push({ file: file.name, error: 'Upload fehlgeschlagen', success: false });
|
|
}
|
|
} catch (e) {
|
|
results.push({ file: file.name, error: e.message, success: false });
|
|
}
|
|
}
|
|
|
|
barDiv.style.width = '100%';
|
|
statusDiv.textContent = 'Upload abgeschlossen!';
|
|
|
|
// Show results
|
|
resultsDiv.innerHTML = '<h4 style="margin: 0 0 12px 0;">Ergebnisse</h4>' +
|
|
results.map(r => `
|
|
<div class="dsms-webui-file-item">
|
|
<div>
|
|
<div style="font-weight: 500;">${r.file}</div>
|
|
${r.success
|
|
? '<div class="cid" style="color: var(--bp-accent);">CID: ' + r.cid + '</div>'
|
|
: '<div style="color: var(--bp-danger);">' + r.error + '</div>'
|
|
}
|
|
</div>
|
|
${r.success ? '<button class="btn btn-ghost btn-sm" onclick="copyToClipboard(\\''+r.cid+'\\')">Kopieren</button>' : ''}
|
|
</div>
|
|
`).join('');
|
|
|
|
setTimeout(() => {
|
|
progressDiv.style.display = 'none';
|
|
barDiv.style.width = '0%';
|
|
}, 2000);
|
|
}
|
|
|
|
async function exploreDsmsCid() {
|
|
const cid = document.getElementById('webui-explore-cid').value.trim();
|
|
if (!cid) return;
|
|
|
|
const resultDiv = document.getElementById('dsms-explore-result');
|
|
const contentDiv = document.getElementById('dsms-explore-content');
|
|
|
|
resultDiv.style.display = 'block';
|
|
contentDiv.innerHTML = '<div class="admin-loading">Lade...</div>';
|
|
|
|
try {
|
|
const res = await fetch(DSMS_GATEWAY_URL + '/api/v1/verify/' + cid);
|
|
const data = await res.json();
|
|
|
|
if (data.exists) {
|
|
contentDiv.innerHTML = `
|
|
<div style="margin-bottom: 16px;">
|
|
<span style="font-size: 24px; ${data.integrity_valid ? 'color: var(--bp-accent);' : 'color: var(--bp-danger);'}">
|
|
${data.integrity_valid ? '✓' : '✗'}
|
|
</span>
|
|
<span style="font-weight: 600; margin-left: 8px;">
|
|
${data.integrity_valid ? 'Dokument verifiziert' : 'Integritätsfehler'}
|
|
</span>
|
|
</div>
|
|
<table style="width: 100%; font-size: 13px;">
|
|
<tr>
|
|
<td style="padding: 8px 0; color: var(--bp-text-muted); width: 150px;">CID</td>
|
|
<td style="padding: 8px 0; font-family: monospace; word-break: break-all;">${cid}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; color: var(--bp-text-muted);">Typ</td>
|
|
<td style="padding: 8px 0;">${data.metadata?.document_type || '--'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; color: var(--bp-text-muted);">Erstellt</td>
|
|
<td style="padding: 8px 0;">${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '--'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="padding: 8px 0; color: var(--bp-text-muted);">Checksum</td>
|
|
<td style="padding: 8px 0; font-family: monospace; font-size: 11px; word-break: break-all;">${data.stored_checksum || '--'}</td>
|
|
</tr>
|
|
</table>
|
|
<div style="margin-top: 16px;">
|
|
<a href="http://localhost:8085/ipfs/${cid}" target="_blank" class="btn btn-ghost btn-sm">Im Gateway öffnen</a>
|
|
</div>
|
|
`;
|
|
} else {
|
|
contentDiv.innerHTML = `
|
|
<div class="dsms-verify-error">
|
|
<strong>Nicht gefunden</strong><br>
|
|
CID existiert nicht im DSMS: ${cid}
|
|
</div>
|
|
`;
|
|
}
|
|
} catch (e) {
|
|
contentDiv.innerHTML = `
|
|
<div class="dsms-verify-error">
|
|
<strong>Fehler</strong><br>
|
|
${e.message}
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
async function runDsmsGarbageCollection() {
|
|
if (!confirm('Garbage Collection ausführen? Dies entfernt nicht gepinnte Objekte.')) return;
|
|
|
|
try {
|
|
// Note: Direct GC requires IPFS API access - show info for now
|
|
alert('Garbage Collection wird im Hintergrund ausgeführt. Dies kann einige Minuten dauern.');
|
|
} catch (e) {
|
|
alert('Fehler: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// Close modal on escape key
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeDsmsWebUI();
|
|
}
|
|
});
|
|
|
|
// Close modal on backdrop click
|
|
document.getElementById('dsms-webui-modal')?.addEventListener('click', (e) => {
|
|
if (e.target.id === 'dsms-webui-modal') {
|
|
closeDsmsWebUI();
|
|
}
|
|
});
|
|
"""
|