diff --git a/crates/pinakes-ui/assets/css/main.css b/crates/pinakes-ui/assets/css/main.css index d25d383..149dd65 100644 --- a/crates/pinakes-ui/assets/css/main.css +++ b/crates/pinakes-ui/assets/css/main.css @@ -1 +1,4627 @@ -@media (prefers-reduced-motion: reduce){*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important}}*{margin:0;padding:0;box-sizing:border-box;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.06) rgba(0,0,0,0)}*::-webkit-scrollbar{width:5px;height:5px}*::-webkit-scrollbar-track{background:rgba(0,0,0,0)}*::-webkit-scrollbar-thumb{background:rgba(255,255,255,.06);border-radius:3px}*::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.14)}:root{--bg-0: #111118;--bg-1: #18181f;--bg-2: #1f1f28;--bg-3: #26263a;--border-subtle: rgba(255,255,255,.06);--border: rgba(255,255,255,.09);--border-strong: rgba(255,255,255,.14);--text-0: #dcdce4;--text-1: #a0a0b8;--text-2: #6c6c84;--accent: #7c7ef5;--accent-dim: rgba(124,126,245,.15);--accent-text: #9698f7;--success: #3ec97a;--error: #e45858;--warning: #d4a037;--radius-sm: 3px;--radius: 5px;--radius-md: 7px;--shadow-sm: 0 1px 3px rgba(0,0,0,.3);--shadow: 0 2px 8px rgba(0,0,0,.35);--shadow-lg: 0 4px 20px rgba(0,0,0,.45)}body{font-family:"Inter",-apple-system,"Segoe UI",system-ui,sans-serif;background:var(--bg-0);color:var(--text-0);font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden}:focus-visible:focus-visible{outline:2px solid #7c7ef5;outline-offset:2px}::selection{background:rgba(124,126,245,.15);color:#9698f7}a{color:#9698f7;text-decoration:none}a:hover{text-decoration:underline}code{padding:1px 5px;border-radius:3px;background:#111118;color:#9698f7;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:11px}ul{list-style:none;padding:0}ul li{padding:3px 0;font-size:12px;color:#a0a0b8}.text-muted{color:#a0a0b8}.text-sm{font-size:11px}.mono{font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:12px}.flex-row{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px}.flex-between{display:flex;justify-content:space-between;align-items:center}.mb-16{margin-bottom:16px}.mb-8{margin-bottom:12px}.mt-16{margin-top:16px}.mt-8{margin-top:12px}@keyframes fade-in{from{opacity:0}to{opacity:1}}@keyframes slide-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}@keyframes pulse{0%, 100%{opacity:1}50%{opacity:.3}}@keyframes spin{to{transform:rotate(360deg)}}@keyframes skeleton-pulse{0%{opacity:.6}50%{opacity:.3}100%{opacity:.6}}@keyframes indeterminate{0%{transform:translateX(-100%)}100%{transform:translateX(400%)}}.app{display:flex;flex-direction:row;justify-content:flex-start;align-items:stretch;height:100vh;overflow:hidden}.sidebar{width:220px;min-width:220px;max-width:220px;background:#18181f;border-right:1px solid rgba(255,255,255,.09);display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;flex-shrink:0;user-select:none;overflow-y:auto;overflow-x:hidden;z-index:10;transition:width .15s,min-width .15s,max-width .15s}.sidebar.collapsed{width:48px;min-width:48px;max-width:48px}.sidebar.collapsed .nav-label,.sidebar.collapsed .sidebar-header .logo,.sidebar.collapsed .sidebar-header .version,.sidebar.collapsed .nav-badge,.sidebar.collapsed .nav-item-text,.sidebar.collapsed .sidebar-footer .status-text,.sidebar.collapsed .user-name,.sidebar.collapsed .role-badge,.sidebar.collapsed .user-info .btn,.sidebar.collapsed .sidebar-import-header span,.sidebar.collapsed .sidebar-import-file{display:none}.sidebar.collapsed .nav-item{justify-content:center;padding:8px;border-left:none;border-radius:3px}.sidebar.collapsed .nav-item.active{border-left:none}.sidebar.collapsed .nav-icon{width:auto;margin:0}.sidebar.collapsed .sidebar-header{padding:12px 8px;justify-content:center}.sidebar.collapsed .nav-section{padding:0 4px}.sidebar.collapsed .sidebar-footer{padding:8px}.sidebar.collapsed .sidebar-footer .user-info{justify-content:center;padding:4px}.sidebar.collapsed .sidebar-import-progress{padding:6px}.sidebar-header{padding:16px 16px 20px;display:flex;flex-direction:row;justify-content:flex-start;align-items:baseline;gap:8px}.sidebar-header .logo{font-size:15px;font-weight:700;letter-spacing:-.4px;color:#dcdce4}.sidebar-header .version{font-size:10px;color:#6c6c84}.sidebar-toggle{background:rgba(0,0,0,0);border:none;color:#6c6c84;padding:8px;font-size:18px;width:100%;text-align:center}.sidebar-toggle:hover{color:#dcdce4}.sidebar-spacer{flex:1}.sidebar-footer{padding:12px;border-top:1px solid rgba(255,255,255,.06);overflow:visible;min-width:0}.nav-section{padding:0 8px;margin-bottom:2px}.nav-label{padding:8px 8px 4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#6c6c84}.nav-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:6px 8px;border-radius:3px;cursor:pointer;color:#a0a0b8;font-size:13px;font-weight:450;transition:color .1s,background .1s;border:none;background:none;width:100%;text-align:left;border-left:2px solid rgba(0,0,0,0);margin-left:0}.nav-item:hover{color:#dcdce4;background:rgba(255,255,255,.03)}.nav-item.active{color:#9698f7;border-left-color:#7c7ef5;background:rgba(124,126,245,.15)}.nav-item-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.sidebar:not(.collapsed) .nav-item-text{overflow:visible}.nav-icon{width:18px;text-align:center;font-size:14px;opacity:.7}.nav-badge{margin-left:auto;font-size:10px;font-weight:600;color:#6c6c84;background:#26263a;padding:1px 6px;border-radius:12px;min-width:20px;text-align:center;font-variant-numeric:tabular-nums}.status-indicator{display:flex;flex-direction:row;justify-content:center;align-items:center;gap:6px;font-size:11px;font-weight:500;min-width:0;overflow:visible}.sidebar:not(.collapsed) .status-indicator{justify-content:flex-start}.status-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}.status-dot.connected{background:#3ec97a}.status-dot.disconnected{background:#e45858}.status-dot.checking{background:#d4a037;animation:pulse 1.5s infinite}.status-text{color:#6c6c84;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.sidebar:not(.collapsed) .status-text{overflow:visible}.main{flex:1;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;overflow:hidden;min-width:0}.header{height:48px;min-height:48px;border-bottom:1px solid rgba(255,255,255,.06);display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px;padding:0 20px;background:#18181f}.page-title{font-size:14px;font-weight:600;color:#dcdce4}.header-spacer{flex:1}.content{flex:1;overflow-y:auto;padding:20px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.06) rgba(0,0,0,0)}.sidebar-import-progress{padding:10px 12px;background:#1f1f28;border-top:1px solid rgba(255,255,255,.06);font-size:11px}.sidebar-import-header{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;margin-bottom:4px;color:#a0a0b8}.sidebar-import-file{color:#6c6c84;font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:4px}.sidebar-import-progress .progress-bar{height:3px}.user-info{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;font-size:12px;overflow:hidden;min-width:0}.user-name{font-weight:500;color:#dcdce4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:90px;flex-shrink:1}.role-badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600;text-transform:uppercase}.role-badge.role-admin{background:rgba(139,92,246,.1);color:#9d8be0}.role-badge.role-editor{background:rgba(34,160,80,.1);color:#5cb97a}.role-badge.role-viewer{background:rgba(59,120,200,.1);color:#6ca0d4}.btn{padding:5px 12px;border-radius:3px;border:none;cursor:pointer;font-size:12px;font-weight:500;transition:all .1s;display:inline-flex;align-items:center;gap:5px;white-space:nowrap;line-height:1.5}.btn-primary{background:#7c7ef5;color:#fff}.btn-primary:hover{background:#8b8df7}.btn-secondary{background:#26263a;color:#dcdce4;border:1px solid rgba(255,255,255,.09)}.btn-secondary:hover{border-color:rgba(255,255,255,.14);background:rgba(255,255,255,.06)}.btn-danger{background:rgba(0,0,0,0);color:#e45858;border:1px solid rgba(228,88,88,.25)}.btn-danger:hover{background:rgba(228,88,88,.08)}.btn-ghost{background:rgba(0,0,0,0);border:none;color:#a0a0b8;padding:5px 8px}.btn-ghost:hover{color:#dcdce4;background:rgba(255,255,255,.04)}.btn-sm{padding:3px 8px;font-size:11px}.btn-icon{padding:4px;border-radius:3px;background:rgba(0,0,0,0);border:none;color:#6c6c84;cursor:pointer;transition:color .1s;font-size:13px}.btn-icon:hover{color:#dcdce4}.btn:disabled,.btn[disabled]{opacity:.4;cursor:not-allowed;pointer-events:none}.btn.btn-disabled-hint:disabled{opacity:.6;border-style:dashed;pointer-events:auto;cursor:help}.card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:16px}.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.card-title{font-size:14px;font-weight:600}.card-body{padding-top:8px}.data-table{width:100%;border-collapse:collapse;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;overflow:hidden}.data-table thead th{padding:8px 14px;text-align:left;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:#6c6c84;border-bottom:1px solid rgba(255,255,255,.09);background:#26263a}.data-table tbody td{padding:8px 14px;font-size:13px;border-bottom:1px solid rgba(255,255,255,.06);max-width:300px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.data-table tbody tr{cursor:pointer;transition:background .08s}.data-table tbody tr:hover{background:rgba(255,255,255,.02)}.data-table tbody tr.row-selected{background:rgba(99,102,241,.12)}.data-table tbody tr:last-child td{border-bottom:none}.sortable-header{cursor:pointer;user-select:none;transition:color .1s}.sortable-header:hover{color:#9698f7}input[type=text],textarea,select{padding:6px 10px;border-radius:3px;border:1px solid rgba(255,255,255,.09);background:#111118;color:#dcdce4;font-size:13px;outline:none;transition:border-color .15s;font-family:inherit}input[type=text]::placeholder,textarea::placeholder,select::placeholder{color:#6c6c84}input[type=text]:focus,textarea:focus,select:focus{border-color:#7c7ef5}input[type=text][type=number],textarea[type=number],select[type=number]{width:80px;padding:6px 8px;-moz-appearance:textfield}input[type=text][type=number]::-webkit-outer-spin-button,input[type=text][type=number]::-webkit-inner-spin-button,textarea[type=number]::-webkit-outer-spin-button,textarea[type=number]::-webkit-inner-spin-button,select[type=number]::-webkit-outer-spin-button,select[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}textarea{min-height:64px;resize:vertical}select{appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath fill='%236c6c84' d='M5 7L1 3h8z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;padding-right:26px;min-width:100px}.form-group{margin-bottom:12px}.form-label{display:block;font-size:11px;font-weight:600;color:#a0a0b8;margin-bottom:4px;text-transform:uppercase;letter-spacing:.03em}.form-row{display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-end;gap:8px}.form-row input[type=text]{flex:1}.form-label-row{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:2px;margin-bottom:4px}.form-label-row .form-label{margin-bottom:0}.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:50%;font-size:9px;font-weight:600;letter-spacing:.5px;text-transform:uppercase}.badge-neutral{background:rgba(255,255,255,.04);color:#a0a0b8}.badge-success{background:rgba(62,201,122,.08);color:#3ec97a}.badge-warning{background:rgba(212,160,55,.06);color:#d4a037}.badge-danger{background:rgba(228,88,88,.06);color:#d47070}.tab-bar{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:4px;border-bottom:1px solid rgba(255,255,255,.09);margin-bottom:12px}.tab-btn{background:rgba(0,0,0,0);border:none;padding:6px 8px;font-size:13px;color:#a0a0b8;border-bottom:2px solid rgba(0,0,0,0);margin-bottom:-1px;cursor:pointer;transition:color .1s,border-color .1s}.tab-btn:hover{color:#dcdce4}.tab-btn.active{color:#9698f7;border-bottom-color:#7c7ef5}.input-sm{padding:6px 10px;border-radius:3px;border:1px solid rgba(255,255,255,.09);background:#111118;color:#dcdce4;font-size:13px;outline:none;transition:border-color .15s;font-family:inherit;padding:3px 8px;font-size:12px}.input-sm::placeholder{color:#6c6c84}.input-sm:focus{border-color:#7c7ef5}.input-suffix{display:flex;flex-direction:row;justify-content:flex-start;align-items:stretch}.input-suffix input{border-radius:3px 0 0 3px;border-right:none;flex:1}.input-suffix .btn{border-radius:0 3px 3px 0}.field-error{color:#d47070;font-size:11px;margin-top:2px}.comments-list{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:8px}.comment-item{padding:8px;background:#18181f;border-radius:5px;border:1px solid rgba(255,255,255,.06)}.comment-text{font-size:13px;color:#dcdce4;line-height:1.5;margin-top:4px}input[type=checkbox]{appearance:none;-webkit-appearance:none;width:16px;height:16px;border:1px solid rgba(255,255,255,.14);border-radius:3px;background:#1f1f28;cursor:pointer;position:relative;flex-shrink:0;transition:all .15s ease}input[type=checkbox]:hover{border-color:#7c7ef5;background:#26263a}input[type=checkbox]:checked{background:#7c7ef5;border-color:#7c7ef5}input[type=checkbox]:checked::after{content:"";position:absolute;left:5px;top:2px;width:4px;height:8px;border:solid #111118;border-width:0 2px 2px 0;transform:rotate(45deg)}input[type=checkbox]:focus-visible:focus-visible{outline:2px solid #7c7ef5;outline-offset:2px}.checkbox-label{display:inline-flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:#a0a0b8;user-select:none}.checkbox-label:hover{color:#dcdce4}.checkbox-label input[type=checkbox]{margin:0}.toggle{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:#dcdce4}.toggle.disabled{opacity:.4;cursor:not-allowed}.toggle-track{width:32px;height:18px;border-radius:9px;background:#26263a;border:1px solid rgba(255,255,255,.09);position:relative;transition:background .15s;flex-shrink:0}.toggle-track.active{background:#7c7ef5;border-color:#7c7ef5}.toggle-track.active .toggle-thumb{transform:translateX(14px)}.toggle-thumb{width:14px;height:14px;border-radius:50%;background:#dcdce4;position:absolute;top:1px;left:1px;transition:transform .15s}.filter-bar{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:12px;padding:12px;background:#111118;border:1px solid rgba(255,255,255,.06);border-radius:5px;margin-bottom:12px}.filter-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.filter-label{font-size:11px;font-weight:500;color:#6c6c84;text-transform:uppercase;letter-spacing:.5px;margin-right:4px}.filter-chip{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:14px;cursor:pointer;font-size:11px;color:#a0a0b8;transition:all .15s ease;user-select:none}.filter-chip:hover{background:#26263a;border-color:rgba(255,255,255,.14);color:#dcdce4}.filter-chip.active{background:rgba(124,126,245,.15);border-color:#7c7ef5;color:#9698f7}.filter-chip input[type=checkbox]{width:12px;height:12px;margin:0}.filter-chip input[type=checkbox]:checked::after{left:3px;top:1px;width:3px;height:6px}.filter-group{display:flex;align-items:center;gap:6px}.filter-group label{display:flex;align-items:center;gap:3px;cursor:pointer;color:#a0a0b8;font-size:11px;white-space:nowrap}.filter-group label:hover{color:#dcdce4}.filter-separator{width:1px;height:20px;background:rgba(255,255,255,.09);flex-shrink:0}.view-toggle{display:flex;border:1px solid rgba(255,255,255,.09);border-radius:3px;overflow:hidden}.view-btn{padding:4px 10px;background:#1f1f28;border:none;color:#6c6c84;cursor:pointer;font-size:18px;line-height:1;transition:background .1s,color .1s}.view-btn:first-child{border-right:1px solid rgba(255,255,255,.09)}.view-btn:hover{color:#dcdce4;background:#26263a}.view-btn.active{background:rgba(124,126,245,.15);color:#9698f7}.breadcrumb{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:4px;padding:10px 16px;font-size:.85rem;color:#6c6c84}.breadcrumb-sep{color:#6c6c84;opacity:.5}.breadcrumb-link{color:#9698f7;text-decoration:none;cursor:pointer}.breadcrumb-link:hover{text-decoration:underline}.breadcrumb-current{color:#dcdce4;font-weight:500}.progress-bar{width:100%;height:8px;background:#26263a;border-radius:4px;overflow:hidden;margin-bottom:6px}.progress-fill{height:100%;background:#7c7ef5;border-radius:4px;transition:width .3s ease}.progress-fill.indeterminate{width:30%;animation:indeterminate 1.5s ease-in-out infinite}.loading-overlay{display:flex;align-items:center;justify-content:center;padding:48px 16px;color:#6c6c84;font-size:13px;gap:10px}.spinner{width:18px;height:18px;border:2px solid rgba(255,255,255,.09);border-top-color:#7c7ef5;border-radius:50%;animation:spin .7s linear infinite}.spinner-small{width:14px;height:14px;border:2px solid rgba(255,255,255,.09);border-top-color:#7c7ef5;border-radius:50%;animation:spin .7s linear infinite}.spinner-tiny{width:10px;height:10px;border:1.5px solid rgba(255,255,255,.09);border-top-color:#7c7ef5;border-radius:50%;animation:spin .7s linear infinite}.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:100;animation:fade-in .1s ease-out}.modal{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;padding:20px;min-width:360px;max-width:480px;box-shadow:0 4px 20px rgba(0,0,0,.45)}.modal.wide{max-width:600px;max-height:70vh;overflow-y:auto}.modal-title{font-size:15px;font-weight:600;margin-bottom:6px}.modal-body{font-size:12px;color:#a0a0b8;margin-bottom:16px;line-height:1.5}.modal-actions{display:flex;flex-direction:row;justify-content:flex-end;align-items:center;gap:6px}.tooltip-trigger{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border-radius:50%;background:#26263a;color:#6c6c84;font-size:9px;font-weight:700;cursor:help;position:relative;flex-shrink:0;margin-left:4px}.tooltip-trigger:hover{background:rgba(124,126,245,.15);color:#9698f7}.tooltip-trigger:hover .tooltip-text{display:block}.tooltip-text{display:none;position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);padding:6px 10px;background:#26263a;border:1px solid rgba(255,255,255,.09);border-radius:3px;color:#dcdce4;font-size:11px;font-weight:400;line-height:1.4;white-space:normal;width:220px;text-transform:none;letter-spacing:normal;box-shadow:0 2px 8px rgba(0,0,0,.35);z-index:100;pointer-events:none}.media-player{position:relative;background:#111118;border-radius:5px;overflow:hidden}.media-player:focus{outline:none}.media-player-audio .player-artwork{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:8px;padding:24px 16px 8px}.player-native-video{width:100%;display:block}.player-native-audio{display:none}.player-artwork img{max-width:200px;max-height:200px;border-radius:5px;object-fit:cover}.player-artwork-placeholder{width:120px;height:120px;display:flex;align-items:center;justify-content:center;background:#1f1f28;border-radius:5px;font-size:48px;opacity:.3}.player-title{font-size:13px;font-weight:500;color:#dcdce4;text-align:center}.player-controls{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:10px 14px;background:#1f1f28}.media-player-video .player-controls{position:absolute;bottom:0;left:0;right:0;background:rgba(0,0,0,.7);opacity:0;transition:opacity .2s}.media-player-video:hover .player-controls{opacity:1}.play-btn,.mute-btn,.fullscreen-btn{background:none;border:none;color:#dcdce4;cursor:pointer;font-size:18px;padding:4px;line-height:1;transition:color .1s}.play-btn:hover,.mute-btn:hover,.fullscreen-btn:hover{color:#9698f7}.player-time{font-size:11px;color:#6c6c84;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;min-width:36px;text-align:center;user-select:none}.seek-bar{flex:1;-webkit-appearance:none;appearance:none;height:4px;border-radius:4px;background:#26263a;outline:none;cursor:pointer}.seek-bar::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:12px;height:12px;border-radius:50%;background:#7c7ef5;cursor:pointer;border:none}.seek-bar::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:#7c7ef5;cursor:pointer;border:none}.volume-slider{width:70px;-webkit-appearance:none;appearance:none;height:4px;border-radius:4px;background:#26263a;outline:none;cursor:pointer}.volume-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:10px;height:10px;border-radius:50%;background:#a0a0b8;cursor:pointer;border:none}.volume-slider::-moz-range-thumb{width:10px;height:10px;border-radius:50%;background:#a0a0b8;cursor:pointer;border:none}.image-viewer-overlay{position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:150;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;animation:fade-in .15s ease-out}.image-viewer-overlay:focus{outline:none}.image-viewer-toolbar{display:flex;justify-content:space-between;align-items:center;padding:10px 16px;background:rgba(0,0,0,.5);border-bottom:1px solid rgba(255,255,255,.08);z-index:2;user-select:none}.image-viewer-toolbar-left,.image-viewer-toolbar-center,.image-viewer-toolbar-right{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px}.iv-btn{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:#dcdce4;border-radius:3px;padding:4px 10px;font-size:12px;cursor:pointer;transition:background .1s}.iv-btn:hover{background:rgba(255,255,255,.12)}.iv-btn.iv-close{color:#e45858;font-weight:600}.iv-zoom-label{font-size:11px;color:#a0a0b8;min-width:40px;text-align:center;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace}.image-viewer-canvas{flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}.image-viewer-canvas img{max-width:100%;max-height:100%;object-fit:contain;user-select:none;-webkit-user-drag:none}.pdf-viewer{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;height:100%;min-height:500px;background:#111118;border-radius:5px;overflow:hidden}.pdf-toolbar{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px;padding:10px 12px;background:#18181f;border-bottom:1px solid rgba(255,255,255,.09)}.pdf-toolbar-group{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:4px}.pdf-toolbar-btn{display:flex;align-items:center;justify-content:center;width:28px;height:28px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:3px;color:#a0a0b8;font-size:14px;cursor:pointer;transition:all .15s}.pdf-toolbar-btn:hover:not(:disabled){background:#26263a;color:#dcdce4}.pdf-toolbar-btn:disabled{opacity:.4;cursor:not-allowed}.pdf-zoom-label{min-width:45px;text-align:center;font-size:12px;color:#a0a0b8}.pdf-container{flex:1;position:relative;overflow:hidden;background:#1f1f28}.pdf-object{width:100%;height:100%;border:none}.pdf-loading,.pdf-error{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:12px;background:#18181f;color:#a0a0b8}.pdf-error{padding:12px;text-align:center}.pdf-fallback{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:16px;padding:48px 12px;text-align:center;color:#6c6c84}.markdown-viewer{padding:16px;text-align:left;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:12px}.markdown-toolbar{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:8px;background:#1f1f28;border-radius:5px;border:1px solid rgba(255,255,255,.09)}.toolbar-btn{padding:6px 12px;border:1px solid rgba(255,255,255,.09);border-radius:3px;background:#18181f;color:#a0a0b8;font-size:13px;font-weight:500;cursor:pointer;transition:all .15s}.toolbar-btn:hover{background:#26263a;border-color:rgba(255,255,255,.14)}.toolbar-btn.active{background:#7c7ef5;color:#fff;border-color:#7c7ef5}.markdown-source{max-width:100%;background:#26263a;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:16px;overflow-x:auto;font-family:"Menlo","Monaco","Courier New",monospace;font-size:13px;line-height:1.7;color:#dcdce4;white-space:pre-wrap;word-wrap:break-word}.markdown-source code{font-family:inherit;background:none;padding:0;border:none}.markdown-content{max-width:800px;color:#dcdce4;line-height:1.7;font-size:14px;text-align:left}.markdown-content h1{font-size:1.8em;font-weight:700;margin:1em 0 .5em;border-bottom:1px solid rgba(255,255,255,.06);padding-bottom:.3em}.markdown-content h2{font-size:1.5em;font-weight:600;margin:.8em 0 .4em;border-bottom:1px solid rgba(255,255,255,.06);padding-bottom:.2em}.markdown-content h3{font-size:1.25em;font-weight:600;margin:.6em 0 .3em}.markdown-content h4{font-size:1.1em;font-weight:600;margin:.5em 0 .25em}.markdown-content h5,.markdown-content h6{font-size:1em;font-weight:600;margin:.4em 0 .2em;color:#a0a0b8}.markdown-content p{margin:0 0 1em}.markdown-content a{color:#7c7ef5;text-decoration:none}.markdown-content a:hover{text-decoration:underline}.markdown-content pre{background:#26263a;border:1px solid rgba(255,255,255,.09);border-radius:3px;padding:12px 16px;overflow-x:auto;margin:0 0 1em;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:12px;line-height:1.5}.markdown-content code{background:#26263a;padding:1px 5px;border-radius:3px;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:.9em}.markdown-content pre code{background:none;padding:0}.markdown-content blockquote{border-left:3px solid #7c7ef5;padding:4px 16px;margin:0 0 1em;color:#a0a0b8;background:rgba(124,126,245,.04)}.markdown-content table{width:100%;border-collapse:collapse;margin:0 0 1em}.markdown-content th,.markdown-content td{padding:6px 12px;border:1px solid rgba(255,255,255,.09);font-size:13px}.markdown-content th{background:#26263a;font-weight:600;text-align:left}.markdown-content tr:nth-child(even){background:#1f1f28}.markdown-content ul,.markdown-content ol{margin:0 0 1em;padding-left:16px}.markdown-content ul{list-style:disc}.markdown-content ol{list-style:decimal}.markdown-content li{padding:2px 0;font-size:14px;color:#dcdce4}.markdown-content hr{border:none;border-top:1px solid rgba(255,255,255,.09);margin:1.5em 0}.markdown-content img{max-width:100%;border-radius:5px}.markdown-content .footnote-definition{font-size:.85em;color:#a0a0b8;margin-top:.5em;padding-left:1.5em}.markdown-content .footnote-definition sup{color:#7c7ef5;margin-right:4px}.markdown-content sup a{color:#7c7ef5;text-decoration:none;font-size:.8em}.wikilink{color:#9698f7;text-decoration:none;border-bottom:1px dashed #7c7ef5;cursor:pointer;transition:border-color .1s,color .1s}.wikilink:hover{color:#7c7ef5;border-bottom-style:solid}.wikilink-embed{display:inline-block;padding:2px 8px;background:rgba(139,92,246,.08);border:1px dashed rgba(139,92,246,.3);border-radius:3px;color:#9d8be0;font-size:12px;cursor:default}.media-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));gap:12px}.media-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;overflow:hidden;cursor:pointer;transition:border-color .12s,box-shadow .12s;position:relative}.media-card:hover{border-color:rgba(255,255,255,.14);box-shadow:0 1px 3px rgba(0,0,0,.3)}.media-card.selected{border-color:#7c7ef5;box-shadow:0 0 0 1px #7c7ef5}.card-checkbox{position:absolute;top:6px;left:6px;z-index:2;opacity:0;transition:opacity .1s}.card-checkbox input[type=checkbox]{width:16px;height:16px;cursor:pointer;filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))}.media-card:hover .card-checkbox,.media-card.selected .card-checkbox{opacity:1}.card-thumbnail{width:100%;aspect-ratio:1;background:#111118;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}.card-thumbnail img,.card-thumbnail .card-thumb-img{width:100%;height:100%;object-fit:cover;position:absolute;top:0;left:0;z-index:1}.card-type-icon{font-size:32px;opacity:.4;display:flex;align-items:center;justify-content:center;width:100%;height:100%;position:absolute;top:0;left:0;z-index:0}.card-info{padding:8px 10px}.card-name{font-size:12px;font-weight:500;color:#dcdce4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:4px}.card-title,.card-artist{font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.3}.card-meta{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;font-size:10px}.card-size{color:#6c6c84;font-size:10px}.table-thumb-cell{width:36px;padding:4px 6px !important;position:relative}.table-thumb{width:28px;height:28px;object-fit:cover;border-radius:3px;display:block}.table-thumb-overlay{position:absolute;top:4px;left:6px;z-index:1}.table-type-icon{display:flex;align-items:center;justify-content:center;width:28px;height:28px;font-size:14px;opacity:.5;border-radius:3px;background:#111118;z-index:0}.type-badge{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}.type-badge.type-audio{background:rgba(139,92,246,.1);color:#9d8be0}.type-badge.type-video{background:rgba(200,72,130,.1);color:#d07eaa}.type-badge.type-image{background:rgba(34,160,80,.1);color:#5cb97a}.type-badge.type-document{background:rgba(59,120,200,.1);color:#6ca0d4}.type-badge.type-text{background:rgba(200,160,36,.1);color:#c4a840}.type-badge.type-other{background:rgba(128,128,160,.08);color:#6c6c84}.tag-list{display:flex;flex-wrap:wrap;gap:4px}.tag-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 10px;background:rgba(124,126,245,.15);color:#9698f7;border-radius:12px;font-size:11px;font-weight:500}.tag-badge.selected{background:#7c7ef5;color:#fff;cursor:pointer}.tag-badge:not(.selected){cursor:pointer}.tag-badge .tag-remove{cursor:pointer;opacity:.4;font-size:13px;line-height:1;transition:opacity .1s}.tag-badge .tag-remove:hover{opacity:1}.tag-group{margin-bottom:6px}.tag-children{margin-left:16px;margin-top:4px;display:flex;flex-wrap:wrap;gap:4px}.tag-confirm-delete{display:inline-flex;align-items:center;gap:4px;font-size:10px;color:#a0a0b8}.tag-confirm-yes{cursor:pointer;color:#e45858;font-weight:600}.tag-confirm-yes:hover{text-decoration:underline}.tag-confirm-no{cursor:pointer;color:#6c6c84;font-weight:500}.tag-confirm-no:hover{text-decoration:underline}.detail-actions{display:flex;gap:6px;margin-bottom:16px}.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.detail-field{padding:10px 12px;background:#111118;border-radius:3px;border:1px solid rgba(255,255,255,.06)}.detail-field.full-width{grid-column:1/-1}.detail-field input[type=text],.detail-field textarea,.detail-field select{width:100%;margin-top:4px}.detail-field textarea{min-height:64px;resize:vertical}.detail-label{font-size:10px;font-weight:600;color:#6c6c84;text-transform:uppercase;letter-spacing:.04em;margin-bottom:2px}.detail-value{font-size:13px;color:#dcdce4;word-break:break-all}.detail-value.mono{font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:11px;color:#a0a0b8}.detail-preview{margin-bottom:16px;background:#111118;border:1px solid rgba(255,255,255,.06);border-radius:5px;overflow:hidden;text-align:center}.detail-preview:has(.markdown-viewer){max-height:none;overflow-y:auto;text-align:left}.detail-preview:not(:has(.markdown-viewer)){max-height:450px}.detail-preview img{max-width:100%;max-height:400px;object-fit:contain;display:block;margin:0 auto}.detail-preview audio{width:100%;padding:16px}.detail-preview video{max-width:100%;max-height:400px;display:block;margin:0 auto}.detail-no-preview{padding:16px 16px;text-align:center;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:10px}.frontmatter-card{max-width:800px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:12px 16px;margin-bottom:16px}.frontmatter-fields{display:grid;grid-template-columns:auto 1fr;gap:4px 12px;margin:0}.frontmatter-fields dt{font-weight:600;font-size:12px;color:#a0a0b8;text-transform:capitalize}.frontmatter-fields dd{font-size:13px;color:#dcdce4;margin:0}.empty-state{text-align:center;padding:48px 12px;color:#6c6c84}.empty-state .empty-icon{font-size:32px;margin-bottom:12px;opacity:.3}.empty-title{font-size:15px;font-weight:600;color:#a0a0b8;margin-bottom:4px}.empty-subtitle{font-size:12px;max-width:320px;margin:0 auto;line-height:1.5}.toast-container{position:fixed;bottom:16px;right:16px;z-index:300;display:flex;flex-direction:column-reverse;gap:6px;align-items:flex-end}.toast-container .toast{position:static;transform:none}.toast{position:fixed;bottom:16px;right:16px;padding:10px 16px;border-radius:5px;background:#26263a;border:1px solid rgba(255,255,255,.09);color:#dcdce4;font-size:12px;box-shadow:0 2px 8px rgba(0,0,0,.35);z-index:300;animation:slide-up .15s ease-out;max-width:420px}.toast.success{border-left:3px solid #3ec97a}.toast.error{border-left:3px solid #e45858}.offline-banner,.error-banner{background:rgba(228,88,88,.06);border:1px solid rgba(228,88,88,.2);border-radius:3px;padding:10px 12px;margin-bottom:12px;font-size:12px;color:#d47070;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px}.offline-banner .offline-icon,.offline-banner .error-icon,.error-banner .offline-icon,.error-banner .error-icon{font-size:14px;flex-shrink:0}.error-banner{padding:10px 14px}.readonly-banner{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:10px 12px;background:rgba(212,160,55,.06);border:1px solid rgba(212,160,55,.15);border-radius:3px;margin-bottom:16px;font-size:12px;color:#d4a037}.batch-actions{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:8px 10px;background:rgba(124,126,245,.15);border:1px solid rgba(124,126,245,.2);border-radius:3px;margin-bottom:12px;font-size:12px;font-weight:500;color:#9698f7}.select-all-banner{display:flex;flex-direction:row;justify-content:center;align-items:center;gap:8px;padding:10px 16px;background:rgba(99,102,241,.08);border-radius:6px;margin-bottom:8px;font-size:.85rem;color:#a0a0b8}.select-all-banner button{background:none;border:none;color:#7c7ef5;cursor:pointer;font-weight:600;text-decoration:underline;font-size:.85rem;padding:0}.select-all-banner button:hover{color:#dcdce4}.import-status-panel{background:#1f1f28;border:1px solid #7c7ef5;border-radius:5px;padding:12px 16px;margin-bottom:16px}.import-status-header{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;margin-bottom:8px;font-size:13px;color:#dcdce4}.import-current-file{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:2px;margin-bottom:6px;font-size:12px;overflow:hidden}.import-file-label{color:#6c6c84;flex-shrink:0}.import-file-name{color:#dcdce4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:monospace;font-size:11px}.import-queue-indicator{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:2px;margin-bottom:8px;font-size:11px}.import-queue-badge{display:flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 6px;background:rgba(124,126,245,.15);color:#9698f7;border-radius:9px;font-weight:600;font-size:10px}.import-queue-text{color:#6c6c84}.import-tabs{display:flex;gap:0;margin-bottom:16px;border-bottom:1px solid rgba(255,255,255,.09)}.import-tab{padding:10px 16px;background:none;border:none;border-bottom:2px solid rgba(0,0,0,0);color:#6c6c84;font-size:12px;font-weight:500;cursor:pointer;transition:color .1s,border-color .1s}.import-tab:hover{color:#dcdce4}.import-tab.active{color:#9698f7;border-bottom-color:#7c7ef5}.queue-panel{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;border-left:1px solid rgba(255,255,255,.09);background:#18181f;min-width:280px;max-width:320px}.queue-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.06)}.queue-header h3{margin:0;font-size:.9rem;color:#dcdce4}.queue-controls{display:flex;gap:2px}.queue-list{overflow-y:auto;flex:1}.queue-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;padding:8px 16px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,.06);transition:background .15s}.queue-item:hover{background:#1f1f28}.queue-item:hover .queue-item-remove{opacity:1}.queue-item-active{background:rgba(124,126,245,.15);border-left:3px solid #7c7ef5}.queue-item-info{flex:1;min-width:0}.queue-item-title{display:block;font-size:.85rem;color:#dcdce4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.queue-item-artist{display:block;font-size:.75rem;color:#6c6c84}.queue-item-remove{opacity:0;transition:opacity .15s}.queue-empty{padding:16px 16px;text-align:center;color:#6c6c84;font-size:.85rem}.statistics-page{padding:20px}.stats-overview,.stats-grid{display:grid;grid-template-columns:repeat(3, 1fr);gap:16px;margin-bottom:24px}@media (max-width: 768px){.stats-overview,.stats-grid{grid-template-columns:1fr}}.stat-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:20px;display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:16px}.stat-card.stat-primary{border-left:3px solid #7c7ef5}.stat-card.stat-success{border-left:3px solid #3ec97a}.stat-card.stat-info{border-left:3px solid #6ca0d4}.stat-card.stat-warning{border-left:3px solid #d4a037}.stat-card.stat-purple{border-left:3px solid #9d8be0}.stat-card.stat-danger{border-left:3px solid #e45858}.stat-icon{flex-shrink:0;color:#6c6c84}.stat-content{flex:1}.stat-value{font-size:28px;font-weight:700;color:#dcdce4;line-height:1.2;font-variant-numeric:tabular-nums}.stat-label{font-size:12px;color:#6c6c84;margin-top:4px;font-weight:500}.stats-section{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:16px;margin-bottom:20px}.section-title{font-size:16px;font-weight:600;color:#dcdce4;margin-bottom:20px}.section-title.small{font-size:14px;margin-bottom:12px;padding-bottom:6px;border-bottom:1px solid rgba(255,255,255,.06)}.chart-bars{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:16px}.bar-item{display:grid;grid-template-columns:120px 1fr 80px;align-items:center;gap:16px}.bar-label{font-size:13px;font-weight:500;color:#a0a0b8;text-align:right}.bar-track{height:28px;background:#26263a;border-radius:3px;overflow:hidden;position:relative}.bar-fill{height:100%;transition:width .6s cubic-bezier(.4, 0, .2, 1);border-radius:3px}.bar-fill.bar-primary{background:linear-gradient(90deg, #7c7ef5 0%, #7c7ef3 100%)}.bar-fill.bar-success{background:linear-gradient(90deg, #3ec97a 0%, #66bb6a 100%)}.bar-value{font-size:13px;font-weight:600;color:#a0a0b8;text-align:right;font-variant-numeric:tabular-nums}.settings-section{margin-bottom:16px}.settings-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;padding:20px;margin-bottom:16px}.settings-card.danger-card{border:1px solid rgba(228,88,88,.25)}.settings-card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid rgba(255,255,255,.06)}.settings-card-title{font-size:14px;font-weight:600}.settings-card-body{padding-top:2px}.settings-field{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.06)}.settings-field:last-child{border-bottom:none}.settings-field select{min-width:120px}.config-path{font-size:11px;color:#6c6c84;margin-bottom:12px;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;padding:6px 10px;background:#111118;border-radius:3px;border:1px solid rgba(255,255,255,.06)}.config-status{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;padding:3px 10px;border-radius:12px;font-size:11px;font-weight:600}.config-status.writable{background:rgba(62,201,122,.1);color:#3ec97a}.config-status.readonly{background:rgba(228,88,88,.1);color:#e45858}.root-list{list-style:none}.root-item{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:#111118;border:1px solid rgba(255,255,255,.06);border-radius:3px;margin-bottom:4px;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:12px;color:#a0a0b8}.info-row{display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid rgba(255,255,255,.06);font-size:13px}.info-row:last-child{border-bottom:none}.info-label{color:#a0a0b8;font-weight:500}.info-value{color:#dcdce4}.tasks-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(400px, 1fr));gap:16px;padding:12px}.task-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;overflow:hidden;transition:all .2s}.task-card:hover{border-color:rgba(255,255,255,.14);box-shadow:0 4px 12px rgba(0,0,0,.08);transform:translateY(-2px)}.task-card-enabled{border-left:3px solid #3ec97a}.task-card-disabled{border-left:3px solid #4a4a5e;opacity:.7}.task-card-header{display:flex;justify-content:space-between;align-items:center;align-items:flex-start;padding:16px;border-bottom:1px solid rgba(255,255,255,.06)}.task-header-left{flex:1;min-width:0}.task-name{font-size:16px;font-weight:600;color:#dcdce4;margin-bottom:2px}.task-schedule{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;font-size:12px;color:#6c6c84;font-family:"Menlo","Monaco","Courier New",monospace}.schedule-icon{font-size:14px}.task-status-badge{flex-shrink:0}.status-badge{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;padding:2px 10px;border-radius:3px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}.status-badge.status-enabled{background:rgba(76,175,80,.12);color:#3ec97a}.status-badge.status-enabled .status-dot{animation:pulse 1.5s infinite}.status-badge.status-disabled{background:#26263a;color:#6c6c84}.status-badge .status-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;background:currentColor}.task-info-grid{display:grid;grid-template-columns:repeat(auto-fit, minmax(120px, 1fr));gap:12px;padding:16px}.task-info-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:flex-start;gap:10px}.task-info-icon{font-size:18px;color:#6c6c84;flex-shrink:0}.task-info-content{flex:1;min-width:0}.task-info-label{font-size:10px;color:#6c6c84;font-weight:600;text-transform:uppercase;letter-spacing:.03em;margin-bottom:2px}.task-info-value{font-size:12px;color:#a0a0b8;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.task-card-actions{display:flex;gap:8px;padding:10px 16px;background:#18181f;border-top:1px solid rgba(255,255,255,.06)}.task-card-actions button{flex:1}.db-actions{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:16px;padding:10px}.db-action-row{display:flex;flex-direction:row;justify-content:space-between;align-items:center;gap:16px;padding:10px;border-radius:6px;background:rgba(0,0,0,.06)}.db-action-info{flex:1}.db-action-info h4{font-size:.95rem;font-weight:600;color:#dcdce4;margin-bottom:2px}.db-action-confirm{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;flex-shrink:0}.library-toolbar{display:flex;justify-content:space-between;align-items:center;padding:8px 0;margin-bottom:12px;gap:12px;flex-wrap:wrap}.toolbar-left{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:10px}.toolbar-right{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:10px}.sort-control select,.page-size-control select{padding:4px 24px 4px 8px;font-size:11px;background:#1f1f28}.page-size-control{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:4px}.library-stats{display:flex;justify-content:space-between;align-items:center;padding:2px 0 6px 0;font-size:11px}.type-filter-row{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;padding:4px 0;margin-bottom:6px;flex-wrap:wrap}.pagination{display:flex;align-items:center;justify-content:center;gap:4px;margin-top:16px;padding:8px 0}.page-btn{min-width:28px;text-align:center;font-variant-numeric:tabular-nums}.page-ellipsis{color:#6c6c84;padding:0 4px;font-size:12px;user-select:none}.audit-controls{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;margin-bottom:12px}.filter-select{padding:4px 24px 4px 8px;font-size:11px;background:#1f1f28}.action-danger{background:rgba(228,88,88,.1);color:#d47070}.action-updated{background:rgba(59,120,200,.1);color:#6ca0d4}.action-collection{background:rgba(34,160,80,.1);color:#5cb97a}.action-collection-remove{background:rgba(212,160,55,.1);color:#c4a840}.action-opened{background:rgba(139,92,246,.1);color:#9d8be0}.action-scanned{background:rgba(128,128,160,.08);color:#6c6c84}.clickable{cursor:pointer;color:#9698f7}.clickable:hover{text-decoration:underline}.clickable-row{cursor:pointer}.clickable-row:hover{background:rgba(255,255,255,.03)}.duplicates-view{padding:0}.duplicates-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.duplicates-header h3{margin:0}.duplicates-summary{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px}.duplicate-group{border:1px solid rgba(255,255,255,.09);border-radius:5px;margin-bottom:8px;overflow:hidden}.duplicate-group-header{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px;width:100%;padding:10px 14px;background:#1f1f28;border:none;cursor:pointer;text-align:left;color:#dcdce4;font-size:13px}.duplicate-group-header:hover{background:#26263a}.expand-icon{font-size:10px;width:14px;flex-shrink:0}.group-name{font-weight:600;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.group-badge{background:#7c7ef5;color:#fff;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;flex-shrink:0}.group-size{flex-shrink:0;font-size:12px}.group-hash{font-size:11px;flex-shrink:0}.duplicate-items{border-top:1px solid rgba(255,255,255,.09)}.duplicate-item{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.06)}.duplicate-item:last-child{border-bottom:none}.duplicate-item-keep{background:rgba(76,175,80,.06)}.dup-thumb{width:48px;height:48px;flex-shrink:0;border-radius:3px;overflow:hidden}.dup-thumb-img{width:100%;height:100%;object-fit:cover}.dup-thumb-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#26263a;font-size:20px;color:#6c6c84}.dup-info{flex:1;min-width:0}.dup-filename{font-weight:600;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dup-path{font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dup-meta{font-size:12px;margin-top:2px}.dup-actions{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;flex-shrink:0}.keep-badge{background:rgba(76,175,80,.12);color:#4caf50;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600}.saved-searches-list{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:4px;max-height:300px;overflow-y:auto}.saved-search-item{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;background:#18181f;border-radius:3px;cursor:pointer;transition:background .15s ease}.saved-search-item:hover{background:#1f1f28}.saved-search-info{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:2px;flex:1;min-width:0}.saved-search-name{font-weight:500;color:#dcdce4}.saved-search-query{font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.backlinks-panel,.outgoing-links-panel{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;margin-top:16px;overflow:hidden}.backlinks-header,.outgoing-links-header{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:10px 14px;background:#26263a;cursor:pointer;user-select:none;transition:background .1s}.backlinks-header:hover,.outgoing-links-header:hover{background:rgba(255,255,255,.04)}.backlinks-toggle,.outgoing-links-toggle{font-size:10px;color:#6c6c84;width:12px;text-align:center}.backlinks-title,.outgoing-links-title{font-size:12px;font-weight:600;color:#dcdce4;flex:1}.backlinks-count,.outgoing-links-count{font-size:11px;color:#6c6c84}.backlinks-reindex-btn{display:flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;margin-left:auto;background:rgba(0,0,0,0);border:1px solid rgba(255,255,255,.09);border-radius:3px;color:#6c6c84;font-size:12px;cursor:pointer;transition:background .1s,color .1s,border-color .1s}.backlinks-reindex-btn:hover:not(:disabled){background:#1f1f28;color:#dcdce4;border-color:rgba(255,255,255,.14)}.backlinks-reindex-btn:disabled{opacity:.5;cursor:not-allowed}.backlinks-content,.outgoing-links-content{padding:12px;border-top:1px solid rgba(255,255,255,.06)}.backlinks-loading,.outgoing-links-loading{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;padding:12px;color:#6c6c84;font-size:12px}.backlinks-error,.outgoing-links-error{padding:8px 12px;background:rgba(228,88,88,.06);border:1px solid rgba(228,88,88,.2);border-radius:3px;font-size:12px;color:#e45858}.backlinks-empty,.outgoing-links-empty{padding:16px;text-align:center;color:#6c6c84;font-size:12px;font-style:italic}.backlinks-list,.outgoing-links-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:6px}.backlink-item,.outgoing-link-item{padding:10px 12px;background:#111118;border:1px solid rgba(255,255,255,.06);border-radius:3px;cursor:pointer;transition:background .1s,border-color .1s}.backlink-item:hover,.outgoing-link-item:hover{background:#18181f;border-color:rgba(255,255,255,.09)}.backlink-item.unresolved,.outgoing-link-item.unresolved{opacity:.7;border-style:dashed}.backlink-source,.outgoing-link-target{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;margin-bottom:2px}.backlink-title,.outgoing-link-text{font-size:13px;font-weight:500;color:#dcdce4;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.backlink-type-badge,.outgoing-link-type-badge{display:inline-block;padding:1px 6px;border-radius:12px;font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}.backlink-type-badge.backlink-type-wikilink,.backlink-type-badge.link-type-wikilink,.outgoing-link-type-badge.backlink-type-wikilink,.outgoing-link-type-badge.link-type-wikilink{background:rgba(124,126,245,.15);color:#9698f7}.backlink-type-badge.backlink-type-embed,.backlink-type-badge.link-type-embed,.outgoing-link-type-badge.backlink-type-embed,.outgoing-link-type-badge.link-type-embed{background:rgba(139,92,246,.1);color:#9d8be0}.backlink-type-badge.backlink-type-markdown_link,.backlink-type-badge.link-type-markdown_link,.outgoing-link-type-badge.backlink-type-markdown_link,.outgoing-link-type-badge.link-type-markdown_link{background:rgba(59,120,200,.1);color:#6ca0d4}.backlink-context{font-size:11px;color:#6c6c84;line-height:1.4;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.backlink-line{color:#a0a0b8;font-weight:500}.unresolved-badge{padding:1px 6px;border-radius:12px;font-size:9px;font-weight:600;background:rgba(212,160,55,.1);color:#d4a037}.outgoing-links-unresolved-badge{margin-left:8px;padding:2px 8px;border-radius:10px;font-size:10px;font-weight:500;background:rgba(212,160,55,.12);color:#d4a037}.outgoing-links-global-unresolved{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:6px;margin-top:12px;padding:10px 12px;background:rgba(212,160,55,.06);border:1px solid rgba(212,160,55,.15);border-radius:3px;font-size:11px;color:#6c6c84}.outgoing-links-global-unresolved .unresolved-icon{color:#d4a037}.backlinks-message{padding:8px 10px;margin-bottom:10px;border-radius:3px;font-size:11px}.backlinks-message.success{background:rgba(62,201,122,.08);border:1px solid rgba(62,201,122,.2);color:#3ec97a}.backlinks-message.error{background:rgba(228,88,88,.06);border:1px solid rgba(228,88,88,.2);color:#e45858}.graph-view{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;height:100%;background:#18181f;border-radius:5px;overflow:hidden}.graph-toolbar{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:16px;padding:12px 16px;background:#1f1f28;border-bottom:1px solid rgba(255,255,255,.09)}.graph-title{font-size:14px;font-weight:600;color:#dcdce4}.graph-controls{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:8px;font-size:12px;color:#a0a0b8}.graph-controls select{padding:4px 20px 4px 8px;font-size:11px;background:#26263a}.graph-stats{margin-left:auto;font-size:11px;color:#6c6c84}.graph-container{flex:1;position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#111118}.graph-loading,.graph-error,.graph-empty{display:flex;flex-direction:column;justify-content:center;align-items:center;gap:10px;padding:48px;color:#6c6c84;font-size:13px;text-align:center}.graph-svg{max-width:100%;max-height:100%;cursor:grab}.graph-svg-container{position:relative;width:100%;height:100%}.graph-zoom-controls{position:absolute;top:16px;left:16px;display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:8px;z-index:5}.zoom-btn{width:36px;height:36px;border-radius:6px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);color:#dcdce4;font-size:18px;font-weight:bold;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s;box-shadow:0 1px 3px rgba(0,0,0,.3)}.zoom-btn:hover{background:#26263a;border-color:rgba(255,255,255,.14);transform:scale(1.05)}.zoom-btn:active{transform:scale(.95)}.graph-edges line{stroke:rgba(255,255,255,.14);stroke-width:1;opacity:.6}.graph-edges line.edge-type-wikilink{stroke:#7c7ef5}.graph-edges line.edge-type-embed{stroke:#9d8be0;stroke-dasharray:4 2}.graph-nodes .graph-node{cursor:pointer}.graph-nodes .graph-node circle{fill:#4caf50;stroke:#388e3c;stroke-width:2;transition:fill .15s,stroke .15s}.graph-nodes .graph-node:hover circle{fill:#66bb6a}.graph-nodes .graph-node.selected circle{fill:#7c7ef5;stroke:#5456d6}.graph-nodes .graph-node text{fill:#a0a0b8;font-size:11px;pointer-events:none;text-anchor:middle;dominant-baseline:central;transform:translateY(16px)}.node-details-panel{position:absolute;top:16px;right:16px;width:280px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,.35);z-index:10}.node-details-header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.06)}.node-details-header h3{font-size:13px;font-weight:600;color:#dcdce4;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.node-details-header .close-btn{background:none;border:none;color:#6c6c84;cursor:pointer;font-size:14px;padding:2px 6px;line-height:1}.node-details-header .close-btn:hover{color:#dcdce4}.node-details-content{padding:14px}.node-details-content .node-title{font-size:12px;color:#a0a0b8;margin-bottom:12px}.node-stats{display:flex;gap:16px;margin-bottom:12px}.node-stats .stat{font-size:12px;color:#6c6c84}.node-stats .stat strong{color:#dcdce4}.physics-controls-panel{position:absolute;top:16px;right:16px;width:300px;background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:5px;box-shadow:0 2px 8px rgba(0,0,0,.35);padding:16px;z-index:10}.physics-controls-panel h4{font-size:13px;font-weight:600;color:#dcdce4;margin:0 0 16px 0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.06)}.physics-controls-panel .btn{width:100%;margin-top:8px}.control-group{margin-bottom:14px}.control-group label{display:block;font-size:11px;font-weight:500;color:#a0a0b8;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}.control-group input[type=range]{width:100%;height:4px;border-radius:4px;background:#26263a;outline:none;-webkit-appearance:none}.control-group input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;background:#7c7ef5;cursor:pointer;transition:transform .1s}.control-group input[type=range]::-webkit-slider-thumb:hover{transform:scale(1.15)}.control-group input[type=range]::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#7c7ef5;cursor:pointer;border:none;transition:transform .1s}.control-group input[type=range]::-moz-range-thumb:hover{transform:scale(1.15)}.control-value{display:inline-block;margin-top:2px;font-size:11px;color:#6c6c84;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace}.theme-light{--bg-0: #f5f5f7;--bg-1: #eeeef0;--bg-2: #fff;--bg-3: #e8e8ec;--border-subtle: rgba(0,0,0,.06);--border: rgba(0,0,0,.1);--border-strong: rgba(0,0,0,.16);--text-0: #1a1a2e;--text-1: #555570;--text-2: #8888a0;--accent: #6366f1;--accent-dim: rgba(99,102,241,.1);--accent-text: #4f52e8;--shadow-sm: 0 1px 3px rgba(0,0,0,.08);--shadow: 0 2px 8px rgba(0,0,0,.1);--shadow-lg: 0 4px 20px rgba(0,0,0,.12)}.theme-light ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.12)}.theme-light ::-webkit-scrollbar-thumb:hover{background:rgba(0,0,0,.08)}.theme-light ::-webkit-scrollbar-track{background:rgba(0,0,0,.06)}.theme-light .graph-nodes .graph-node text{fill:#1a1a2e}.theme-light .graph-edges line{stroke:rgba(0,0,0,.12)}.theme-light .pdf-container{background:#e8e8ec}.skeleton-pulse{animation:skeleton-pulse 1.5s ease-in-out infinite;background:#26263a;border-radius:4px}.skeleton-card{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:8px;padding:8px}.skeleton-thumb{width:100%;aspect-ratio:1;border-radius:6px}.skeleton-text{height:14px;width:80%}.skeleton-text-short{width:50%}.skeleton-row{display:flex;gap:12px;padding:10px 16px;align-items:center}.skeleton-cell{height:14px;flex:1;border-radius:4px}.skeleton-cell-icon{width:32px;height:32px;flex:none;border-radius:4px}.skeleton-cell-wide{flex:3}.loading-overlay{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:10px;background:rgba(0,0,0,.3);z-index:100;border-radius:8px}.loading-spinner{width:32px;height:32px;border:3px solid rgba(255,255,255,.09);border-top-color:#7c7ef5;border-radius:50%;animation:spin .8s linear infinite}.loading-message{color:#a0a0b8;font-size:.9rem}.login-container{display:flex;align-items:center;justify-content:center;height:100vh;background:#111118}.login-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;padding:24px;width:360px;box-shadow:0 4px 20px rgba(0,0,0,.45)}.login-title{font-size:20px;font-weight:700;color:#dcdce4;text-align:center;margin-bottom:2px}.login-subtitle{font-size:13px;color:#6c6c84;text-align:center;margin-bottom:20px}.login-error{background:rgba(228,88,88,.08);border:1px solid rgba(228,88,88,.2);border-radius:3px;padding:8px 12px;margin-bottom:12px;font-size:12px;color:#e45858}.login-form input[type=text],.login-form input[type=password]{width:100%}.login-btn{width:100%;padding:8px 16px;font-size:13px;margin-top:2px}.pagination{display:flex;flex-direction:row;justify-content:center;align-items:center;gap:2px;margin-top:16px;padding:8px 0}.page-btn{min-width:28px;text-align:center;font-variant-numeric:tabular-nums}.page-ellipsis{color:#6c6c84;padding:0 4px;font-size:12px;user-select:none}.help-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;z-index:200;animation:fade-in .1s ease-out}.help-dialog{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;padding:16px;min-width:300px;max-width:400px;box-shadow:0 4px 20px rgba(0,0,0,.45)}.help-dialog h3{font-size:16px;font-weight:600;margin-bottom:16px}.help-shortcuts{display:flex;flex-direction:column;justify-content:flex-start;align-items:stretch;gap:8px;margin-bottom:16px}.shortcut-row{display:flex;flex-direction:row;justify-content:flex-start;align-items:center;gap:12px}.shortcut-row kbd{display:inline-block;padding:2px 8px;background:#111118;border:1px solid rgba(255,255,255,.09);border-radius:3px;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:11px;color:#dcdce4;min-width:32px;text-align:center}.shortcut-row span{font-size:13px;color:#a0a0b8}.help-close{display:block;width:100%;padding:6px 12px;background:#26263a;border:1px solid rgba(255,255,255,.09);border-radius:3px;color:#dcdce4;font-size:12px;cursor:pointer;text-align:center}.help-close:hover{background:rgba(255,255,255,.06)}.plugin-page{padding:16px 24px;max-width:100%;overflow-x:hidden}.plugin-page-title{font-size:14px;font-weight:600;color:#dcdce4;margin:0 0 16px}.plugin-container{display:flex;flex-direction:column;gap:var(--plugin-gap, 0px);padding:var(--plugin-padding, 0)}.plugin-grid{display:grid;grid-template-columns:repeat(var(--plugin-columns, 1), 1fr);gap:var(--plugin-gap, 0px)}.plugin-flex{display:flex;gap:var(--plugin-gap, 0px)}.plugin-flex[data-direction=row]{flex-direction:row}.plugin-flex[data-direction=column]{flex-direction:column}.plugin-flex[data-justify=flex-start]{justify-content:flex-start}.plugin-flex[data-justify=flex-end]{justify-content:flex-end}.plugin-flex[data-justify=center]{justify-content:center}.plugin-flex[data-justify=space-between]{justify-content:space-between}.plugin-flex[data-justify=space-around]{justify-content:space-around}.plugin-flex[data-justify=space-evenly]{justify-content:space-evenly}.plugin-flex[data-align=flex-start]{align-items:flex-start}.plugin-flex[data-align=flex-end]{align-items:flex-end}.plugin-flex[data-align=center]{align-items:center}.plugin-flex[data-align=stretch]{align-items:stretch}.plugin-flex[data-align=baseline]{align-items:baseline}.plugin-flex[data-wrap=wrap]{flex-wrap:wrap}.plugin-flex[data-wrap=nowrap]{flex-wrap:nowrap}.plugin-split{display:flex}.plugin-split-sidebar{width:var(--plugin-sidebar-width, 200px);flex-shrink:0}.plugin-split-main{flex:1;min-width:0}.plugin-card{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;overflow:hidden}.plugin-card-header{padding:12px 16px;font-size:12px;font-weight:600;color:#dcdce4;border-bottom:1px solid rgba(255,255,255,.09);background:#26263a}.plugin-card-content{padding:16px}.plugin-card-footer{padding:12px 16px;border-top:1px solid rgba(255,255,255,.09);background:#18181f}.plugin-heading{color:#dcdce4;margin:0;line-height:1.2}.plugin-heading.level-1{font-size:28px;font-weight:700}.plugin-heading.level-2{font-size:18px;font-weight:600}.plugin-heading.level-3{font-size:16px;font-weight:600}.plugin-heading.level-4{font-size:14px;font-weight:500}.plugin-heading.level-5{font-size:13px;font-weight:500}.plugin-heading.level-6{font-size:12px;font-weight:500}.plugin-text{margin:0;font-size:12px;color:#dcdce4;line-height:1.4}.plugin-text.text-secondary{color:#a0a0b8}.plugin-text.text-error{color:#d47070}.plugin-text.text-success{color:#3ec97a}.plugin-text.text-warning{color:#d4a037}.plugin-text.text-bold{font-weight:600}.plugin-text.text-italic{font-style:italic}.plugin-text.text-small{font-size:10px}.plugin-text.text-large{font-size:15px}.plugin-code{background:#18181f;border:1px solid rgba(255,255,255,.09);border-radius:5px;padding:16px 24px;font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace;font-size:12px;color:#dcdce4;overflow-x:auto;white-space:pre}.plugin-code code{font-family:inherit;font-size:inherit;color:inherit}.plugin-tabs{display:flex;flex-direction:column}.plugin-tab-list{display:flex;gap:2px;border-bottom:1px solid rgba(255,255,255,.09);margin-bottom:16px}.plugin-tab{padding:8px 20px;font-size:12px;font-weight:500;color:#a0a0b8;background:rgba(0,0,0,0);border:none;border-bottom:2px solid rgba(0,0,0,0);cursor:pointer;transition:color .1s,border-color .1s}.plugin-tab:hover{color:#dcdce4}.plugin-tab.active{color:#9698f7;border-bottom-color:#7c7ef5}.plugin-tab .tab-icon{margin-right:4px}.plugin-tab-panel:not(.active){display:none}.plugin-description-list-wrapper{width:100%}.plugin-description-list{display:grid;grid-template-columns:max-content 1fr;gap:4px 16px;margin:0;padding:0}.plugin-description-list dt{font-size:10px;font-weight:500;color:#a0a0b8;text-transform:uppercase;letter-spacing:.5px;padding:6px 0;white-space:nowrap}.plugin-description-list dd{font-size:12px;color:#dcdce4;padding:6px 0;margin:0;word-break:break-word}.plugin-description-list.horizontal{display:flex;flex-wrap:wrap;gap:16px 24px;display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr))}.plugin-description-list.horizontal dt{width:auto;padding:0}.plugin-description-list.horizontal dd{width:auto;padding:0}.plugin-description-list.horizontal dt,.plugin-description-list.horizontal dd{display:inline}.plugin-description-list.horizontal dt{font-size:9px;text-transform:uppercase;letter-spacing:.5px;color:#6c6c84;margin-bottom:2px}.plugin-description-list.horizontal dd{font-size:13px;font-weight:600;color:#dcdce4}.plugin-data-table-wrapper{overflow-x:auto}.plugin-data-table{width:100%;border-collapse:collapse;font-size:12px}.plugin-data-table thead tr{border-bottom:1px solid rgba(255,255,255,.14)}.plugin-data-table thead th{padding:8px 12px;text-align:left;font-size:10px;font-weight:600;color:#a0a0b8;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap}.plugin-data-table tbody tr{border-bottom:1px solid rgba(255,255,255,.06);transition:background .08s}.plugin-data-table tbody tr:hover{background:rgba(255,255,255,.03)}.plugin-data-table tbody tr:last-child{border-bottom:none}.plugin-data-table tbody td{padding:8px 12px;color:#dcdce4;vertical-align:middle}.plugin-col-constrained{width:var(--plugin-col-width)}.table-filter{margin-bottom:12px}.table-filter input{width:240px;padding:6px 12px;background:#18181f;border:1px solid rgba(255,255,255,.09);border-radius:5px;color:#dcdce4;font-size:12px}.table-filter input::placeholder{color:#6c6c84}.table-filter input:focus{outline:none;border-color:#7c7ef5}.table-pagination{display:flex;align-items:center;gap:12px;padding:8px 0;font-size:12px;color:#a0a0b8}.row-actions{white-space:nowrap;width:1%}.row-actions .plugin-button{padding:4px 8px;font-size:10px;margin-right:4px}.plugin-media-grid{display:grid;grid-template-columns:repeat(var(--plugin-columns, 2), 1fr);gap:var(--plugin-gap, 8px)}.media-grid-item{background:#1f1f28;border:1px solid rgba(255,255,255,.09);border-radius:7px;overflow:hidden;display:flex;flex-direction:column}.media-grid-img{width:100%;aspect-ratio:16/9;object-fit:cover;display:block}.media-grid-no-img{width:100%;aspect-ratio:16/9;background:#26263a;display:flex;align-items:center;justify-content:center;font-size:10px;color:#6c6c84}.media-grid-caption{padding:8px 12px;font-size:10px;color:#dcdce4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.plugin-list{list-style:none;margin:0;padding:0}.plugin-list-item{padding:8px 0}.plugin-list-divider{border:none;border-top:1px solid rgba(255,255,255,.06);margin:0}.plugin-list-empty{padding:16px;text-align:center;color:#6c6c84;font-size:12px}.plugin-button{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border:1px solid rgba(255,255,255,.09);border-radius:5px;font-size:12px;font-weight:500;cursor:pointer;transition:background .08s,border-color .08s,color .08s;background:#1f1f28;color:#dcdce4}.plugin-button:disabled{opacity:.45;cursor:not-allowed}.plugin-button.btn-primary{background:#7c7ef5;border-color:#7c7ef5;color:#fff}.plugin-button.btn-primary:hover:not(:disabled){background:#8b8df7}.plugin-button.btn-secondary{background:#26263a;border-color:rgba(255,255,255,.14);color:#dcdce4}.plugin-button.btn-secondary:hover:not(:disabled){background:rgba(255,255,255,.04)}.plugin-button.btn-tertiary{background:rgba(0,0,0,0);border-color:rgba(0,0,0,0);color:#9698f7}.plugin-button.btn-tertiary:hover:not(:disabled){background:rgba(124,126,245,.15)}.plugin-button.btn-danger{background:rgba(0,0,0,0);border-color:rgba(228,88,88,.2);color:#d47070}.plugin-button.btn-danger:hover:not(:disabled){background:rgba(228,88,88,.06)}.plugin-button.btn-success{background:rgba(0,0,0,0);border-color:rgba(62,201,122,.2);color:#3ec97a}.plugin-button.btn-success:hover:not(:disabled){background:rgba(62,201,122,.08)}.plugin-button.btn-ghost{background:rgba(0,0,0,0);border-color:rgba(0,0,0,0);color:#a0a0b8}.plugin-button.btn-ghost:hover:not(:disabled){background:rgba(255,255,255,.04)}.plugin-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:50%;font-size:9px;font-weight:600;letter-spacing:.5px;text-transform:uppercase}.plugin-badge.badge-default,.plugin-badge.badge-neutral{background:rgba(255,255,255,.04);color:#a0a0b8}.plugin-badge.badge-primary{background:rgba(124,126,245,.15);color:#9698f7}.plugin-badge.badge-secondary{background:rgba(255,255,255,.03);color:#dcdce4}.plugin-badge.badge-success{background:rgba(62,201,122,.08);color:#3ec97a}.plugin-badge.badge-warning{background:rgba(212,160,55,.06);color:#d4a037}.plugin-badge.badge-error{background:rgba(228,88,88,.06);color:#d47070}.plugin-badge.badge-info{background:rgba(99,102,241,.08);color:#9698f7}.plugin-form{display:flex;flex-direction:column;gap:16px}.form-field{display:flex;flex-direction:column;gap:6px}.form-field label{font-size:12px;font-weight:500;color:#dcdce4}.form-field input,.form-field textarea,.form-field select{padding:8px 12px;background:#18181f;border:1px solid rgba(255,255,255,.09);border-radius:5px;color:#dcdce4;font-size:12px;font-family:inherit}.form-field input::placeholder,.form-field textarea::placeholder,.form-field select::placeholder{color:#6c6c84}.form-field input:focus,.form-field textarea:focus,.form-field select:focus{outline:none;border-color:#7c7ef5;box-shadow:0 0 0 2px rgba(124,126,245,.15)}.form-field textarea{min-height:80px;resize:vertical}.form-field select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23a0a0b8' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center;padding-right:32px}.form-help{margin:0;font-size:10px;color:#6c6c84}.form-actions{display:flex;gap:12px;padding-top:8px}.required{color:#e45858}.plugin-link{color:#9698f7;text-decoration:none}.plugin-link:hover{text-decoration:underline}.plugin-link-blocked{color:#6c6c84;text-decoration:line-through;cursor:not-allowed}.plugin-progress{background:#18181f;border:1px solid rgba(255,255,255,.09);border-radius:5px;height:8px;overflow:hidden;display:flex;align-items:center;gap:8px}.plugin-progress-bar{height:100%;background:#7c7ef5;border-radius:4px;transition:width .3s ease;width:var(--plugin-progress, 0%)}.plugin-progress-label{font-size:10px;color:#a0a0b8;white-space:nowrap;flex-shrink:0}.plugin-chart{overflow:auto;height:var(--plugin-chart-height, 200px)}.plugin-chart .chart-title{font-size:13px;font-weight:600;color:#dcdce4;margin-bottom:8px}.plugin-chart .chart-x-label,.plugin-chart .chart-y-label{font-size:10px;color:#6c6c84;margin-bottom:4px}.plugin-chart .chart-data-table{overflow-x:auto}.plugin-chart .chart-no-data{padding:24px;text-align:center;color:#6c6c84;font-size:12px}.plugin-loading{padding:16px;color:#a0a0b8;font-size:12px;font-style:italic}.plugin-error{padding:12px 16px;background:rgba(228,88,88,.06);border:1px solid rgba(228,88,88,.2);border-radius:5px;color:#d47070;font-size:12px}.plugin-feedback{position:sticky;bottom:16px;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:12px 16px;border-radius:7px;font-size:12px;z-index:300;box-shadow:0 4px 20px rgba(0,0,0,.45)}.plugin-feedback.success{background:rgba(62,201,122,.08);border:1px solid rgba(62,201,122,.2);color:#3ec97a}.plugin-feedback.error{background:rgba(228,88,88,.06);border:1px solid rgba(228,88,88,.2);color:#d47070}.plugin-feedback-dismiss{background:rgba(0,0,0,0);border:none;color:inherit;font-size:14px;cursor:pointer;line-height:1;padding:0;opacity:.7}.plugin-feedback-dismiss:hover{opacity:1}.plugin-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.65);display:flex;align-items:center;justify-content:center;z-index:100}.plugin-modal{position:relative;background:#1f1f28;border:1px solid rgba(255,255,255,.14);border-radius:12px;padding:32px;min-width:380px;max-width:640px;max-height:80vh;overflow-y:auto;box-shadow:0 4px 20px rgba(0,0,0,.45);z-index:200}.plugin-modal-close{position:absolute;top:16px;right:16px;background:rgba(0,0,0,0);border:none;color:#a0a0b8;font-size:14px;cursor:pointer;line-height:1;padding:4px;border-radius:5px}.plugin-modal-close:hover{background:rgba(255,255,255,.04);color:#dcdce4} \ No newline at end of file +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} +* { + margin: 0; + padding: 0; + box-sizing: border-box; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.06) rgba(0, 0, 0, 0); +} +*::-webkit-scrollbar { + width: 5px; + height: 5px; +} +*::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0); +} +*::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.06); + border-radius: 3px; +} +*::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.14); +} +:root { + --bg-0: #111118; + --bg-1: #18181f; + --bg-2: #1f1f28; + --bg-3: #26263a; + --border-subtle: rgba(255, 255, 255, 0.06); + --border: rgba(255, 255, 255, 0.09); + --border-strong: rgba(255, 255, 255, 0.14); + --text-0: #dcdce4; + --text-1: #a0a0b8; + --text-2: #6c6c84; + --accent: #7c7ef5; + --accent-dim: rgba(124, 126, 245, 0.15); + --accent-text: #9698f7; + --success: #3ec97a; + --error: #e45858; + --warning: #d4a037; + --radius-sm: 3px; + --radius: 5px; + --radius-md: 7px; + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); + --shadow: 0 2px 8px rgba(0, 0, 0, 0.35); + --shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.45); +} +body { + font-family: + "Inter", + -apple-system, + "Segoe UI", + system-ui, + sans-serif; + background: var(--bg-0); + color: var(--text-0); + font-size: 13px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + overflow: hidden; +} +:focus-visible:focus-visible { + outline: 2px solid #7c7ef5; + outline-offset: 2px; +} +::selection { + background: rgba(124, 126, 245, 0.15); + color: #9698f7; +} +a { + color: #9698f7; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +code { + padding: 1px 5px; + border-radius: 3px; + background: #111118; + color: #9698f7; + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; + font-size: 11px; +} +ul { + list-style: none; + padding: 0; +} +ul li { + padding: 3px 0; + font-size: 12px; + color: #a0a0b8; +} +.text-muted { + color: #a0a0b8; +} +.text-sm { + font-size: 11px; +} +.mono { + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; + font-size: 12px; +} +.flex-row { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; +} +.flex-between { + display: flex; + justify-content: space-between; + align-items: center; +} +.mb-16 { + margin-bottom: 16px; +} +.mb-8 { + margin-bottom: 12px; +} +.mt-16 { + margin-top: 16px; +} +.mt-8 { + margin-top: 12px; +} +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes slide-up { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.3; + } +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +@keyframes skeleton-pulse { + 0% { + opacity: 0.6; + } + 50% { + opacity: 0.3; + } + 100% { + opacity: 0.6; + } +} +@keyframes indeterminate { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(400%); + } +} +.app { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; + height: 100vh; + overflow: hidden; +} +.sidebar { + width: 220px; + min-width: 220px; + max-width: 220px; + background: #18181f; + border-right: 1px solid rgba(255, 255, 255, 0.09); + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + flex-shrink: 0; + user-select: none; + overflow-y: auto; + overflow-x: hidden; + z-index: 10; + transition: + width 0.15s, + min-width 0.15s, + max-width 0.15s; +} +.sidebar.collapsed { + width: 48px; + min-width: 48px; + max-width: 48px; +} +.sidebar.collapsed .nav-label, +.sidebar.collapsed .sidebar-header .logo, +.sidebar.collapsed .sidebar-header .version, +.sidebar.collapsed .nav-badge, +.sidebar.collapsed .nav-item-text, +.sidebar.collapsed .sidebar-footer .status-text, +.sidebar.collapsed .user-name, +.sidebar.collapsed .role-badge, +.sidebar.collapsed .user-info .btn, +.sidebar.collapsed .sidebar-import-header span, +.sidebar.collapsed .sidebar-import-file { + display: none; +} +.sidebar.collapsed .nav-item { + justify-content: center; + padding: 8px; + border-left: none; + border-radius: 3px; +} +.sidebar.collapsed .nav-item.active { + border-left: none; +} +.sidebar.collapsed .nav-icon { + width: auto; + margin: 0; +} +.sidebar.collapsed .sidebar-header { + padding: 12px 8px; + justify-content: center; +} +.sidebar.collapsed .nav-section { + padding: 0 4px; +} +.sidebar.collapsed .sidebar-footer { + padding: 8px; +} +.sidebar.collapsed .sidebar-footer .user-info { + justify-content: center; + padding: 4px; +} +.sidebar.collapsed .sidebar-import-progress { + padding: 6px; +} +.sidebar-header { + padding: 16px 16px 20px; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: baseline; + gap: 8px; +} +.sidebar-header .logo { + font-size: 15px; + font-weight: 700; + letter-spacing: -0.4px; + color: #dcdce4; +} +.sidebar-header .version { + font-size: 10px; + color: #6c6c84; +} +.sidebar-toggle { + background: rgba(0, 0, 0, 0); + border: none; + color: #6c6c84; + padding: 8px; + font-size: 18px; + width: 100%; + text-align: center; +} +.sidebar-toggle:hover { + color: #dcdce4; +} +.sidebar-spacer { + flex: 1; +} +.sidebar-footer { + padding: 12px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + overflow: visible; + min-width: 0; +} +.nav-section { + padding: 0 8px; + margin-bottom: 2px; +} +.nav-label { + padding: 8px 8px 4px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #6c6c84; +} +.nav-item { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + padding: 6px 8px; + border-radius: 3px; + cursor: pointer; + color: #a0a0b8; + font-size: 13px; + font-weight: 450; + transition: + color 0.1s, + background 0.1s; + border: none; + background: none; + width: 100%; + text-align: left; + border-left: 2px solid rgba(0, 0, 0, 0); + margin-left: 0; +} +.nav-item:hover { + color: #dcdce4; + background: rgba(255, 255, 255, 0.03); +} +.nav-item.active { + color: #9698f7; + border-left-color: #7c7ef5; + background: rgba(124, 126, 245, 0.15); +} +.nav-item-text { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} +.sidebar:not(.collapsed) .nav-item-text { + overflow: visible; +} +.nav-icon { + width: 18px; + text-align: center; + font-size: 14px; + opacity: 0.7; +} +.nav-badge { + margin-left: auto; + font-size: 10px; + font-weight: 600; + color: #6c6c84; + background: #26263a; + padding: 1px 6px; + border-radius: 12px; + min-width: 20px; + text-align: center; + font-variant-numeric: tabular-nums; +} +.status-indicator { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 6px; + font-size: 11px; + font-weight: 500; + min-width: 0; + overflow: visible; +} +.sidebar:not(.collapsed) .status-indicator { + justify-content: flex-start; +} +.status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} +.status-dot.connected { + background: #3ec97a; +} +.status-dot.disconnected { + background: #e45858; +} +.status-dot.checking { + background: #d4a037; + animation: pulse 1.5s infinite; +} +.status-text { + color: #6c6c84; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} +.sidebar:not(.collapsed) .status-text { + overflow: visible; +} +.main { + flex: 1; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + overflow: hidden; + min-width: 0; +} +.header { + height: 48px; + min-height: 48px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 12px; + padding: 0 20px; + background: #18181f; +} +.page-title { + font-size: 14px; + font-weight: 600; + color: #dcdce4; +} +.header-spacer { + flex: 1; +} +.content { + flex: 1; + overflow-y: auto; + padding: 20px; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.06) rgba(0, 0, 0, 0); +} +.sidebar-import-progress { + padding: 10px 12px; + background: #1f1f28; + border-top: 1px solid rgba(255, 255, 255, 0.06); + font-size: 11px; +} +.sidebar-import-header { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 6px; + margin-bottom: 4px; + color: #a0a0b8; +} +.sidebar-import-file { + color: #6c6c84; + font-size: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 4px; +} +.sidebar-import-progress .progress-bar { + height: 3px; +} +.user-info { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 6px; + font-size: 12px; + overflow: hidden; + min-width: 0; +} +.user-name { + font-weight: 500; + color: #dcdce4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 90px; + flex-shrink: 1; +} +.role-badge { + display: inline-block; + padding: 1px 6px; + border-radius: 3px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; +} +.role-badge.role-admin { + background: rgba(139, 92, 246, 0.1); + color: #9d8be0; +} +.role-badge.role-editor { + background: rgba(34, 160, 80, 0.1); + color: #5cb97a; +} +.role-badge.role-viewer { + background: rgba(59, 120, 200, 0.1); + color: #6ca0d4; +} +.btn { + padding: 5px 12px; + border-radius: 3px; + border: none; + cursor: pointer; + font-size: 12px; + font-weight: 500; + transition: all 0.1s; + display: inline-flex; + align-items: center; + gap: 5px; + white-space: nowrap; + line-height: 1.5; +} +.btn-primary { + background: #7c7ef5; + color: #fff; +} +.btn-primary:hover { + background: #8b8df7; +} +.btn-secondary { + background: #26263a; + color: #dcdce4; + border: 1px solid rgba(255, 255, 255, 0.09); +} +.btn-secondary:hover { + border-color: rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.06); +} +.btn-danger { + background: rgba(0, 0, 0, 0); + color: #e45858; + border: 1px solid rgba(228, 88, 88, 0.25); +} +.btn-danger:hover { + background: rgba(228, 88, 88, 0.08); +} +.btn-ghost { + background: rgba(0, 0, 0, 0); + border: none; + color: #a0a0b8; + padding: 5px 8px; +} +.btn-ghost:hover { + color: #dcdce4; + background: rgba(255, 255, 255, 0.04); +} +.btn-sm { + padding: 3px 8px; + font-size: 11px; +} +.btn-icon { + padding: 4px; + border-radius: 3px; + background: rgba(0, 0, 0, 0); + border: none; + color: #6c6c84; + cursor: pointer; + transition: color 0.1s; + font-size: 13px; +} +.btn-icon:hover { + color: #dcdce4; +} +.btn:disabled, +.btn[disabled] { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} +.btn.btn-disabled-hint:disabled { + opacity: 0.6; + border-style: dashed; + pointer-events: auto; + cursor: help; +} +.card { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + padding: 16px; +} +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; +} +.card-title { + font-size: 14px; + font-weight: 600; +} +.card-body { + padding-top: 8px; +} +.data-table { + width: 100%; + border-collapse: collapse; + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + overflow: hidden; +} +.data-table thead th { + padding: 8px 14px; + text-align: left; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #6c6c84; + border-bottom: 1px solid rgba(255, 255, 255, 0.09); + background: #26263a; +} +.data-table tbody td { + padding: 8px 14px; + font-size: 13px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + max-width: 300px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.data-table tbody tr { + cursor: pointer; + transition: background 0.08s; +} +.data-table tbody tr:hover { + background: rgba(255, 255, 255, 0.02); +} +.data-table tbody tr.row-selected { + background: rgba(99, 102, 241, 0.12); +} +.data-table tbody tr:last-child td { + border-bottom: none; +} +.sortable-header { + cursor: pointer; + user-select: none; + transition: color 0.1s; +} +.sortable-header:hover { + color: #9698f7; +} +input[type="text"], +textarea, +select { + padding: 6px 10px; + border-radius: 3px; + border: 1px solid rgba(255, 255, 255, 0.09); + background: #111118; + color: #dcdce4; + font-size: 13px; + outline: none; + transition: border-color 0.15s; + font-family: inherit; +} +input[type="text"]::placeholder, +textarea::placeholder, +select::placeholder { + color: #6c6c84; +} +input[type="text"]:focus, +textarea:focus, +select:focus { + border-color: #7c7ef5; +} +input[type="text"][type="number"], +textarea[type="number"], +select[type="number"] { + width: 80px; + padding: 6px 8px; + -moz-appearance: textfield; +} +input[type="text"][type="number"]::-webkit-outer-spin-button, +input[type="text"][type="number"]::-webkit-inner-spin-button, +textarea[type="number"]::-webkit-outer-spin-button, +textarea[type="number"]::-webkit-inner-spin-button, +select[type="number"]::-webkit-outer-spin-button, +select[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} +textarea { + min-height: 64px; + resize: vertical; +} +select { + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath fill='%236c6c84' d='M5 7L1 3h8z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + padding-right: 26px; + min-width: 100px; +} +.form-group { + margin-bottom: 12px; +} +.form-label { + display: block; + font-size: 11px; + font-weight: 600; + color: #a0a0b8; + margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: 0.03em; +} +.form-row { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-end; + gap: 8px; +} +.form-row input[type="text"] { + flex: 1; +} +.form-label-row { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 2px; + margin-bottom: 4px; +} +.form-label-row .form-label { + margin-bottom: 0; +} +.badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 50%; + font-size: 9px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; +} +.badge-neutral { + background: rgba(255, 255, 255, 0.04); + color: #a0a0b8; +} +.badge-success { + background: rgba(62, 201, 122, 0.08); + color: #3ec97a; +} +.badge-warning { + background: rgba(212, 160, 55, 0.06); + color: #d4a037; +} +.badge-danger { + background: rgba(228, 88, 88, 0.06); + color: #d47070; +} +.tab-bar { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 4px; + border-bottom: 1px solid rgba(255, 255, 255, 0.09); + margin-bottom: 12px; +} +.tab-btn { + background: rgba(0, 0, 0, 0); + border: none; + padding: 6px 8px; + font-size: 13px; + color: #a0a0b8; + border-bottom: 2px solid rgba(0, 0, 0, 0); + margin-bottom: -1px; + cursor: pointer; + transition: + color 0.1s, + border-color 0.1s; +} +.tab-btn:hover { + color: #dcdce4; +} +.tab-btn.active { + color: #9698f7; + border-bottom-color: #7c7ef5; +} +.input-sm { + padding: 6px 10px; + border-radius: 3px; + border: 1px solid rgba(255, 255, 255, 0.09); + background: #111118; + color: #dcdce4; + font-size: 13px; + outline: none; + transition: border-color 0.15s; + font-family: inherit; + padding: 3px 8px; + font-size: 12px; +} +.input-sm::placeholder { + color: #6c6c84; +} +.input-sm:focus { + border-color: #7c7ef5; +} +.input-suffix { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; +} +.input-suffix input { + border-radius: 3px 0 0 3px; + border-right: none; + flex: 1; +} +.input-suffix .btn { + border-radius: 0 3px 3px 0; +} +.field-error { + color: #d47070; + font-size: 11px; + margin-top: 2px; +} +.comments-list { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 8px; +} +.comment-item { + padding: 8px; + background: #18181f; + border-radius: 5px; + border: 1px solid rgba(255, 255, 255, 0.06); +} +.comment-text { + font-size: 13px; + color: #dcdce4; + line-height: 1.5; + margin-top: 4px; +} +input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + width: 16px; + height: 16px; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 3px; + background: #1f1f28; + cursor: pointer; + position: relative; + flex-shrink: 0; + transition: all 0.15s ease; +} +input[type="checkbox"]:hover { + border-color: #7c7ef5; + background: #26263a; +} +input[type="checkbox"]:checked { + background: #7c7ef5; + border-color: #7c7ef5; +} +input[type="checkbox"]:checked::after { + content: ""; + position: absolute; + left: 5px; + top: 2px; + width: 4px; + height: 8px; + border: solid #111118; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} +input[type="checkbox"]:focus-visible:focus-visible { + outline: 2px solid #7c7ef5; + outline-offset: 2px; +} +.checkbox-label { + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 13px; + color: #a0a0b8; + user-select: none; +} +.checkbox-label:hover { + color: #dcdce4; +} +.checkbox-label input[type="checkbox"] { + margin: 0; +} +.toggle { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 13px; + color: #dcdce4; +} +.toggle.disabled { + opacity: 0.4; + cursor: not-allowed; +} +.toggle-track { + width: 32px; + height: 18px; + border-radius: 9px; + background: #26263a; + border: 1px solid rgba(255, 255, 255, 0.09); + position: relative; + transition: background 0.15s; + flex-shrink: 0; +} +.toggle-track.active { + background: #7c7ef5; + border-color: #7c7ef5; +} +.toggle-track.active .toggle-thumb { + transform: translateX(14px); +} +.toggle-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: #dcdce4; + position: absolute; + top: 1px; + left: 1px; + transition: transform 0.15s; +} +.filter-bar { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 12px; + padding: 12px; + background: #111118; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 5px; + margin-bottom: 12px; +} +.filter-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} +.filter-label { + font-size: 11px; + font-weight: 500; + color: #6c6c84; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-right: 4px; +} +.filter-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 10px; + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 14px; + cursor: pointer; + font-size: 11px; + color: #a0a0b8; + transition: all 0.15s ease; + user-select: none; +} +.filter-chip:hover { + background: #26263a; + border-color: rgba(255, 255, 255, 0.14); + color: #dcdce4; +} +.filter-chip.active { + background: rgba(124, 126, 245, 0.15); + border-color: #7c7ef5; + color: #9698f7; +} +.filter-chip input[type="checkbox"] { + width: 12px; + height: 12px; + margin: 0; +} +.filter-chip input[type="checkbox"]:checked::after { + left: 3px; + top: 1px; + width: 3px; + height: 6px; +} +.filter-group { + display: flex; + align-items: center; + gap: 6px; +} +.filter-group label { + display: flex; + align-items: center; + gap: 3px; + cursor: pointer; + color: #a0a0b8; + font-size: 11px; + white-space: nowrap; +} +.filter-group label:hover { + color: #dcdce4; +} +.filter-separator { + width: 1px; + height: 20px; + background: rgba(255, 255, 255, 0.09); + flex-shrink: 0; +} +.view-toggle { + display: flex; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 3px; + overflow: hidden; +} +.view-btn { + padding: 4px 10px; + background: #1f1f28; + border: none; + color: #6c6c84; + cursor: pointer; + font-size: 18px; + line-height: 1; + transition: + background 0.1s, + color 0.1s; +} +.view-btn:first-child { + border-right: 1px solid rgba(255, 255, 255, 0.09); +} +.view-btn:hover { + color: #dcdce4; + background: #26263a; +} +.view-btn.active { + background: rgba(124, 126, 245, 0.15); + color: #9698f7; +} +.breadcrumb { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 4px; + padding: 10px 16px; + font-size: 0.85rem; + color: #6c6c84; +} +.breadcrumb-sep { + color: #6c6c84; + opacity: 0.5; +} +.breadcrumb-link { + color: #9698f7; + text-decoration: none; + cursor: pointer; +} +.breadcrumb-link:hover { + text-decoration: underline; +} +.breadcrumb-current { + color: #dcdce4; + font-weight: 500; +} +.progress-bar { + width: 100%; + height: 8px; + background: #26263a; + border-radius: 4px; + overflow: hidden; + margin-bottom: 6px; +} +.progress-fill { + height: 100%; + background: #7c7ef5; + border-radius: 4px; + transition: width 0.3s ease; +} +.progress-fill.indeterminate { + width: 30%; + animation: indeterminate 1.5s ease-in-out infinite; +} +.loading-overlay { + display: flex; + align-items: center; + justify-content: center; + padding: 48px 16px; + color: #6c6c84; + font-size: 13px; + gap: 10px; +} +.spinner { + width: 18px; + height: 18px; + border: 2px solid rgba(255, 255, 255, 0.09); + border-top-color: #7c7ef5; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} +.spinner-small { + width: 14px; + height: 14px; + border: 2px solid rgba(255, 255, 255, 0.09); + border-top-color: #7c7ef5; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} +.spinner-tiny { + width: 10px; + height: 10px; + border: 1.5px solid rgba(255, 255, 255, 0.09); + border-top-color: #7c7ef5; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + animation: fade-in 0.1s ease-out; +} +.modal { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 7px; + padding: 20px; + min-width: 360px; + max-width: 480px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); +} +.modal.wide { + max-width: 600px; + max-height: 70vh; + overflow-y: auto; +} +.modal-title { + font-size: 15px; + font-weight: 600; + margin-bottom: 6px; +} +.modal-body { + font-size: 12px; + color: #a0a0b8; + margin-bottom: 16px; + line-height: 1.5; +} +.modal-actions { + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; + gap: 6px; +} +.tooltip-trigger { + display: inline-flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; + border-radius: 50%; + background: #26263a; + color: #6c6c84; + font-size: 9px; + font-weight: 700; + cursor: help; + position: relative; + flex-shrink: 0; + margin-left: 4px; +} +.tooltip-trigger:hover { + background: rgba(124, 126, 245, 0.15); + color: #9698f7; +} +.tooltip-trigger:hover .tooltip-text { + display: block; +} +.tooltip-text { + display: none; + position: absolute; + bottom: calc(100% + 6px); + left: 50%; + transform: translateX(-50%); + padding: 6px 10px; + background: #26263a; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 3px; + color: #dcdce4; + font-size: 11px; + font-weight: 400; + line-height: 1.4; + white-space: normal; + width: 220px; + text-transform: none; + letter-spacing: normal; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); + z-index: 100; + pointer-events: none; +} +.media-player { + position: relative; + background: #111118; + border-radius: 5px; + overflow: hidden; +} +.media-player:focus { + outline: none; +} +.media-player-audio .player-artwork { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 8px; + padding: 24px 16px 8px; +} +.player-native-video { + width: 100%; + display: block; +} +.player-native-audio { + display: none; +} +.player-artwork img { + max-width: 200px; + max-height: 200px; + border-radius: 5px; + object-fit: cover; +} +.player-artwork-placeholder { + width: 120px; + height: 120px; + display: flex; + align-items: center; + justify-content: center; + background: #1f1f28; + border-radius: 5px; + font-size: 48px; + opacity: 0.3; +} +.player-title { + font-size: 13px; + font-weight: 500; + color: #dcdce4; + text-align: center; +} +.player-controls { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: #1f1f28; +} +.media-player-video .player-controls { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.7); + opacity: 0; + transition: opacity 0.2s; +} +.media-player-video:hover .player-controls { + opacity: 1; +} +.play-btn, +.mute-btn, +.fullscreen-btn { + background: none; + border: none; + color: #dcdce4; + cursor: pointer; + font-size: 18px; + padding: 4px; + line-height: 1; + transition: color 0.1s; +} +.play-btn:hover, +.mute-btn:hover, +.fullscreen-btn:hover { + color: #9698f7; +} +.player-time { + font-size: 11px; + color: #6c6c84; + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; + min-width: 36px; + text-align: center; + user-select: none; +} +.seek-bar { + flex: 1; + -webkit-appearance: none; + appearance: none; + height: 4px; + border-radius: 4px; + background: #26263a; + outline: none; + cursor: pointer; +} +.seek-bar::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 12px; + height: 12px; + border-radius: 50%; + background: #7c7ef5; + cursor: pointer; + border: none; +} +.seek-bar::-moz-range-thumb { + width: 12px; + height: 12px; + border-radius: 50%; + background: #7c7ef5; + cursor: pointer; + border: none; +} +.volume-slider { + width: 70px; + -webkit-appearance: none; + appearance: none; + height: 4px; + border-radius: 4px; + background: #26263a; + outline: none; + cursor: pointer; +} +.volume-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 10px; + height: 10px; + border-radius: 50%; + background: #a0a0b8; + cursor: pointer; + border: none; +} +.volume-slider::-moz-range-thumb { + width: 10px; + height: 10px; + border-radius: 50%; + background: #a0a0b8; + cursor: pointer; + border: none; +} +.image-viewer-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.92); + z-index: 150; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + animation: fade-in 0.15s ease-out; +} +.image-viewer-overlay:focus { + outline: none; +} +.image-viewer-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + background: rgba(0, 0, 0, 0.5); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + z-index: 2; + user-select: none; +} +.image-viewer-toolbar-left, +.image-viewer-toolbar-center, +.image-viewer-toolbar-right { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 6px; +} +.iv-btn { + background: rgba(255, 255, 255, 0.06); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #dcdce4; + border-radius: 3px; + padding: 4px 10px; + font-size: 12px; + cursor: pointer; + transition: background 0.1s; +} +.iv-btn:hover { + background: rgba(255, 255, 255, 0.12); +} +.iv-btn.iv-close { + color: #e45858; + font-weight: 600; +} +.iv-zoom-label { + font-size: 11px; + color: #a0a0b8; + min-width: 40px; + text-align: center; + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; +} +.image-viewer-canvas { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; +} +.image-viewer-canvas img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + user-select: none; + -webkit-user-drag: none; +} +.pdf-viewer { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + height: 100%; + min-height: 500px; + background: #111118; + border-radius: 5px; + overflow: hidden; +} +.pdf-toolbar { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 12px; + padding: 10px 12px; + background: #18181f; + border-bottom: 1px solid rgba(255, 255, 255, 0.09); +} +.pdf-toolbar-group { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 4px; +} +.pdf-toolbar-btn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 3px; + color: #a0a0b8; + font-size: 14px; + cursor: pointer; + transition: all 0.15s; +} +.pdf-toolbar-btn:hover:not(:disabled) { + background: #26263a; + color: #dcdce4; +} +.pdf-toolbar-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} +.pdf-zoom-label { + min-width: 45px; + text-align: center; + font-size: 12px; + color: #a0a0b8; +} +.pdf-container { + flex: 1; + position: relative; + overflow: hidden; + background: #1f1f28; +} +.pdf-object { + width: 100%; + height: 100%; + border: none; +} +.pdf-loading, +.pdf-error { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 12px; + background: #18181f; + color: #a0a0b8; +} +.pdf-error { + padding: 12px; + text-align: center; +} +.pdf-fallback { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 16px; + padding: 48px 12px; + text-align: center; + color: #6c6c84; +} +.markdown-viewer { + padding: 16px; + text-align: left; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 12px; +} +.markdown-toolbar { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + padding: 8px; + background: #1f1f28; + border-radius: 5px; + border: 1px solid rgba(255, 255, 255, 0.09); +} +.toolbar-btn { + padding: 6px 12px; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 3px; + background: #18181f; + color: #a0a0b8; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; +} +.toolbar-btn:hover { + background: #26263a; + border-color: rgba(255, 255, 255, 0.14); +} +.toolbar-btn.active { + background: #7c7ef5; + color: #fff; + border-color: #7c7ef5; +} +.markdown-source { + max-width: 100%; + background: #26263a; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + padding: 16px; + overflow-x: auto; + font-family: "Menlo", "Monaco", "Courier New", monospace; + font-size: 13px; + line-height: 1.7; + color: #dcdce4; + white-space: pre-wrap; + word-wrap: break-word; +} +.markdown-source code { + font-family: inherit; + background: none; + padding: 0; + border: none; +} +.markdown-content { + max-width: 800px; + color: #dcdce4; + line-height: 1.7; + font-size: 14px; + text-align: left; +} +.markdown-content h1 { + font-size: 1.8em; + font-weight: 700; + margin: 1em 0 0.5em; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + padding-bottom: 0.3em; +} +.markdown-content h2 { + font-size: 1.5em; + font-weight: 600; + margin: 0.8em 0 0.4em; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + padding-bottom: 0.2em; +} +.markdown-content h3 { + font-size: 1.25em; + font-weight: 600; + margin: 0.6em 0 0.3em; +} +.markdown-content h4 { + font-size: 1.1em; + font-weight: 600; + margin: 0.5em 0 0.25em; +} +.markdown-content h5, +.markdown-content h6 { + font-size: 1em; + font-weight: 600; + margin: 0.4em 0 0.2em; + color: #a0a0b8; +} +.markdown-content p { + margin: 0 0 1em; +} +.markdown-content a { + color: #7c7ef5; + text-decoration: none; +} +.markdown-content a:hover { + text-decoration: underline; +} +.markdown-content pre { + background: #26263a; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 3px; + padding: 12px 16px; + overflow-x: auto; + margin: 0 0 1em; + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; + font-size: 12px; + line-height: 1.5; +} +.markdown-content code { + background: #26263a; + padding: 1px 5px; + border-radius: 3px; + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; + font-size: 0.9em; +} +.markdown-content pre code { + background: none; + padding: 0; +} +.markdown-content blockquote { + border-left: 3px solid #7c7ef5; + padding: 4px 16px; + margin: 0 0 1em; + color: #a0a0b8; + background: rgba(124, 126, 245, 0.04); +} +.markdown-content table { + width: 100%; + border-collapse: collapse; + margin: 0 0 1em; +} +.markdown-content th, +.markdown-content td { + padding: 6px 12px; + border: 1px solid rgba(255, 255, 255, 0.09); + font-size: 13px; +} +.markdown-content th { + background: #26263a; + font-weight: 600; + text-align: left; +} +.markdown-content tr:nth-child(even) { + background: #1f1f28; +} +.markdown-content ul, +.markdown-content ol { + margin: 0 0 1em; + padding-left: 16px; +} +.markdown-content ul { + list-style: disc; +} +.markdown-content ol { + list-style: decimal; +} +.markdown-content li { + padding: 2px 0; + font-size: 14px; + color: #dcdce4; +} +.markdown-content hr { + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.09); + margin: 1.5em 0; +} +.markdown-content img { + max-width: 100%; + border-radius: 5px; +} +.markdown-content .footnote-definition { + font-size: 0.85em; + color: #a0a0b8; + margin-top: 0.5em; + padding-left: 1.5em; +} +.markdown-content .footnote-definition sup { + color: #7c7ef5; + margin-right: 4px; +} +.markdown-content sup a { + color: #7c7ef5; + text-decoration: none; + font-size: 0.8em; +} +.wikilink { + color: #9698f7; + text-decoration: none; + border-bottom: 1px dashed #7c7ef5; + cursor: pointer; + transition: + border-color 0.1s, + color 0.1s; +} +.wikilink:hover { + color: #7c7ef5; + border-bottom-style: solid; +} +.wikilink-embed { + display: inline-block; + padding: 2px 8px; + background: rgba(139, 92, 246, 0.08); + border: 1px dashed rgba(139, 92, 246, 0.3); + border-radius: 3px; + color: #9d8be0; + font-size: 12px; + cursor: default; +} +.media-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 12px; +} +.media-card { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + overflow: hidden; + cursor: pointer; + transition: + border-color 0.12s, + box-shadow 0.12s; + position: relative; +} +.media-card:hover { + border-color: rgba(255, 255, 255, 0.14); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} +.media-card.selected { + border-color: #7c7ef5; + box-shadow: 0 0 0 1px #7c7ef5; +} +.card-checkbox { + position: absolute; + top: 6px; + left: 6px; + z-index: 2; + opacity: 0; + transition: opacity 0.1s; +} +.card-checkbox input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5)); +} +.media-card:hover .card-checkbox, +.media-card.selected .card-checkbox { + opacity: 1; +} +.card-thumbnail { + width: 100%; + aspect-ratio: 1; + background: #111118; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + position: relative; +} +.card-thumbnail img, +.card-thumbnail .card-thumb-img { + width: 100%; + height: 100%; + object-fit: cover; + position: absolute; + top: 0; + left: 0; + z-index: 1; +} +.card-type-icon { + font-size: 32px; + opacity: 0.4; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + z-index: 0; +} +.card-info { + padding: 8px 10px; +} +.card-name { + font-size: 12px; + font-weight: 500; + color: #dcdce4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 4px; +} +.card-title, +.card-artist { + font-size: 10px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 1.3; +} +.card-meta { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 6px; + font-size: 10px; +} +.card-size { + color: #6c6c84; + font-size: 10px; +} +.table-thumb-cell { + width: 36px; + padding: 4px 6px !important; + position: relative; +} +.table-thumb { + width: 28px; + height: 28px; + object-fit: cover; + border-radius: 3px; + display: block; +} +.table-thumb-overlay { + position: absolute; + top: 4px; + left: 6px; + z-index: 1; +} +.table-type-icon { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + font-size: 14px; + opacity: 0.5; + border-radius: 3px; + background: #111118; + z-index: 0; +} +.type-badge { + display: inline-block; + padding: 1px 6px; + border-radius: 3px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; +} +.type-badge.type-audio { + background: rgba(139, 92, 246, 0.1); + color: #9d8be0; +} +.type-badge.type-video { + background: rgba(200, 72, 130, 0.1); + color: #d07eaa; +} +.type-badge.type-image { + background: rgba(34, 160, 80, 0.1); + color: #5cb97a; +} +.type-badge.type-document { + background: rgba(59, 120, 200, 0.1); + color: #6ca0d4; +} +.type-badge.type-text { + background: rgba(200, 160, 36, 0.1); + color: #c4a840; +} +.type-badge.type-other { + background: rgba(128, 128, 160, 0.08); + color: #6c6c84; +} +.tag-list { + display: flex; + flex-wrap: wrap; + gap: 4px; +} +.tag-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 10px; + background: rgba(124, 126, 245, 0.15); + color: #9698f7; + border-radius: 12px; + font-size: 11px; + font-weight: 500; +} +.tag-badge.selected { + background: #7c7ef5; + color: #fff; + cursor: pointer; +} +.tag-badge:not(.selected) { + cursor: pointer; +} +.tag-badge .tag-remove { + cursor: pointer; + opacity: 0.4; + font-size: 13px; + line-height: 1; + transition: opacity 0.1s; +} +.tag-badge .tag-remove:hover { + opacity: 1; +} +.tag-group { + margin-bottom: 6px; +} +.tag-children { + margin-left: 16px; + margin-top: 4px; + display: flex; + flex-wrap: wrap; + gap: 4px; +} +.tag-confirm-delete { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 10px; + color: #a0a0b8; +} +.tag-confirm-yes { + cursor: pointer; + color: #e45858; + font-weight: 600; +} +.tag-confirm-yes:hover { + text-decoration: underline; +} +.tag-confirm-no { + cursor: pointer; + color: #6c6c84; + font-weight: 500; +} +.tag-confirm-no:hover { + text-decoration: underline; +} +.detail-actions { + display: flex; + gap: 6px; + margin-bottom: 16px; +} +.detail-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} +.detail-field { + padding: 10px 12px; + background: #111118; + border-radius: 3px; + border: 1px solid rgba(255, 255, 255, 0.06); +} +.detail-field.full-width { + grid-column: 1/-1; +} +.detail-field input[type="text"], +.detail-field textarea, +.detail-field select { + width: 100%; + margin-top: 4px; +} +.detail-field textarea { + min-height: 64px; + resize: vertical; +} +.detail-label { + font-size: 10px; + font-weight: 600; + color: #6c6c84; + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 2px; +} +.detail-value { + font-size: 13px; + color: #dcdce4; + word-break: break-all; +} +.detail-value.mono { + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; + font-size: 11px; + color: #a0a0b8; +} +.detail-preview { + margin-bottom: 16px; + background: #111118; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 5px; + overflow: hidden; + text-align: center; +} +.detail-preview:has(.markdown-viewer) { + max-height: none; + overflow-y: auto; + text-align: left; +} +.detail-preview:not(:has(.markdown-viewer)) { + max-height: 450px; +} +.detail-preview img { + max-width: 100%; + max-height: 400px; + object-fit: contain; + display: block; + margin: 0 auto; +} +.detail-preview audio { + width: 100%; + padding: 16px; +} +.detail-preview video { + max-width: 100%; + max-height: 400px; + display: block; + margin: 0 auto; +} +.detail-no-preview { + padding: 16px 16px; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; +} +.frontmatter-card { + max-width: 800px; + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + padding: 12px 16px; + margin-bottom: 16px; +} +.frontmatter-fields { + display: grid; + grid-template-columns: auto 1fr; + gap: 4px 12px; + margin: 0; +} +.frontmatter-fields dt { + font-weight: 600; + font-size: 12px; + color: #a0a0b8; + text-transform: capitalize; +} +.frontmatter-fields dd { + font-size: 13px; + color: #dcdce4; + margin: 0; +} +.empty-state { + text-align: center; + padding: 48px 12px; + color: #6c6c84; +} +.empty-state .empty-icon { + font-size: 32px; + margin-bottom: 12px; + opacity: 0.3; +} +.empty-title { + font-size: 15px; + font-weight: 600; + color: #a0a0b8; + margin-bottom: 4px; +} +.empty-subtitle { + font-size: 12px; + max-width: 320px; + margin: 0 auto; + line-height: 1.5; +} +.toast-container { + position: fixed; + bottom: 16px; + right: 16px; + z-index: 300; + display: flex; + flex-direction: column-reverse; + gap: 6px; + align-items: flex-end; +} +.toast-container .toast { + position: static; + transform: none; +} +.toast { + position: fixed; + bottom: 16px; + right: 16px; + padding: 10px 16px; + border-radius: 5px; + background: #26263a; + border: 1px solid rgba(255, 255, 255, 0.09); + color: #dcdce4; + font-size: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); + z-index: 300; + animation: slide-up 0.15s ease-out; + max-width: 420px; +} +.toast.success { + border-left: 3px solid #3ec97a; +} +.toast.error { + border-left: 3px solid #e45858; +} +.offline-banner, +.error-banner { + background: rgba(228, 88, 88, 0.06); + border: 1px solid rgba(228, 88, 88, 0.2); + border-radius: 3px; + padding: 10px 12px; + margin-bottom: 12px; + font-size: 12px; + color: #d47070; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; +} +.offline-banner .offline-icon, +.offline-banner .error-icon, +.error-banner .offline-icon, +.error-banner .error-icon { + font-size: 14px; + flex-shrink: 0; +} +.error-banner { + padding: 10px 14px; +} +.readonly-banner { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + padding: 10px 12px; + background: rgba(212, 160, 55, 0.06); + border: 1px solid rgba(212, 160, 55, 0.15); + border-radius: 3px; + margin-bottom: 16px; + font-size: 12px; + color: #d4a037; +} +.batch-actions { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + padding: 8px 10px; + background: rgba(124, 126, 245, 0.15); + border: 1px solid rgba(124, 126, 245, 0.2); + border-radius: 3px; + margin-bottom: 12px; + font-size: 12px; + font-weight: 500; + color: #9698f7; +} +.select-all-banner { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 8px; + padding: 10px 16px; + background: rgba(99, 102, 241, 0.08); + border-radius: 6px; + margin-bottom: 8px; + font-size: 0.85rem; + color: #a0a0b8; +} +.select-all-banner button { + background: none; + border: none; + color: #7c7ef5; + cursor: pointer; + font-weight: 600; + text-decoration: underline; + font-size: 0.85rem; + padding: 0; +} +.select-all-banner button:hover { + color: #dcdce4; +} +.import-status-panel { + background: #1f1f28; + border: 1px solid #7c7ef5; + border-radius: 5px; + padding: 12px 16px; + margin-bottom: 16px; +} +.import-status-header { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + margin-bottom: 8px; + font-size: 13px; + color: #dcdce4; +} +.import-current-file { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 2px; + margin-bottom: 6px; + font-size: 12px; + overflow: hidden; +} +.import-file-label { + color: #6c6c84; + flex-shrink: 0; +} +.import-file-name { + color: #dcdce4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-family: monospace; + font-size: 11px; +} +.import-queue-indicator { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 2px; + margin-bottom: 8px; + font-size: 11px; +} +.import-queue-badge { + display: flex; + align-items: center; + justify-content: center; + min-width: 18px; + height: 18px; + padding: 0 6px; + background: rgba(124, 126, 245, 0.15); + color: #9698f7; + border-radius: 9px; + font-weight: 600; + font-size: 10px; +} +.import-queue-text { + color: #6c6c84; +} +.import-tabs { + display: flex; + gap: 0; + margin-bottom: 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.09); +} +.import-tab { + padding: 10px 16px; + background: none; + border: none; + border-bottom: 2px solid rgba(0, 0, 0, 0); + color: #6c6c84; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: + color 0.1s, + border-color 0.1s; +} +.import-tab:hover { + color: #dcdce4; +} +.import-tab.active { + color: #9698f7; + border-bottom-color: #7c7ef5; +} +.queue-panel { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + border-left: 1px solid rgba(255, 255, 255, 0.09); + background: #18181f; + min-width: 280px; + max-width: 320px; +} +.queue-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} +.queue-header h3 { + margin: 0; + font-size: 0.9rem; + color: #dcdce4; +} +.queue-controls { + display: flex; + gap: 2px; +} +.queue-list { + overflow-y: auto; + flex: 1; +} +.queue-item { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + padding: 8px 16px; + cursor: pointer; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + transition: background 0.15s; +} +.queue-item:hover { + background: #1f1f28; +} +.queue-item:hover .queue-item-remove { + opacity: 1; +} +.queue-item-active { + background: rgba(124, 126, 245, 0.15); + border-left: 3px solid #7c7ef5; +} +.queue-item-info { + flex: 1; + min-width: 0; +} +.queue-item-title { + display: block; + font-size: 0.85rem; + color: #dcdce4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.queue-item-artist { + display: block; + font-size: 0.75rem; + color: #6c6c84; +} +.queue-item-remove { + opacity: 0; + transition: opacity 0.15s; +} +.queue-empty { + padding: 16px 16px; + text-align: center; + color: #6c6c84; + font-size: 0.85rem; +} +.statistics-page { + padding: 20px; +} +.stats-overview, +.stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + margin-bottom: 24px; +} +@media (max-width: 768px) { + .stats-overview, + .stats-grid { + grid-template-columns: 1fr; + } +} +.stat-card { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + padding: 20px; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 16px; +} +.stat-card.stat-primary { + border-left: 3px solid #7c7ef5; +} +.stat-card.stat-success { + border-left: 3px solid #3ec97a; +} +.stat-card.stat-info { + border-left: 3px solid #6ca0d4; +} +.stat-card.stat-warning { + border-left: 3px solid #d4a037; +} +.stat-card.stat-purple { + border-left: 3px solid #9d8be0; +} +.stat-card.stat-danger { + border-left: 3px solid #e45858; +} +.stat-icon { + flex-shrink: 0; + color: #6c6c84; +} +.stat-content { + flex: 1; +} +.stat-value { + font-size: 28px; + font-weight: 700; + color: #dcdce4; + line-height: 1.2; + font-variant-numeric: tabular-nums; +} +.stat-label { + font-size: 12px; + color: #6c6c84; + margin-top: 4px; + font-weight: 500; +} +.stats-section { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + padding: 16px; + margin-bottom: 20px; +} +.section-title { + font-size: 16px; + font-weight: 600; + color: #dcdce4; + margin-bottom: 20px; +} +.section-title.small { + font-size: 14px; + margin-bottom: 12px; + padding-bottom: 6px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} +.chart-bars { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 16px; +} +.bar-item { + display: grid; + grid-template-columns: 120px 1fr 80px; + align-items: center; + gap: 16px; +} +.bar-label { + font-size: 13px; + font-weight: 500; + color: #a0a0b8; + text-align: right; +} +.bar-track { + height: 28px; + background: #26263a; + border-radius: 3px; + overflow: hidden; + position: relative; +} +.bar-fill { + height: 100%; + transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: 3px; +} +.bar-fill.bar-primary { + background: linear-gradient(90deg, #7c7ef5 0%, #7c7ef3 100%); +} +.bar-fill.bar-success { + background: linear-gradient(90deg, #3ec97a 0%, #66bb6a 100%); +} +.bar-value { + font-size: 13px; + font-weight: 600; + color: #a0a0b8; + text-align: right; + font-variant-numeric: tabular-nums; +} +.settings-section { + margin-bottom: 16px; +} +.settings-card { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 7px; + padding: 20px; + margin-bottom: 16px; +} +.settings-card.danger-card { + border: 1px solid rgba(228, 88, 88, 0.25); +} +.settings-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} +.settings-card-title { + font-size: 14px; + font-weight: 600; +} +.settings-card-body { + padding-top: 2px; +} +.settings-field { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} +.settings-field:last-child { + border-bottom: none; +} +.settings-field select { + min-width: 120px; +} +.config-path { + font-size: 11px; + color: #6c6c84; + margin-bottom: 12px; + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; + padding: 6px 10px; + background: #111118; + border-radius: 3px; + border: 1px solid rgba(255, 255, 255, 0.06); +} +.config-status { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 6px; + padding: 3px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; +} +.config-status.writable { + background: rgba(62, 201, 122, 0.1); + color: #3ec97a; +} +.config-status.readonly { + background: rgba(228, 88, 88, 0.1); + color: #e45858; +} +.root-list { + list-style: none; +} +.root-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: #111118; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 3px; + margin-bottom: 4px; + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; + font-size: 12px; + color: #a0a0b8; +} +.info-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + font-size: 13px; +} +.info-row:last-child { + border-bottom: none; +} +.info-label { + color: #a0a0b8; + font-weight: 500; +} +.info-value { + color: #dcdce4; +} +.tasks-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + gap: 16px; + padding: 12px; +} +.task-card { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + overflow: hidden; + transition: all 0.2s; +} +.task-card:hover { + border-color: rgba(255, 255, 255, 0.14); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transform: translateY(-2px); +} +.task-card-enabled { + border-left: 3px solid #3ec97a; +} +.task-card-disabled { + border-left: 3px solid #4a4a5e; + opacity: 0.7; +} +.task-card-header { + display: flex; + justify-content: space-between; + align-items: center; + align-items: flex-start; + padding: 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} +.task-header-left { + flex: 1; + min-width: 0; +} +.task-name { + font-size: 16px; + font-weight: 600; + color: #dcdce4; + margin-bottom: 2px; +} +.task-schedule { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 6px; + font-size: 12px; + color: #6c6c84; + font-family: "Menlo", "Monaco", "Courier New", monospace; +} +.schedule-icon { + font-size: 14px; +} +.task-status-badge { + flex-shrink: 0; +} +.status-badge { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 6px; + padding: 2px 10px; + border-radius: 3px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} +.status-badge.status-enabled { + background: rgba(76, 175, 80, 0.12); + color: #3ec97a; +} +.status-badge.status-enabled .status-dot { + animation: pulse 1.5s infinite; +} +.status-badge.status-disabled { + background: #26263a; + color: #6c6c84; +} +.status-badge .status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + background: currentColor; +} +.task-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; + padding: 16px; +} +.task-info-item { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + gap: 10px; +} +.task-info-icon { + font-size: 18px; + color: #6c6c84; + flex-shrink: 0; +} +.task-info-content { + flex: 1; + min-width: 0; +} +.task-info-label { + font-size: 10px; + color: #6c6c84; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + margin-bottom: 2px; +} +.task-info-value { + font-size: 12px; + color: #a0a0b8; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.task-card-actions { + display: flex; + gap: 8px; + padding: 10px 16px; + background: #18181f; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} +.task-card-actions button { + flex: 1; +} +.db-actions { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 16px; + padding: 10px; +} +.db-action-row { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 10px; + border-radius: 6px; + background: rgba(0, 0, 0, 0.06); +} +.db-action-info { + flex: 1; +} +.db-action-info h4 { + font-size: 0.95rem; + font-weight: 600; + color: #dcdce4; + margin-bottom: 2px; +} +.db-action-confirm { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + flex-shrink: 0; +} +.library-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0; + margin-bottom: 12px; + gap: 12px; + flex-wrap: wrap; +} +.toolbar-left { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 10px; +} +.toolbar-right { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 10px; +} +.sort-control select, +.page-size-control select { + padding: 4px 24px 4px 8px; + font-size: 11px; + background: #1f1f28; +} +.page-size-control { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 4px; +} +.library-stats { + display: flex; + justify-content: space-between; + align-items: center; + padding: 2px 0 6px 0; + font-size: 11px; +} +.type-filter-row { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 6px; + padding: 4px 0; + margin-bottom: 6px; + flex-wrap: wrap; +} +.pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + margin-top: 16px; + padding: 8px 0; +} +.page-btn { + min-width: 28px; + text-align: center; + font-variant-numeric: tabular-nums; +} +.page-ellipsis { + color: #6c6c84; + padding: 0 4px; + font-size: 12px; + user-select: none; +} +.audit-controls { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} +.filter-select { + padding: 4px 24px 4px 8px; + font-size: 11px; + background: #1f1f28; +} +.action-danger { + background: rgba(228, 88, 88, 0.1); + color: #d47070; +} +.action-updated { + background: rgba(59, 120, 200, 0.1); + color: #6ca0d4; +} +.action-collection { + background: rgba(34, 160, 80, 0.1); + color: #5cb97a; +} +.action-collection-remove { + background: rgba(212, 160, 55, 0.1); + color: #c4a840; +} +.action-opened { + background: rgba(139, 92, 246, 0.1); + color: #9d8be0; +} +.action-scanned { + background: rgba(128, 128, 160, 0.08); + color: #6c6c84; +} +.clickable { + cursor: pointer; + color: #9698f7; +} +.clickable:hover { + text-decoration: underline; +} +.clickable-row { + cursor: pointer; +} +.clickable-row:hover { + background: rgba(255, 255, 255, 0.03); +} +.duplicates-view { + padding: 0; +} +.duplicates-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} +.duplicates-header h3 { + margin: 0; +} +.duplicates-summary { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 12px; +} +.duplicate-group { + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + margin-bottom: 8px; + overflow: hidden; +} +.duplicate-group-header { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 12px; + width: 100%; + padding: 10px 14px; + background: #1f1f28; + border: none; + cursor: pointer; + text-align: left; + color: #dcdce4; + font-size: 13px; +} +.duplicate-group-header:hover { + background: #26263a; +} +.expand-icon { + font-size: 10px; + width: 14px; + flex-shrink: 0; +} +.group-name { + font-weight: 600; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.group-badge { + background: #7c7ef5; + color: #fff; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 600; + flex-shrink: 0; +} +.group-size { + flex-shrink: 0; + font-size: 12px; +} +.group-hash { + font-size: 11px; + flex-shrink: 0; +} +.duplicate-items { + border-top: 1px solid rgba(255, 255, 255, 0.09); +} +.duplicate-item { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 12px; + padding: 10px 14px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} +.duplicate-item:last-child { + border-bottom: none; +} +.duplicate-item-keep { + background: rgba(76, 175, 80, 0.06); +} +.dup-thumb { + width: 48px; + height: 48px; + flex-shrink: 0; + border-radius: 3px; + overflow: hidden; +} +.dup-thumb-img { + width: 100%; + height: 100%; + object-fit: cover; +} +.dup-thumb-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background: #26263a; + font-size: 20px; + color: #6c6c84; +} +.dup-info { + flex: 1; + min-width: 0; +} +.dup-filename { + font-weight: 600; + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.dup-path { + font-size: 11px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.dup-meta { + font-size: 12px; + margin-top: 2px; +} +.dup-actions { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 6px; + flex-shrink: 0; +} +.keep-badge { + background: rgba(76, 175, 80, 0.12); + color: #4caf50; + padding: 2px 10px; + border-radius: 10px; + font-size: 11px; + font-weight: 600; +} +.saved-searches-list { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 4px; + max-height: 300px; + overflow-y: auto; +} +.saved-search-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + background: #18181f; + border-radius: 3px; + cursor: pointer; + transition: background 0.15s ease; +} +.saved-search-item:hover { + background: #1f1f28; +} +.saved-search-info { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 2px; + flex: 1; + min-width: 0; +} +.saved-search-name { + font-weight: 500; + color: #dcdce4; +} +.saved-search-query { + font-size: 11px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.backlinks-panel, +.outgoing-links-panel { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + margin-top: 16px; + overflow: hidden; +} +.backlinks-header, +.outgoing-links-header { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + padding: 10px 14px; + background: #26263a; + cursor: pointer; + user-select: none; + transition: background 0.1s; +} +.backlinks-header:hover, +.outgoing-links-header:hover { + background: rgba(255, 255, 255, 0.04); +} +.backlinks-toggle, +.outgoing-links-toggle { + font-size: 10px; + color: #6c6c84; + width: 12px; + text-align: center; +} +.backlinks-title, +.outgoing-links-title { + font-size: 12px; + font-weight: 600; + color: #dcdce4; + flex: 1; +} +.backlinks-count, +.outgoing-links-count { + font-size: 11px; + color: #6c6c84; +} +.backlinks-reindex-btn { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + margin-left: auto; + background: rgba(0, 0, 0, 0); + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 3px; + color: #6c6c84; + font-size: 12px; + cursor: pointer; + transition: + background 0.1s, + color 0.1s, + border-color 0.1s; +} +.backlinks-reindex-btn:hover:not(:disabled) { + background: #1f1f28; + color: #dcdce4; + border-color: rgba(255, 255, 255, 0.14); +} +.backlinks-reindex-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.backlinks-content, +.outgoing-links-content { + padding: 12px; + border-top: 1px solid rgba(255, 255, 255, 0.06); +} +.backlinks-loading, +.outgoing-links-loading { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + padding: 12px; + color: #6c6c84; + font-size: 12px; +} +.backlinks-error, +.outgoing-links-error { + padding: 8px 12px; + background: rgba(228, 88, 88, 0.06); + border: 1px solid rgba(228, 88, 88, 0.2); + border-radius: 3px; + font-size: 12px; + color: #e45858; +} +.backlinks-empty, +.outgoing-links-empty { + padding: 16px; + text-align: center; + color: #6c6c84; + font-size: 12px; + font-style: italic; +} +.backlinks-list, +.outgoing-links-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 6px; +} +.backlink-item, +.outgoing-link-item { + padding: 10px 12px; + background: #111118; + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 3px; + cursor: pointer; + transition: + background 0.1s, + border-color 0.1s; +} +.backlink-item:hover, +.outgoing-link-item:hover { + background: #18181f; + border-color: rgba(255, 255, 255, 0.09); +} +.backlink-item.unresolved, +.outgoing-link-item.unresolved { + opacity: 0.7; + border-style: dashed; +} +.backlink-source, +.outgoing-link-target { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + margin-bottom: 2px; +} +.backlink-title, +.outgoing-link-text { + font-size: 13px; + font-weight: 500; + color: #dcdce4; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.backlink-type-badge, +.outgoing-link-type-badge { + display: inline-block; + padding: 1px 6px; + border-radius: 12px; + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; +} +.backlink-type-badge.backlink-type-wikilink, +.backlink-type-badge.link-type-wikilink, +.outgoing-link-type-badge.backlink-type-wikilink, +.outgoing-link-type-badge.link-type-wikilink { + background: rgba(124, 126, 245, 0.15); + color: #9698f7; +} +.backlink-type-badge.backlink-type-embed, +.backlink-type-badge.link-type-embed, +.outgoing-link-type-badge.backlink-type-embed, +.outgoing-link-type-badge.link-type-embed { + background: rgba(139, 92, 246, 0.1); + color: #9d8be0; +} +.backlink-type-badge.backlink-type-markdown_link, +.backlink-type-badge.link-type-markdown_link, +.outgoing-link-type-badge.backlink-type-markdown_link, +.outgoing-link-type-badge.link-type-markdown_link { + background: rgba(59, 120, 200, 0.1); + color: #6ca0d4; +} +.backlink-context { + font-size: 11px; + color: #6c6c84; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} +.backlink-line { + color: #a0a0b8; + font-weight: 500; +} +.unresolved-badge { + padding: 1px 6px; + border-radius: 12px; + font-size: 9px; + font-weight: 600; + background: rgba(212, 160, 55, 0.1); + color: #d4a037; +} +.outgoing-links-unresolved-badge { + margin-left: 8px; + padding: 2px 8px; + border-radius: 10px; + font-size: 10px; + font-weight: 500; + background: rgba(212, 160, 55, 0.12); + color: #d4a037; +} +.outgoing-links-global-unresolved { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 6px; + margin-top: 12px; + padding: 10px 12px; + background: rgba(212, 160, 55, 0.06); + border: 1px solid rgba(212, 160, 55, 0.15); + border-radius: 3px; + font-size: 11px; + color: #6c6c84; +} +.outgoing-links-global-unresolved .unresolved-icon { + color: #d4a037; +} +.backlinks-message { + padding: 8px 10px; + margin-bottom: 10px; + border-radius: 3px; + font-size: 11px; +} +.backlinks-message.success { + background: rgba(62, 201, 122, 0.08); + border: 1px solid rgba(62, 201, 122, 0.2); + color: #3ec97a; +} +.backlinks-message.error { + background: rgba(228, 88, 88, 0.06); + border: 1px solid rgba(228, 88, 88, 0.2); + color: #e45858; +} +.graph-view { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + height: 100%; + background: #18181f; + border-radius: 5px; + overflow: hidden; +} +.graph-toolbar { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 16px; + padding: 12px 16px; + background: #1f1f28; + border-bottom: 1px solid rgba(255, 255, 255, 0.09); +} +.graph-title { + font-size: 14px; + font-weight: 600; + color: #dcdce4; +} +.graph-controls { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 8px; + font-size: 12px; + color: #a0a0b8; +} +.graph-controls select { + padding: 4px 20px 4px 8px; + font-size: 11px; + background: #26263a; +} +.graph-stats { + margin-left: auto; + font-size: 11px; + color: #6c6c84; +} +.graph-container { + flex: 1; + position: relative; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + background: #111118; +} +.graph-loading, +.graph-error, +.graph-empty { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; + padding: 48px; + color: #6c6c84; + font-size: 13px; + text-align: center; +} +.graph-svg { + max-width: 100%; + max-height: 100%; + cursor: grab; +} +.graph-svg-container { + position: relative; + width: 100%; + height: 100%; +} +.graph-zoom-controls { + position: absolute; + top: 16px; + left: 16px; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 8px; + z-index: 5; +} +.zoom-btn { + width: 36px; + height: 36px; + border-radius: 6px; + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + color: #dcdce4; + font-size: 18px; + font-weight: bold; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.15s; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} +.zoom-btn:hover { + background: #26263a; + border-color: rgba(255, 255, 255, 0.14); + transform: scale(1.05); +} +.zoom-btn:active { + transform: scale(0.95); +} +.graph-edges line { + stroke: rgba(255, 255, 255, 0.14); + stroke-width: 1; + opacity: 0.6; +} +.graph-edges line.edge-type-wikilink { + stroke: #7c7ef5; +} +.graph-edges line.edge-type-embed { + stroke: #9d8be0; + stroke-dasharray: 4 2; +} +.graph-nodes .graph-node { + cursor: pointer; +} +.graph-nodes .graph-node circle { + fill: #4caf50; + stroke: #388e3c; + stroke-width: 2; + transition: + fill 0.15s, + stroke 0.15s; +} +.graph-nodes .graph-node:hover circle { + fill: #66bb6a; +} +.graph-nodes .graph-node.selected circle { + fill: #7c7ef5; + stroke: #5456d6; +} +.graph-nodes .graph-node text { + fill: #a0a0b8; + font-size: 11px; + pointer-events: none; + text-anchor: middle; + dominant-baseline: central; + transform: translateY(16px); +} +.node-details-panel { + position: absolute; + top: 16px; + right: 16px; + width: 280px; + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); + z-index: 10; +} +.node-details-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 14px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} +.node-details-header h3 { + font-size: 13px; + font-weight: 600; + color: #dcdce4; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.node-details-header .close-btn { + background: none; + border: none; + color: #6c6c84; + cursor: pointer; + font-size: 14px; + padding: 2px 6px; + line-height: 1; +} +.node-details-header .close-btn:hover { + color: #dcdce4; +} +.node-details-content { + padding: 14px; +} +.node-details-content .node-title { + font-size: 12px; + color: #a0a0b8; + margin-bottom: 12px; +} +.node-stats { + display: flex; + gap: 16px; + margin-bottom: 12px; +} +.node-stats .stat { + font-size: 12px; + color: #6c6c84; +} +.node-stats .stat strong { + color: #dcdce4; +} +.physics-controls-panel { + position: absolute; + top: 16px; + right: 16px; + width: 300px; + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); + padding: 16px; + z-index: 10; +} +.physics-controls-panel h4 { + font-size: 13px; + font-weight: 600; + color: #dcdce4; + margin: 0 0 16px 0; + padding-bottom: 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} +.physics-controls-panel .btn { + width: 100%; + margin-top: 8px; +} +.control-group { + margin-bottom: 14px; +} +.control-group label { + display: block; + font-size: 11px; + font-weight: 500; + color: #a0a0b8; + margin-bottom: 6px; + text-transform: uppercase; + letter-spacing: 0.5px; +} +.control-group input[type="range"] { + width: 100%; + height: 4px; + border-radius: 4px; + background: #26263a; + outline: none; + -webkit-appearance: none; +} +.control-group input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + border-radius: 50%; + background: #7c7ef5; + cursor: pointer; + transition: transform 0.1s; +} +.control-group input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.15); +} +.control-group input[type="range"]::-moz-range-thumb { + width: 14px; + height: 14px; + border-radius: 50%; + background: #7c7ef5; + cursor: pointer; + border: none; + transition: transform 0.1s; +} +.control-group input[type="range"]::-moz-range-thumb:hover { + transform: scale(1.15); +} +.control-value { + display: inline-block; + margin-top: 2px; + font-size: 11px; + color: #6c6c84; + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; +} +.theme-light { + --bg-0: #f5f5f7; + --bg-1: #eeeef0; + --bg-2: #fff; + --bg-3: #e8e8ec; + --border-subtle: rgba(0, 0, 0, 0.06); + --border: rgba(0, 0, 0, 0.1); + --border-strong: rgba(0, 0, 0, 0.16); + --text-0: #1a1a2e; + --text-1: #555570; + --text-2: #8888a0; + --accent: #6366f1; + --accent-dim: rgba(99, 102, 241, 0.1); + --accent-text: #4f52e8; + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); + --shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 4px 20px rgba(0, 0, 0, 0.12); +} +.theme-light ::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.12); +} +.theme-light ::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.08); +} +.theme-light ::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.06); +} +.theme-light .graph-nodes .graph-node text { + fill: #1a1a2e; +} +.theme-light .graph-edges line { + stroke: rgba(0, 0, 0, 0.12); +} +.theme-light .pdf-container { + background: #e8e8ec; +} +.skeleton-pulse { + animation: skeleton-pulse 1.5s ease-in-out infinite; + background: #26263a; + border-radius: 4px; +} +.skeleton-card { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 8px; + padding: 8px; +} +.skeleton-thumb { + width: 100%; + aspect-ratio: 1; + border-radius: 6px; +} +.skeleton-text { + height: 14px; + width: 80%; +} +.skeleton-text-short { + width: 50%; +} +.skeleton-row { + display: flex; + gap: 12px; + padding: 10px 16px; + align-items: center; +} +.skeleton-cell { + height: 14px; + flex: 1; + border-radius: 4px; +} +.skeleton-cell-icon { + width: 32px; + height: 32px; + flex: none; + border-radius: 4px; +} +.skeleton-cell-wide { + flex: 3; +} +.loading-overlay { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; + background: rgba(0, 0, 0, 0.3); + z-index: 100; + border-radius: 8px; +} +.loading-spinner { + width: 32px; + height: 32px; + border: 3px solid rgba(255, 255, 255, 0.09); + border-top-color: #7c7ef5; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} +.loading-message { + color: #a0a0b8; + font-size: 0.9rem; +} +.login-container { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + background: #111118; +} +.login-card { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 7px; + padding: 24px; + width: 360px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); +} +.login-title { + font-size: 20px; + font-weight: 700; + color: #dcdce4; + text-align: center; + margin-bottom: 2px; +} +.login-subtitle { + font-size: 13px; + color: #6c6c84; + text-align: center; + margin-bottom: 20px; +} +.login-error { + background: rgba(228, 88, 88, 0.08); + border: 1px solid rgba(228, 88, 88, 0.2); + border-radius: 3px; + padding: 8px 12px; + margin-bottom: 12px; + font-size: 12px; + color: #e45858; +} +.login-form input[type="text"], +.login-form input[type="password"] { + width: 100%; +} +.login-btn { + width: 100%; + padding: 8px 16px; + font-size: 13px; + margin-top: 2px; +} +.pagination { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 2px; + margin-top: 16px; + padding: 8px 0; +} +.page-btn { + min-width: 28px; + text-align: center; + font-variant-numeric: tabular-nums; +} +.page-ellipsis { + color: #6c6c84; + padding: 0 4px; + font-size: 12px; + user-select: none; +} +.help-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + animation: fade-in 0.1s ease-out; +} +.help-dialog { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 7px; + padding: 16px; + min-width: 300px; + max-width: 400px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); +} +.help-dialog h3 { + font-size: 16px; + font-weight: 600; + margin-bottom: 16px; +} +.help-shortcuts { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + gap: 8px; + margin-bottom: 16px; +} +.shortcut-row { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + gap: 12px; +} +.shortcut-row kbd { + display: inline-block; + padding: 2px 8px; + background: #111118; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 3px; + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; + font-size: 11px; + color: #dcdce4; + min-width: 32px; + text-align: center; +} +.shortcut-row span { + font-size: 13px; + color: #a0a0b8; +} +.help-close { + display: block; + width: 100%; + padding: 6px 12px; + background: #26263a; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 3px; + color: #dcdce4; + font-size: 12px; + cursor: pointer; + text-align: center; +} +.help-close:hover { + background: rgba(255, 255, 255, 0.06); +} +.plugin-page { + padding: 16px 24px; + max-width: 100%; + overflow-x: hidden; +} +.plugin-page-title { + font-size: 14px; + font-weight: 600; + color: #dcdce4; + margin: 0 0 16px; +} +.plugin-container { + display: flex; + flex-direction: column; + gap: var(--plugin-gap, 0px); + padding: var(--plugin-padding, 0); +} +.plugin-grid { + display: grid; + grid-template-columns: repeat(var(--plugin-columns, 1), 1fr); + gap: var(--plugin-gap, 0px); +} +.plugin-flex { + display: flex; + gap: var(--plugin-gap, 0px); +} +.plugin-flex[data-direction="row"] { + flex-direction: row; +} +.plugin-flex[data-direction="column"] { + flex-direction: column; +} +.plugin-flex[data-justify="flex-start"] { + justify-content: flex-start; +} +.plugin-flex[data-justify="flex-end"] { + justify-content: flex-end; +} +.plugin-flex[data-justify="center"] { + justify-content: center; +} +.plugin-flex[data-justify="space-between"] { + justify-content: space-between; +} +.plugin-flex[data-justify="space-around"] { + justify-content: space-around; +} +.plugin-flex[data-justify="space-evenly"] { + justify-content: space-evenly; +} +.plugin-flex[data-align="flex-start"] { + align-items: flex-start; +} +.plugin-flex[data-align="flex-end"] { + align-items: flex-end; +} +.plugin-flex[data-align="center"] { + align-items: center; +} +.plugin-flex[data-align="stretch"] { + align-items: stretch; +} +.plugin-flex[data-align="baseline"] { + align-items: baseline; +} +.plugin-flex[data-wrap="wrap"] { + flex-wrap: wrap; +} +.plugin-flex[data-wrap="nowrap"] { + flex-wrap: nowrap; +} +.plugin-split { + display: flex; +} +.plugin-split-sidebar { + width: var(--plugin-sidebar-width, 200px); + flex-shrink: 0; +} +.plugin-split-main { + flex: 1; + min-width: 0; +} +.plugin-card { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 7px; + overflow: hidden; +} +.plugin-card-header { + padding: 12px 16px; + font-size: 12px; + font-weight: 600; + color: #dcdce4; + border-bottom: 1px solid rgba(255, 255, 255, 0.09); + background: #26263a; +} +.plugin-card-content { + padding: 16px; +} +.plugin-card-footer { + padding: 12px 16px; + border-top: 1px solid rgba(255, 255, 255, 0.09); + background: #18181f; +} +.plugin-heading { + color: #dcdce4; + margin: 0; + line-height: 1.2; +} +.plugin-heading.level-1 { + font-size: 28px; + font-weight: 700; +} +.plugin-heading.level-2 { + font-size: 18px; + font-weight: 600; +} +.plugin-heading.level-3 { + font-size: 16px; + font-weight: 600; +} +.plugin-heading.level-4 { + font-size: 14px; + font-weight: 500; +} +.plugin-heading.level-5 { + font-size: 13px; + font-weight: 500; +} +.plugin-heading.level-6 { + font-size: 12px; + font-weight: 500; +} +.plugin-text { + margin: 0; + font-size: 12px; + color: #dcdce4; + line-height: 1.4; +} +.plugin-text.text-secondary { + color: #a0a0b8; +} +.plugin-text.text-error { + color: #d47070; +} +.plugin-text.text-success { + color: #3ec97a; +} +.plugin-text.text-warning { + color: #d4a037; +} +.plugin-text.text-bold { + font-weight: 600; +} +.plugin-text.text-italic { + font-style: italic; +} +.plugin-text.text-small { + font-size: 10px; +} +.plugin-text.text-large { + font-size: 15px; +} +.plugin-code { + background: #18181f; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + padding: 16px 24px; + font-family: "JetBrains Mono", "Fira Code", ui-monospace, monospace; + font-size: 12px; + color: #dcdce4; + overflow-x: auto; + white-space: pre; +} +.plugin-code code { + font-family: inherit; + font-size: inherit; + color: inherit; +} +.plugin-tabs { + display: flex; + flex-direction: column; +} +.plugin-tab-list { + display: flex; + gap: 2px; + border-bottom: 1px solid rgba(255, 255, 255, 0.09); + margin-bottom: 16px; +} +.plugin-tab { + padding: 8px 20px; + font-size: 12px; + font-weight: 500; + color: #a0a0b8; + background: rgba(0, 0, 0, 0); + border: none; + border-bottom: 2px solid rgba(0, 0, 0, 0); + cursor: pointer; + transition: + color 0.1s, + border-color 0.1s; +} +.plugin-tab:hover { + color: #dcdce4; +} +.plugin-tab.active { + color: #9698f7; + border-bottom-color: #7c7ef5; +} +.plugin-tab .tab-icon { + margin-right: 4px; +} +.plugin-tab-panel:not(.active) { + display: none; +} +.plugin-description-list-wrapper { + width: 100%; +} +.plugin-description-list { + display: grid; + grid-template-columns: max-content 1fr; + gap: 4px 16px; + margin: 0; + padding: 0; +} +.plugin-description-list dt { + font-size: 10px; + font-weight: 500; + color: #a0a0b8; + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 6px 0; + white-space: nowrap; +} +.plugin-description-list dd { + font-size: 12px; + color: #dcdce4; + padding: 6px 0; + margin: 0; + word-break: break-word; +} +.plugin-description-list.horizontal { + display: flex; + flex-wrap: wrap; + gap: 16px 24px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); +} +.plugin-description-list.horizontal dt { + width: auto; + padding: 0; +} +.plugin-description-list.horizontal dd { + width: auto; + padding: 0; +} +.plugin-description-list.horizontal dt, +.plugin-description-list.horizontal dd { + display: inline; +} +.plugin-description-list.horizontal dt { + font-size: 9px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #6c6c84; + margin-bottom: 2px; +} +.plugin-description-list.horizontal dd { + font-size: 13px; + font-weight: 600; + color: #dcdce4; +} +.plugin-data-table-wrapper { + overflow-x: auto; +} +.plugin-data-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} +.plugin-data-table thead tr { + border-bottom: 1px solid rgba(255, 255, 255, 0.14); +} +.plugin-data-table thead th { + padding: 8px 12px; + text-align: left; + font-size: 10px; + font-weight: 600; + color: #a0a0b8; + text-transform: uppercase; + letter-spacing: 0.5px; + white-space: nowrap; +} +.plugin-data-table tbody tr { + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + transition: background 0.08s; +} +.plugin-data-table tbody tr:hover { + background: rgba(255, 255, 255, 0.03); +} +.plugin-data-table tbody tr:last-child { + border-bottom: none; +} +.plugin-data-table tbody td { + padding: 8px 12px; + color: #dcdce4; + vertical-align: middle; +} +.plugin-col-constrained { + width: var(--plugin-col-width); +} +.table-filter { + margin-bottom: 12px; +} +.table-filter input { + width: 240px; + padding: 6px 12px; + background: #18181f; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + color: #dcdce4; + font-size: 12px; +} +.table-filter input::placeholder { + color: #6c6c84; +} +.table-filter input:focus { + outline: none; + border-color: #7c7ef5; +} +.table-pagination { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 0; + font-size: 12px; + color: #a0a0b8; +} +.row-actions { + white-space: nowrap; + width: 1%; +} +.row-actions .plugin-button { + padding: 4px 8px; + font-size: 10px; + margin-right: 4px; +} +.plugin-media-grid { + display: grid; + grid-template-columns: repeat(var(--plugin-columns, 2), 1fr); + gap: var(--plugin-gap, 8px); +} +.media-grid-item { + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 7px; + overflow: hidden; + display: flex; + flex-direction: column; +} +.media-grid-img { + width: 100%; + aspect-ratio: 16/9; + object-fit: cover; + display: block; +} +.media-grid-no-img { + width: 100%; + aspect-ratio: 16/9; + background: #26263a; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + color: #6c6c84; +} +.media-grid-caption { + padding: 8px 12px; + font-size: 10px; + color: #dcdce4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.plugin-list { + list-style: none; + margin: 0; + padding: 0; +} +.plugin-list-item { + padding: 8px 0; +} +.plugin-list-divider { + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.06); + margin: 0; +} +.plugin-list-empty { + padding: 16px; + text-align: center; + color: #6c6c84; + font-size: 12px; +} +.plugin-button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: + background 0.08s, + border-color 0.08s, + color 0.08s; + background: #1f1f28; + color: #dcdce4; +} +.plugin-button:disabled { + opacity: 0.45; + cursor: not-allowed; +} +.plugin-button.btn-primary { + background: #7c7ef5; + border-color: #7c7ef5; + color: #fff; +} +.plugin-button.btn-primary:hover:not(:disabled) { + background: #8b8df7; +} +.plugin-button.btn-secondary { + background: #26263a; + border-color: rgba(255, 255, 255, 0.14); + color: #dcdce4; +} +.plugin-button.btn-secondary:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.04); +} +.plugin-button.btn-tertiary { + background: rgba(0, 0, 0, 0); + border-color: rgba(0, 0, 0, 0); + color: #9698f7; +} +.plugin-button.btn-tertiary:hover:not(:disabled) { + background: rgba(124, 126, 245, 0.15); +} +.plugin-button.btn-danger { + background: rgba(0, 0, 0, 0); + border-color: rgba(228, 88, 88, 0.2); + color: #d47070; +} +.plugin-button.btn-danger:hover:not(:disabled) { + background: rgba(228, 88, 88, 0.06); +} +.plugin-button.btn-success { + background: rgba(0, 0, 0, 0); + border-color: rgba(62, 201, 122, 0.2); + color: #3ec97a; +} +.plugin-button.btn-success:hover:not(:disabled) { + background: rgba(62, 201, 122, 0.08); +} +.plugin-button.btn-ghost { + background: rgba(0, 0, 0, 0); + border-color: rgba(0, 0, 0, 0); + color: #a0a0b8; +} +.plugin-button.btn-ghost:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.04); +} +.plugin-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 50%; + font-size: 9px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; +} +.plugin-badge.badge-default, +.plugin-badge.badge-neutral { + background: rgba(255, 255, 255, 0.04); + color: #a0a0b8; +} +.plugin-badge.badge-primary { + background: rgba(124, 126, 245, 0.15); + color: #9698f7; +} +.plugin-badge.badge-secondary { + background: rgba(255, 255, 255, 0.03); + color: #dcdce4; +} +.plugin-badge.badge-success { + background: rgba(62, 201, 122, 0.08); + color: #3ec97a; +} +.plugin-badge.badge-warning { + background: rgba(212, 160, 55, 0.06); + color: #d4a037; +} +.plugin-badge.badge-error { + background: rgba(228, 88, 88, 0.06); + color: #d47070; +} +.plugin-badge.badge-info { + background: rgba(99, 102, 241, 0.08); + color: #9698f7; +} +.plugin-form { + display: flex; + flex-direction: column; + gap: 16px; +} +.form-field { + display: flex; + flex-direction: column; + gap: 6px; +} +.form-field label { + font-size: 12px; + font-weight: 500; + color: #dcdce4; +} +.form-field input, +.form-field textarea, +.form-field select { + padding: 8px 12px; + background: #18181f; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + color: #dcdce4; + font-size: 12px; + font-family: inherit; +} +.form-field input::placeholder, +.form-field textarea::placeholder, +.form-field select::placeholder { + color: #6c6c84; +} +.form-field input:focus, +.form-field textarea:focus, +.form-field select:focus { + outline: none; + border-color: #7c7ef5; + box-shadow: 0 0 0 2px rgba(124, 126, 245, 0.15); +} +.form-field textarea { + min-height: 80px; + resize: vertical; +} +.form-field select { + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23a0a0b8' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + padding-right: 32px; +} +.form-help { + margin: 0; + font-size: 10px; + color: #6c6c84; +} +.form-actions { + display: flex; + gap: 12px; + padding-top: 8px; +} +.required { + color: #e45858; +} +.plugin-link { + color: #9698f7; + text-decoration: none; +} +.plugin-link:hover { + text-decoration: underline; +} +.plugin-link-blocked { + color: #6c6c84; + text-decoration: line-through; + cursor: not-allowed; +} +.plugin-progress { + background: #18181f; + border: 1px solid rgba(255, 255, 255, 0.09); + border-radius: 5px; + height: 8px; + overflow: hidden; + display: flex; + align-items: center; + gap: 8px; +} +.plugin-progress-bar { + height: 100%; + background: #7c7ef5; + border-radius: 4px; + transition: width 0.3s ease; + width: var(--plugin-progress, 0%); +} +.plugin-progress-label { + font-size: 10px; + color: #a0a0b8; + white-space: nowrap; + flex-shrink: 0; +} +.plugin-chart { + overflow: auto; + height: var(--plugin-chart-height, 200px); +} +.plugin-chart .chart-title { + font-size: 13px; + font-weight: 600; + color: #dcdce4; + margin-bottom: 8px; +} +.plugin-chart .chart-x-label, +.plugin-chart .chart-y-label { + font-size: 10px; + color: #6c6c84; + margin-bottom: 4px; +} +.plugin-chart .chart-data-table { + overflow-x: auto; +} +.plugin-chart .chart-no-data { + padding: 24px; + text-align: center; + color: #6c6c84; + font-size: 12px; +} +.plugin-loading { + padding: 16px; + color: #a0a0b8; + font-size: 12px; + font-style: italic; +} +.plugin-error { + padding: 12px 16px; + background: rgba(228, 88, 88, 0.06); + border: 1px solid rgba(228, 88, 88, 0.2); + border-radius: 5px; + color: #d47070; + font-size: 12px; +} +.plugin-feedback { + position: sticky; + bottom: 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 12px 16px; + border-radius: 7px; + font-size: 12px; + z-index: 300; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); +} +.plugin-feedback.success { + background: rgba(62, 201, 122, 0.08); + border: 1px solid rgba(62, 201, 122, 0.2); + color: #3ec97a; +} +.plugin-feedback.error { + background: rgba(228, 88, 88, 0.06); + border: 1px solid rgba(228, 88, 88, 0.2); + color: #d47070; +} +.plugin-feedback-dismiss { + background: rgba(0, 0, 0, 0); + border: none; + color: inherit; + font-size: 14px; + cursor: pointer; + line-height: 1; + padding: 0; + opacity: 0.7; +} +.plugin-feedback-dismiss:hover { + opacity: 1; +} +.plugin-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.65); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} +.plugin-modal { + position: relative; + background: #1f1f28; + border: 1px solid rgba(255, 255, 255, 0.14); + border-radius: 12px; + padding: 32px; + min-width: 380px; + max-width: 640px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45); + z-index: 200; +} +.plugin-modal-close { + position: absolute; + top: 16px; + right: 16px; + background: rgba(0, 0, 0, 0); + border: none; + color: #a0a0b8; + font-size: 14px; + cursor: pointer; + line-height: 1; + padding: 4px; + border-radius: 5px; +} +.plugin-modal-close:hover { + background: rgba(255, 255, 255, 0.04); + color: #dcdce4; +} diff --git a/docs/api/analytics.md b/docs/api/analytics.md index f706d2f..086e07e 100644 --- a/docs/api/analytics.md +++ b/docs/api/analytics.md @@ -16,11 +16,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Event recorded | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Event recorded | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -30,18 +30,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `limit` | query | No | Maximum number of results | -| `offset` | query | No | Pagination offset | +| Name | In | Required | Description | +| -------- | ----- | -------- | ------------------------- | +| `limit` | query | No | Maximum number of results | +| `offset` | query | No | Pagination offset | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Most viewed media | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Most viewed media | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -51,18 +51,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `limit` | query | No | Maximum number of results | -| `offset` | query | No | Pagination offset | +| Name | In | Required | Description | +| -------- | ----- | -------- | ------------------------- | +| `limit` | query | No | Maximum number of results | +| `offset` | query | No | Pagination offset | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Recently viewed media | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Recently viewed media | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -72,18 +72,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Watch progress | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Watch progress | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -93,9 +93,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Request Body @@ -105,13 +105,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Progress updated | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Progress updated | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- - diff --git a/docs/api/audit.md b/docs/api/audit.md index adcf493..5385ca8 100644 --- a/docs/api/audit.md +++ b/docs/api/audit.md @@ -10,18 +10,17 @@ Audit log entries #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Page size | +| Name | In | Required | Description | +| -------- | ----- | -------- | ----------------- | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Page size | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Audit log entries | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Audit log entries | +| 401 | Unauthorized | +| 500 | Internal server error | --- - diff --git a/docs/api/auth.md b/docs/api/auth.md index f62c099..edbd3bd 100644 --- a/docs/api/auth.md +++ b/docs/api/auth.md @@ -16,12 +16,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Login successful | -| 400 | Bad request | -| 401 | Invalid credentials | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Login successful | +| 400 | Bad request | +| 401 | Invalid credentials | +| 500 | Internal server error | --- @@ -31,11 +31,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Logged out | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Logged out | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -45,28 +45,27 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Current user info | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Current user info | +| 401 | Unauthorized | +| 500 | Internal server error | --- ### POST /api/v1/auth/refresh -Refresh the current session, extending its expiry by the configured -duration. +Refresh the current session, extending its expiry by the configured duration. **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -|--------|-------------| -| 200 | Session refreshed | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Session refreshed | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -78,11 +77,11 @@ Revoke all sessions for the current user #### Responses -| Status | Description | -|--------|-------------| -| 200 | All sessions revoked | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | All sessions revoked | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -92,12 +91,11 @@ Revoke all sessions for the current user #### Responses -| Status | Description | -|--------|-------------| -| 200 | Active sessions | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Active sessions | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- - diff --git a/docs/api/backup.md b/docs/api/backup.md index e7c7884..86a184f 100644 --- a/docs/api/backup.md +++ b/docs/api/backup.md @@ -6,22 +6,21 @@ Database backup ### POST /api/v1/admin/backup -Create a database backup and return it as a downloadable file. -POST /api/v1/admin/backup +Create a database backup and return it as a downloadable file. POST +/api/v1/admin/backup -For `SQLite`: creates a backup via VACUUM INTO and returns the file. -For `PostgreSQL`: returns unsupported error (use `pg_dump` instead). +For `SQLite`: creates a backup via VACUUM INTO and returns the file. For +`PostgreSQL`: returns unsupported error (use `pg_dump` instead). **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -|--------|-------------| -| 200 | Backup file download | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Backup file download | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- - diff --git a/docs/api/books.md b/docs/api/books.md index 6af55ad..3ddf863 100644 --- a/docs/api/books.md +++ b/docs/api/books.md @@ -12,23 +12,23 @@ List all books with optional search filters #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `isbn` | query | No | Filter by ISBN | -| `author` | query | No | Filter by author | -| `series` | query | No | Filter by series | -| `publisher` | query | No | Filter by publisher | -| `language` | query | No | Filter by language | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +| ----------- | ----- | -------- | ------------------- | +| `isbn` | query | No | Filter by ISBN | +| `author` | query | No | Filter by author | +| `series` | query | No | Filter by series | +| `publisher` | query | No | Filter by publisher | +| `language` | query | No | Filter by language | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of books | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | List of books | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -40,17 +40,17 @@ List all authors with book counts #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +| -------- | ----- | -------- | ----------------- | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Authors with book counts | -| 401 | Unauthorized | +| Status | Description | +| ------ | ------------------------ | +| 200 | Authors with book counts | +| 401 | Unauthorized | --- @@ -62,18 +62,18 @@ Get books by a specific author #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `name` | path | Yes | Author name | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +| -------- | ----- | -------- | ----------------- | +| `name` | path | Yes | Author name | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Books by author | -| 401 | Unauthorized | +| Status | Description | +| ------ | --------------- | +| 200 | Books by author | +| 401 | Unauthorized | --- @@ -85,16 +85,16 @@ Get user's reading list #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `status` | query | No | Filter by reading status | +| Name | In | Required | Description | +| -------- | ----- | -------- | ------------------------ | +| `status` | query | No | Filter by reading status | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Reading list | -| 401 | Unauthorized | +| Status | Description | +| ------ | ------------ | +| 200 | Reading list | +| 401 | Unauthorized | --- @@ -106,10 +106,10 @@ List all series with book counts #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of series with counts | -| 401 | Unauthorized | +| Status | Description | +| ------ | -------------------------- | +| 200 | List of series with counts | +| 401 | Unauthorized | --- @@ -121,16 +121,16 @@ Get books in a specific series #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `name` | path | Yes | Series name | +| Name | In | Required | Description | +| ------ | ---- | -------- | ----------- | +| `name` | path | Yes | Series name | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Books in series | -| 401 | Unauthorized | +| Status | Description | +| ------ | --------------- | +| 200 | Books in series | +| 401 | Unauthorized | --- @@ -142,17 +142,17 @@ Get book metadata by media ID #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Book metadata | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ------------- | +| 200 | Book metadata | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -164,17 +164,17 @@ Get reading progress for a book #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Reading progress | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ---------------- | +| 200 | Reading progress | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -186,9 +186,9 @@ Update reading progress for a book #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Request Body @@ -198,11 +198,10 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 204 | Progress updated | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +| ------ | ---------------- | +| 204 | Progress updated | +| 400 | Bad request | +| 401 | Unauthorized | --- - diff --git a/docs/api/collections.md b/docs/api/collections.md index f86b5e0..9db76ba 100644 --- a/docs/api/collections.md +++ b/docs/api/collections.md @@ -10,11 +10,11 @@ Media collections #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of collections | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | List of collections | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -30,13 +30,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Collection created | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Collection created | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -46,18 +46,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Collection ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Collection ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Collection | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Collection | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -67,19 +67,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Collection ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Collection ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Collection deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Collection deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -89,18 +89,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Collection ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Collection ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Collection members | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Collection members | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -110,9 +110,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Collection ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Collection ID | #### Request Body @@ -122,13 +122,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Member added | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Member added | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -138,20 +138,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Collection ID | -| `media_id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---------- | ---- | -------- | ------------- | +| `id` | path | Yes | Collection ID | +| `media_id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Member removed | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Member removed | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- - diff --git a/docs/api/config.md b/docs/api/config.md index f88299f..a05fcd0 100644 --- a/docs/api/config.md +++ b/docs/api/config.md @@ -10,12 +10,12 @@ Server configuration #### Responses -| Status | Description | -|--------|-------------| -| 200 | Current server configuration | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | ---------------------------- | +| 200 | Current server configuration | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -31,13 +31,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Updated configuration | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Updated configuration | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -53,12 +53,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Updated configuration | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Updated configuration | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -74,12 +74,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Updated configuration | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Updated configuration | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -89,11 +89,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | UI configuration | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | UI configuration | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -109,12 +109,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Updated UI configuration | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | ------------------------ | +| 200 | Updated UI configuration | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- - diff --git a/docs/api/database.md b/docs/api/database.md index 373df31..bed7ff6 100644 --- a/docs/api/database.md +++ b/docs/api/database.md @@ -10,12 +10,12 @@ Database administration #### Responses -| Status | Description | -|--------|-------------| -| 200 | Database cleared | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Database cleared | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -25,12 +25,12 @@ Database administration #### Responses -| Status | Description | -|--------|-------------| -| 200 | Database statistics | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Database statistics | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -40,12 +40,11 @@ Database administration #### Responses -| Status | Description | -|--------|-------------| -| 200 | Database vacuumed | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Database vacuumed | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- - diff --git a/docs/api/duplicates.md b/docs/api/duplicates.md index b1005b7..63a200d 100644 --- a/docs/api/duplicates.md +++ b/docs/api/duplicates.md @@ -10,11 +10,10 @@ Duplicate media detection #### Responses -| Status | Description | -|--------|-------------| -| 200 | Duplicate groups | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Duplicate groups | +| 401 | Unauthorized | +| 500 | Internal server error | --- - diff --git a/docs/api/enrichment.md b/docs/api/enrichment.md index 5012641..f4ec48e 100644 --- a/docs/api/enrichment.md +++ b/docs/api/enrichment.md @@ -16,13 +16,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Enrichment job submitted | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | ------------------------ | +| 200 | Enrichment job submitted | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -32,19 +32,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Enrichment job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | ------------------------ | +| 200 | Enrichment job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -54,18 +54,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | External metadata | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | External metadata | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- - diff --git a/docs/api/export.md b/docs/api/export.md index 3410a9f..593c1aa 100644 --- a/docs/api/export.md +++ b/docs/api/export.md @@ -10,12 +10,12 @@ Media library export #### Responses -| Status | Description | -|--------|-------------| -| 200 | Export job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Export job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -31,12 +31,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Export job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Export job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- - diff --git a/docs/api/health.md b/docs/api/health.md index 44f6d4a..3ed9cf6 100644 --- a/docs/api/health.md +++ b/docs/api/health.md @@ -12,9 +12,9 @@ Comprehensive health check - includes database, filesystem, and cache status #### Responses -| Status | Description | -|--------|-------------| -| 200 | Health status | +| Status | Description | +| ------ | ------------- | +| 200 | Health status | --- @@ -24,40 +24,39 @@ Comprehensive health check - includes database, filesystem, and cache status #### Responses -| Status | Description | -|--------|-------------| -| 200 | Detailed health status | +| Status | Description | +| ------ | ---------------------- | +| 200 | Detailed health status | --- ### GET /api/v1/health/live -Liveness probe - just checks if the server is running -Returns 200 OK if the server process is alive +Liveness probe - just checks if the server is running Returns 200 OK if the +server process is alive **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -|--------|-------------| -| 200 | Server is alive | +| Status | Description | +| ------ | --------------- | +| 200 | Server is alive | --- ### GET /api/v1/health/ready -Readiness probe - checks if the server can serve requests -Returns 200 OK if database is accessible +Readiness probe - checks if the server can serve requests Returns 200 OK if +database is accessible **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -|--------|-------------| -| 200 | Server is ready | -| 503 | Server not ready | +| Status | Description | +| ------ | ---------------- | +| 200 | Server is ready | +| 503 | Server not ready | --- - diff --git a/docs/api/integrity.md b/docs/api/integrity.md index fd22c23..5c2b4c8 100644 --- a/docs/api/integrity.md +++ b/docs/api/integrity.md @@ -10,12 +10,12 @@ Library integrity checks and repairs #### Responses -| Status | Description | -|--------|-------------| -| 200 | Orphan detection job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | ------------------------------ | +| 200 | Orphan detection job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -31,12 +31,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Orphans resolved | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Orphans resolved | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -46,12 +46,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Thumbnail cleanup job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | ------------------------------- | +| 200 | Thumbnail cleanup job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -67,12 +67,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Thumbnail generation job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | ---------------------------------- | +| 200 | Thumbnail generation job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -88,12 +88,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Integrity verification job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | ------------------------------------ | +| 200 | Integrity verification job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- - diff --git a/docs/api/jobs.md b/docs/api/jobs.md index a46b7fd..4cf6a2d 100644 --- a/docs/api/jobs.md +++ b/docs/api/jobs.md @@ -10,11 +10,11 @@ Background job management #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of jobs | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +| ------ | ------------ | +| 200 | List of jobs | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -24,18 +24,18 @@ Background job management #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Job ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Job ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Job details | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------ | +| 200 | Job details | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -45,18 +45,17 @@ Background job management #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Job ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Job ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Job cancelled | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------- | +| 200 | Job cancelled | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- - diff --git a/docs/api/media.md b/docs/api/media.md index b612729..bdd1dd7 100644 --- a/docs/api/media.md +++ b/docs/api/media.md @@ -10,19 +10,19 @@ Media item management #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Page size | -| `sort` | query | No | Sort field | +| Name | In | Required | Description | +| -------- | ----- | -------- | ----------------- | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Page size | +| `sort` | query | No | Sort field | #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of media items | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | List of media items | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -32,12 +32,12 @@ Media item management #### Responses -| Status | Description | -|--------|-------------| -| 200 | All media deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | All media deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -53,13 +53,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Batch collection result | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | ----------------------- | +| 200 | Batch collection result | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -75,13 +75,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Batch delete result | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Batch delete result | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -97,13 +97,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Batch move result | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Batch move result | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -119,13 +119,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Batch tag result | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Batch tag result | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -141,13 +141,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Batch update result | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Batch update result | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -157,11 +157,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media count | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media count | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -177,13 +177,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media imported | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media imported | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -199,13 +199,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Batch import results | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Batch import results | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -221,13 +221,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Directory import results | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | ------------------------ | +| 200 | Directory import results | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -243,13 +243,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media imported | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media imported | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -265,13 +265,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Directory preview | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Directory preview | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -281,18 +281,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Page size | +| Name | In | Required | Description | +| -------- | ----- | -------- | ----------------- | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Page size | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Trashed media items | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Trashed media items | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -302,12 +302,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Trash emptied | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Trash emptied | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -317,11 +317,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Trash info | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Trash info | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -331,18 +331,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media item | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media item | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -352,9 +352,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Request Body @@ -364,14 +364,14 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Updated media item | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Updated media item | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -381,19 +381,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -403,9 +403,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Request Body @@ -415,14 +415,14 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Custom field set | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Custom field set | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -432,20 +432,20 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | -| `name` | path | Yes | Custom field name | +| Name | In | Required | Description | +| ------ | ---- | -------- | ----------------- | +| `id` | path | Yes | Media item ID | +| `name` | path | Yes | Custom field name | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Custom field deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Custom field deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -455,9 +455,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Request Body @@ -467,14 +467,14 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Moved media item | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Moved media item | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -484,18 +484,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media opened | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media opened | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -505,20 +505,20 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | -| `permanent` | query | No | Set to 'true' for permanent deletion | +| Name | In | Required | Description | +| ----------- | ----- | -------- | ------------------------------------ | +| `id` | path | Yes | Media item ID | +| `permanent` | query | No | Set to 'true' for permanent deletion | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -528,9 +528,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Request Body @@ -540,14 +540,14 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Renamed media item | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Renamed media item | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -557,19 +557,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media restored | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media restored | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -579,19 +579,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media stream | -| 206 | Partial content | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media stream | +| 206 | Partial content | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -601,18 +601,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Thumbnail image | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Thumbnail image | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -622,19 +622,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media moved to trash | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media moved to trash | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- - diff --git a/docs/api/notes.md b/docs/api/notes.md index 330c8f3..0ffc1fe 100644 --- a/docs/api/notes.md +++ b/docs/api/notes.md @@ -14,18 +14,18 @@ GET /api/v1/media/{id}/backlinks #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Backlinks | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Backlinks | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -39,18 +39,18 @@ GET /api/v1/media/{id}/outgoing-links #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Outgoing links | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Outgoing links | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -64,18 +64,18 @@ POST /api/v1/media/{id}/reindex-links #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Links reindexed | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Links reindexed | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -89,18 +89,18 @@ GET /api/v1/notes/graph?center={uuid}&depth={n} #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `center` | query | No | Center node ID | -| `depth` | query | No | Traversal depth (max 5, default 2) | +| Name | In | Required | Description | +| -------- | ----- | -------- | ---------------------------------- | +| `center` | query | No | Center node ID | +| `depth` | query | No | Traversal depth (max 5, default 2) | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Graph data | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Graph data | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -114,11 +114,11 @@ POST /api/v1/notes/resolve-links #### Responses -| Status | Description | -|--------|-------------| -| 200 | Links resolved | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Links resolved | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -132,11 +132,10 @@ GET /api/v1/notes/unresolved-count #### Responses -| Status | Description | -|--------|-------------| -| 200 | Unresolved link count | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Unresolved link count | +| 401 | Unauthorized | +| 500 | Internal server error | --- - diff --git a/docs/api/photos.md b/docs/api/photos.md index 5afdba3..3613838 100644 --- a/docs/api/photos.md +++ b/docs/api/photos.md @@ -12,21 +12,21 @@ Get photos in a bounding box for map view #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `lat1` | query | Yes | Bounding box latitude 1 | -| `lon1` | query | Yes | Bounding box longitude 1 | -| `lat2` | query | Yes | Bounding box latitude 2 | -| `lon2` | query | Yes | Bounding box longitude 2 | +| Name | In | Required | Description | +| ------ | ----- | -------- | ------------------------ | +| `lat1` | query | Yes | Bounding box latitude 1 | +| `lon1` | query | Yes | Bounding box longitude 1 | +| `lat2` | query | Yes | Bounding box latitude 2 | +| `lon2` | query | Yes | Bounding box longitude 2 | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Map markers | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Map markers | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -38,20 +38,19 @@ Get timeline of photos grouped by date #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `group_by` | query | No | Grouping: day, month, year | -| `year` | query | No | Filter by year | -| `month` | query | No | Filter by month | -| `limit` | query | No | Max items (default 10000) | +| Name | In | Required | Description | +| ---------- | ----- | -------- | -------------------------- | +| `group_by` | query | No | Grouping: day, month, year | +| `year` | query | No | Filter by year | +| `month` | query | No | Filter by month | +| `limit` | query | No | Max items (default 10000) | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Photo timeline groups | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Photo timeline groups | +| 401 | Unauthorized | +| 500 | Internal server error | --- - diff --git a/docs/api/playlists.md b/docs/api/playlists.md index 2f97cde..fdcbf35 100644 --- a/docs/api/playlists.md +++ b/docs/api/playlists.md @@ -10,11 +10,11 @@ Media playlists #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of playlists | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | List of playlists | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -30,12 +30,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Playlist created | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Playlist created | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -45,18 +45,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Playlist ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Playlist details | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ---------------- | +| 200 | Playlist details | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -66,9 +66,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Playlist ID | #### Request Body @@ -78,13 +78,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Playlist updated | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ---------------- | +| 200 | Playlist updated | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -94,18 +94,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Playlist ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Playlist deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ---------------- | +| 200 | Playlist deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -115,18 +115,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Playlist ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Playlist items | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 200 | Playlist items | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -136,9 +136,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Playlist ID | #### Request Body @@ -148,12 +148,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Item added | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------ | +| 200 | Item added | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -163,9 +163,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Playlist ID | #### Request Body @@ -175,12 +175,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Item reordered | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 200 | Item reordered | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -190,19 +190,19 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Playlist ID | -| `media_id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---------- | ---- | -------- | ------------- | +| `id` | path | Yes | Playlist ID | +| `media_id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Item removed | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------ | +| 200 | Item removed | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -212,18 +212,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Playlist ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Playlist ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Shuffled playlist items | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ----------------------- | +| 200 | Shuffled playlist items | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- - diff --git a/docs/api/plugins.md b/docs/api/plugins.md index eaab41e..683de6c 100644 --- a/docs/api/plugins.md +++ b/docs/api/plugins.md @@ -12,11 +12,11 @@ List all installed plugins #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of plugins | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | List of plugins | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -34,12 +34,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Plugin installed | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +| ------ | ---------------- | +| 200 | Plugin installed | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -58,10 +58,10 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Event received | -| 401 | Unauthorized | +| Status | Description | +| ------ | -------------- | +| 200 | Event received | +| 401 | Unauthorized | --- @@ -73,10 +73,10 @@ List all UI pages provided by loaded plugins #### Responses -| Status | Description | -|--------|-------------| -| 200 | Plugin UI pages | -| 401 | Unauthorized | +| Status | Description | +| ------ | --------------- | +| 200 | Plugin UI pages | +| 401 | Unauthorized | --- @@ -88,10 +88,10 @@ List merged CSS custom property overrides from all enabled plugins #### Responses -| Status | Description | -|--------|-------------| -| 200 | Plugin UI theme extensions | -| 401 | Unauthorized | +| Status | Description | +| ------ | -------------------------- | +| 200 | Plugin UI theme extensions | +| 401 | Unauthorized | --- @@ -103,10 +103,10 @@ List all UI widgets provided by loaded plugins #### Responses -| Status | Description | -|--------|-------------| -| 200 | Plugin UI widgets | -| 401 | Unauthorized | +| Status | Description | +| ------ | ----------------- | +| 200 | Plugin UI widgets | +| 401 | Unauthorized | --- @@ -118,17 +118,17 @@ Get a specific plugin by ID #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Plugin ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Plugin ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Plugin details | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 200 | Plugin details | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -140,18 +140,18 @@ Uninstall a plugin #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Plugin ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Plugin ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Plugin uninstalled | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------------ | +| 200 | Plugin uninstalled | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -163,18 +163,18 @@ Reload a plugin (for development) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Plugin ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Plugin ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Plugin reloaded | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | --------------- | +| 200 | Plugin reloaded | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -186,9 +186,9 @@ Enable or disable a plugin #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Plugin ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Plugin ID | #### Request Body @@ -198,12 +198,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Plugin toggled | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 200 | Plugin toggled | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- - diff --git a/docs/api/saved_searches.md b/docs/api/saved_searches.md index 12e374d..3889deb 100644 --- a/docs/api/saved_searches.md +++ b/docs/api/saved_searches.md @@ -10,11 +10,11 @@ Saved search queries #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of saved searches | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | ---------------------- | +| 200 | List of saved searches | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -30,12 +30,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Search saved | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Search saved | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -45,18 +45,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Saved search ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | --------------- | +| `id` | path | Yes | Saved search ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Saved search deleted | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Saved search deleted | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- - diff --git a/docs/api/scan.md b/docs/api/scan.md index 9c2af4b..3101bca 100644 --- a/docs/api/scan.md +++ b/docs/api/scan.md @@ -18,12 +18,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Scan job submitted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Scan job submitted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -33,10 +33,9 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Scan status | -| 401 | Unauthorized | +| Status | Description | +| ------ | ------------ | +| 200 | Scan status | +| 401 | Unauthorized | --- - diff --git a/docs/api/scheduled_tasks.md b/docs/api/scheduled_tasks.md index 2367493..d357c66 100644 --- a/docs/api/scheduled_tasks.md +++ b/docs/api/scheduled_tasks.md @@ -10,11 +10,11 @@ Scheduled background tasks #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of scheduled tasks | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +| ------ | ----------------------- | +| 200 | List of scheduled tasks | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -24,18 +24,18 @@ Scheduled background tasks #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Task ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Task ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Task triggered | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 200 | Task triggered | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -45,18 +45,17 @@ Scheduled background tasks #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Task ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Task ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Task toggled | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------ | +| 200 | Task toggled | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- - diff --git a/docs/api/search.md b/docs/api/search.md index 102d2fb..2ca9429 100644 --- a/docs/api/search.md +++ b/docs/api/search.md @@ -10,21 +10,21 @@ Full-text media search #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `q` | query | Yes | Search query | -| `sort` | query | No | Sort order | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +| -------- | ----- | -------- | ----------------- | +| `q` | query | Yes | Search query | +| `sort` | query | No | Sort order | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Search results | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Search results | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -40,12 +40,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Search results | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Search results | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- - diff --git a/docs/api/shares.md b/docs/api/shares.md index 9702f41..cd62b74 100644 --- a/docs/api/shares.md +++ b/docs/api/shares.md @@ -6,86 +6,81 @@ Media sharing and notifications ### GET /api/v1/notifications/shares -Get unread share notifications -GET /api/notifications/shares +Get unread share notifications GET /api/notifications/shares **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -|--------|-------------| -| 200 | Unread notifications | -| 401 | Unauthorized | +| Status | Description | +| ------ | -------------------- | +| 200 | Unread notifications | +| 401 | Unauthorized | --- ### POST /api/v1/notifications/shares/read-all -Mark all notifications as read -POST /api/notifications/shares/read-all +Mark all notifications as read POST /api/notifications/shares/read-all **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -|--------|-------------| -| 200 | All notifications marked as read | -| 401 | Unauthorized | +| Status | Description | +| ------ | -------------------------------- | +| 200 | All notifications marked as read | +| 401 | Unauthorized | --- ### POST /api/v1/notifications/shares/{id}/read -Mark a notification as read -POST /api/notifications/shares/{id}/read +Mark a notification as read POST /api/notifications/shares/{id}/read **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Notification ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | --------------- | +| `id` | path | Yes | Notification ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Notification marked as read | -| 401 | Unauthorized | +| Status | Description | +| ------ | --------------------------- | +| 200 | Notification marked as read | +| 401 | Unauthorized | --- ### GET /api/v1/shared/{token} -Access a public shared resource -GET /api/shared/{token} +Access a public shared resource GET /api/shared/{token} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `token` | path | Yes | Share token | -| `password` | query | No | Share password if required | +| Name | In | Required | Description | +| ---------- | ----- | -------- | -------------------------- | +| `token` | path | Yes | Share token | +| `password` | query | No | Share password if required | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Shared content | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 200 | Shared content | +| 401 | Unauthorized | +| 404 | Not found | --- ### POST /api/v1/shares -Create a new share -POST /api/shares +Create a new share POST /api/shares **Authentication:** Required (Bearer JWT) @@ -97,19 +92,18 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Share created | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Share created | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- ### POST /api/v1/shares/batch/delete -Batch delete shares -POST /api/shares/batch/delete +Batch delete shares POST /api/shares/batch/delete **Authentication:** Required (Bearer JWT) @@ -121,97 +115,93 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Shares deleted | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +| ------ | -------------- | +| 200 | Shares deleted | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | --- ### GET /api/v1/shares/incoming -List incoming shares (shares shared with me) -GET /api/shares/incoming +List incoming shares (shares shared with me) GET /api/shares/incoming **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +| -------- | ----- | -------- | ----------------- | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Incoming shares | -| 401 | Unauthorized | +| Status | Description | +| ------ | --------------- | +| 200 | Incoming shares | +| 401 | Unauthorized | --- ### GET /api/v1/shares/outgoing -List outgoing shares (shares I created) -GET /api/shares/outgoing +List outgoing shares (shares I created) GET /api/shares/outgoing **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +| -------- | ----- | -------- | ----------------- | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Outgoing shares | -| 401 | Unauthorized | +| Status | Description | +| ------ | --------------- | +| 200 | Outgoing shares | +| 401 | Unauthorized | --- ### GET /api/v1/shares/{id} -Get share details -GET /api/shares/{id} +Get share details GET /api/shares/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Share ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Share ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Share details | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------- | +| 200 | Share details | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### PATCH /api/v1/shares/{id} -Update a share -PATCH /api/shares/{id} +Update a share PATCH /api/shares/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Share ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Share ID | #### Request Body @@ -221,62 +211,59 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Share updated | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------- | +| 200 | Share updated | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### DELETE /api/v1/shares/{id} -Delete (revoke) a share -DELETE /api/shares/{id} +Delete (revoke) a share DELETE /api/shares/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Share ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Share ID | #### Responses -| Status | Description | -|--------|-------------| -| 204 | Share deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------- | +| 204 | Share deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### GET /api/v1/shares/{id}/activity -Get share activity log -GET /api/shares/{id}/activity +Get share activity log GET /api/shares/{id}/activity **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Share ID | -| `offset` | query | No | Pagination offset | -| `limit` | query | No | Pagination limit | +| Name | In | Required | Description | +| -------- | ----- | -------- | ----------------- | +| `id` | path | Yes | Share ID | +| `offset` | query | No | Pagination offset | +| `limit` | query | No | Pagination limit | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Share activity | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 200 | Share activity | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- - diff --git a/docs/api/social.md b/docs/api/social.md index e706183..097a627 100644 --- a/docs/api/social.md +++ b/docs/api/social.md @@ -10,11 +10,11 @@ Ratings, comments, favorites, and share links #### Responses -| Status | Description | -|--------|-------------| -| 200 | User favorites | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | User favorites | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -30,11 +30,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Added to favorites | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Added to favorites | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -44,17 +44,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `media_id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---------- | ---- | -------- | ------------- | +| `media_id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Removed from favorites | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | ---------------------- | +| 200 | Removed from favorites | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -70,12 +70,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Share link created | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Share link created | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -85,17 +85,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media comments | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media comments | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -105,9 +105,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Request Body @@ -117,12 +117,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Comment added | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Comment added | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -132,9 +132,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Request Body @@ -144,12 +144,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Rating saved | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Rating saved | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -159,17 +159,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media ratings | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media ratings | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -179,18 +179,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `token` | path | Yes | Share token | -| `password` | query | No | Share password | +| Name | In | Required | Description | +| ---------- | ----- | -------- | -------------- | +| `token` | path | Yes | Share token | +| `password` | query | No | Share password | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Shared media | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ------------ | +| 200 | Shared media | +| 401 | Unauthorized | +| 404 | Not found | --- - diff --git a/docs/api/statistics.md b/docs/api/statistics.md index 270ad62..b74f02f 100644 --- a/docs/api/statistics.md +++ b/docs/api/statistics.md @@ -10,11 +10,10 @@ Library statistics #### Responses -| Status | Description | -|--------|-------------| -| 200 | Library statistics | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Library statistics | +| 401 | Unauthorized | +| 500 | Internal server error | --- - diff --git a/docs/api/streaming.md b/docs/api/streaming.md index 11a3352..0bd0a35 100644 --- a/docs/api/streaming.md +++ b/docs/api/streaming.md @@ -10,18 +10,18 @@ HLS and DASH adaptive streaming #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | DASH manifest | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ------------- | +| 200 | DASH manifest | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -31,20 +31,20 @@ HLS and DASH adaptive streaming #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | -| `profile` | path | Yes | Transcode profile name | -| `segment` | path | Yes | Segment filename | +| Name | In | Required | Description | +| --------- | ---- | -------- | ---------------------- | +| `id` | path | Yes | Media item ID | +| `profile` | path | Yes | Transcode profile name | +| `segment` | path | Yes | Segment filename | #### Responses -| Status | Description | -|--------|-------------| -| 200 | DASH segment data | -| 202 | Segment not yet available | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +| ------ | ------------------------- | +| 200 | DASH segment data | +| 202 | Segment not yet available | +| 400 | Bad request | +| 401 | Unauthorized | --- @@ -54,17 +54,17 @@ HLS and DASH adaptive streaming #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | HLS master playlist | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ------------------- | +| 200 | HLS master playlist | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -74,19 +74,19 @@ HLS and DASH adaptive streaming #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | -| `profile` | path | Yes | Transcode profile name | +| Name | In | Required | Description | +| --------- | ---- | -------- | ---------------------- | +| `id` | path | Yes | Media item ID | +| `profile` | path | Yes | Transcode profile name | #### Responses -| Status | Description | -|--------|-------------| -| 200 | HLS variant playlist | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | -------------------- | +| 200 | HLS variant playlist | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -96,20 +96,19 @@ HLS and DASH adaptive streaming #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | -| `profile` | path | Yes | Transcode profile name | -| `segment` | path | Yes | Segment filename | +| Name | In | Required | Description | +| --------- | ---- | -------- | ---------------------- | +| `id` | path | Yes | Media item ID | +| `profile` | path | Yes | Transcode profile name | +| `segment` | path | Yes | Segment filename | #### Responses -| Status | Description | -|--------|-------------| -| 200 | HLS segment data | -| 202 | Segment not yet available | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +| ------ | ------------------------- | +| 200 | HLS segment data | +| 202 | Segment not yet available | +| 400 | Bad request | +| 401 | Unauthorized | --- - diff --git a/docs/api/subtitles.md b/docs/api/subtitles.md index ce36e05..d170681 100644 --- a/docs/api/subtitles.md +++ b/docs/api/subtitles.md @@ -10,17 +10,17 @@ Media subtitle management #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Subtitles | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ------------ | +| 200 | Subtitles | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -30,9 +30,9 @@ Media subtitle management #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Request Body @@ -42,12 +42,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Subtitle added | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Subtitle added | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -57,18 +57,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `media_id` | path | Yes | Media item ID | -| `subtitle_id` | path | Yes | Subtitle ID | +| Name | In | Required | Description | +| ------------- | ---- | -------- | ------------- | +| `media_id` | path | Yes | Media item ID | +| `subtitle_id` | path | Yes | Subtitle ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Subtitle content | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ---------------- | +| 200 | Subtitle content | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -78,17 +78,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Subtitle ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Subtitle ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Subtitle deleted | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ---------------- | +| 200 | Subtitle deleted | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -98,9 +98,9 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Subtitle ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Subtitle ID | #### Request Body @@ -110,11 +110,10 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Offset updated | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 200 | Offset updated | +| 401 | Unauthorized | +| 404 | Not found | --- - diff --git a/docs/api/sync.md b/docs/api/sync.md index 165d4c0..fd1aa5b 100644 --- a/docs/api/sync.md +++ b/docs/api/sync.md @@ -6,8 +6,7 @@ Multi-device library synchronization ### POST /api/v1/sync/ack -Acknowledge processed changes -POST /api/sync/ack +Acknowledge processed changes POST /api/sync/ack **Authentication:** Required (Bearer JWT) @@ -19,66 +18,63 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Changes acknowledged | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +| ------ | -------------------- | +| 200 | Changes acknowledged | +| 400 | Bad request | +| 401 | Unauthorized | --- ### GET /api/v1/sync/changes -Get changes since cursor -GET /api/sync/changes +Get changes since cursor GET /api/sync/changes **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `cursor` | query | No | Sync cursor | -| `limit` | query | No | Max changes (max 1000) | +| Name | In | Required | Description | +| -------- | ----- | -------- | ---------------------- | +| `cursor` | query | No | Sync cursor | +| `limit` | query | No | Max changes (max 1000) | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Changes since cursor | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +| ------ | -------------------- | +| 200 | Changes since cursor | +| 400 | Bad request | +| 401 | Unauthorized | --- ### GET /api/v1/sync/conflicts -List unresolved conflicts -GET /api/sync/conflicts +List unresolved conflicts GET /api/sync/conflicts **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -|--------|-------------| -| 200 | Unresolved conflicts | -| 401 | Unauthorized | +| Status | Description | +| ------ | -------------------- | +| 200 | Unresolved conflicts | +| 401 | Unauthorized | --- ### POST /api/v1/sync/conflicts/{id}/resolve -Resolve a sync conflict -POST /api/sync/conflicts/{id}/resolve +Resolve a sync conflict POST /api/sync/conflicts/{id}/resolve **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Conflict ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Conflict ID | #### Request Body @@ -88,34 +84,32 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Conflict resolved | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +| ------ | ----------------- | +| 200 | Conflict resolved | +| 400 | Bad request | +| 401 | Unauthorized | --- ### GET /api/v1/sync/devices -List user's sync devices -GET /api/sync/devices +List user's sync devices GET /api/sync/devices **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of devices | -| 401 | Unauthorized | +| Status | Description | +| ------ | --------------- | +| 200 | List of devices | +| 401 | Unauthorized | --- ### POST /api/v1/sync/devices -Register a new sync device -POST /api/sync/devices +Register a new sync device POST /api/sync/devices **Authentication:** Required (Bearer JWT) @@ -127,51 +121,49 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Device registered | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Device registered | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- ### GET /api/v1/sync/devices/{id} -Get device details -GET /api/sync/devices/{id} +Get device details GET /api/sync/devices/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Device ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Device ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Device details | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 200 | Device details | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### PUT /api/v1/sync/devices/{id} -Update a device -PUT /api/sync/devices/{id} +Update a device PUT /api/sync/devices/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Device ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Device ID | #### Request Body @@ -181,91 +173,87 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Device updated | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 200 | Device updated | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### DELETE /api/v1/sync/devices/{id} -Delete a device -DELETE /api/sync/devices/{id} +Delete a device DELETE /api/sync/devices/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Device ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Device ID | #### Responses -| Status | Description | -|--------|-------------| -| 204 | Device deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 204 | Device deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### POST /api/v1/sync/devices/{id}/token -Regenerate device token -POST /api/sync/devices/{id}/token +Regenerate device token POST /api/sync/devices/{id}/token **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Device ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Device ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Token regenerated | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ----------------- | +| 200 | Token regenerated | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- ### GET /api/v1/sync/download/{path} -Download a file for sync (supports Range header) -GET /api/sync/download/{*path} +Download a file for sync (supports Range header) GET /api/sync/download/{*path} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `path` | path | Yes | File path | +| Name | In | Required | Description | +| ------ | ---- | -------- | ----------- | +| `path` | path | Yes | File path | #### Responses -| Status | Description | -|--------|-------------| -| 200 | File content | -| 206 | Partial content | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | --------------- | +| 200 | File content | +| 206 | Partial content | +| 401 | Unauthorized | +| 404 | Not found | --- ### POST /api/v1/sync/report -Report local changes from client -POST /api/sync/report +Report local changes from client POST /api/sync/report **Authentication:** Required (Bearer JWT) @@ -277,18 +265,17 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Changes processed | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +| ------ | ----------------- | +| 200 | Changes processed | +| 400 | Bad request | +| 401 | Unauthorized | --- ### POST /api/v1/sync/upload -Create an upload session for chunked upload -POST /api/sync/upload +Create an upload session for chunked upload POST /api/sync/upload **Authentication:** Required (Bearer JWT) @@ -300,113 +287,107 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Upload session created | -| 400 | Bad request | -| 401 | Unauthorized | +| Status | Description | +| ------ | ---------------------- | +| 200 | Upload session created | +| 400 | Bad request | +| 401 | Unauthorized | --- ### GET /api/v1/sync/upload/{id} -Get upload session status -GET /api/sync/upload/{id} +Get upload session status GET /api/sync/upload/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Upload session ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------------- | +| `id` | path | Yes | Upload session ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Upload session status | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | --------------------- | +| 200 | Upload session status | +| 401 | Unauthorized | +| 404 | Not found | --- ### DELETE /api/v1/sync/upload/{id} -Cancel an upload session -DELETE /api/sync/upload/{id} +Cancel an upload session DELETE /api/sync/upload/{id} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Upload session ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------------- | +| `id` | path | Yes | Upload session ID | #### Responses -| Status | Description | -|--------|-------------| -| 204 | Upload cancelled | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ---------------- | +| 204 | Upload cancelled | +| 401 | Unauthorized | +| 404 | Not found | --- ### PUT /api/v1/sync/upload/{id}/chunks/{index} -Upload a chunk -PUT /api/sync/upload/{id}/chunks/{index} +Upload a chunk PUT /api/sync/upload/{id}/chunks/{index} **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Upload session ID | -| `index` | path | Yes | Chunk index | +| Name | In | Required | Description | +| ------- | ---- | -------- | ----------------- | +| `id` | path | Yes | Upload session ID | +| `index` | path | Yes | Chunk index | #### Request Body -Chunk binary data -`Content-Type: application/octet-stream` +Chunk binary data `Content-Type: application/octet-stream` See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Chunk received | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | -------------- | +| 200 | Chunk received | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | --- ### POST /api/v1/sync/upload/{id}/complete -Complete an upload session -POST /api/sync/upload/{id}/complete +Complete an upload session POST /api/sync/upload/{id}/complete **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Upload session ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------------- | +| `id` | path | Yes | Upload session ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Upload completed | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ---------------- | +| 200 | Upload completed | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | --- - diff --git a/docs/api/tags.md b/docs/api/tags.md index a9a71c0..6bdefc4 100644 --- a/docs/api/tags.md +++ b/docs/api/tags.md @@ -10,18 +10,18 @@ Media tag management #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `media_id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---------- | ---- | -------- | ------------- | +| `media_id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Media tags | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Media tags | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -31,9 +31,9 @@ Media tag management #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `media_id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---------- | ---- | -------- | ------------- | +| `media_id` | path | Yes | Media item ID | #### Request Body @@ -43,13 +43,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Tag applied | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Tag applied | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -59,20 +59,20 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `media_id` | path | Yes | Media item ID | -| `tag_id` | path | Yes | Tag ID | +| Name | In | Required | Description | +| ---------- | ---- | -------- | ------------- | +| `media_id` | path | Yes | Media item ID | +| `tag_id` | path | Yes | Tag ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Tag removed | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Tag removed | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- @@ -82,11 +82,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of tags | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | List of tags | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -102,13 +102,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Tag created | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Tag created | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -118,18 +118,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Tag ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Tag ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Tag | -| 401 | Unauthorized | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Tag | +| 401 | Unauthorized | +| 404 | Not found | +| 500 | Internal server error | --- @@ -139,19 +139,18 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Tag ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | Tag ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Tag deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | Tag deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | +| 500 | Internal server error | --- - diff --git a/docs/api/transcode.md b/docs/api/transcode.md index 126135e..3cbb7f9 100644 --- a/docs/api/transcode.md +++ b/docs/api/transcode.md @@ -10,9 +10,9 @@ Video transcoding sessions #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Request Body @@ -22,12 +22,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Transcode job submitted | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | ----------------------- | +| 200 | Transcode job submitted | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- @@ -37,10 +37,10 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of transcode sessions | -| 401 | Unauthorized | +| Status | Description | +| ------ | -------------------------- | +| 200 | List of transcode sessions | +| 401 | Unauthorized | --- @@ -50,17 +50,17 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Transcode session ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | -------------------- | +| `id` | path | Yes | Transcode session ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Transcode session details | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ------------------------- | +| 200 | Transcode session details | +| 401 | Unauthorized | +| 404 | Not found | --- @@ -70,17 +70,16 @@ See `docs/api/openapi.json` for the full schema. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Transcode session ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | -------------------- | +| `id` | path | Yes | Transcode session ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | Transcode session cancelled | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | --------------------------- | +| 200 | Transcode session cancelled | +| 401 | Unauthorized | +| 404 | Not found | --- - diff --git a/docs/api/upload.md b/docs/api/upload.md index da8a61b..698b963 100644 --- a/docs/api/upload.md +++ b/docs/api/upload.md @@ -6,84 +6,79 @@ File upload and managed storage ### GET /api/v1/managed/stats -Get managed storage statistics -GET /api/managed/stats +Get managed storage statistics GET /api/managed/stats **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -|--------|-------------| -| 200 | Managed storage statistics | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | -------------------------- | +| 200 | Managed storage statistics | +| 401 | Unauthorized | +| 500 | Internal server error | --- ### GET /api/v1/media/{id}/download -Download a managed file -GET /api/media/{id}/download +Download a managed file GET /api/media/{id}/download **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | File content | -| 400 | Bad request | -| 401 | Unauthorized | -| 404 | Not found | +| Status | Description | +| ------ | ------------ | +| 200 | File content | +| 400 | Bad request | +| 401 | Unauthorized | +| 404 | Not found | --- ### POST /api/v1/media/{id}/move-to-managed -Migrate an external file to managed storage -POST /api/media/{id}/move-to-managed +Migrate an external file to managed storage POST /api/media/{id}/move-to-managed **Authentication:** Required (Bearer JWT) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | Media item ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ------------- | +| `id` | path | Yes | Media item ID | #### Responses -| Status | Description | -|--------|-------------| -| 204 | File migrated | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 204 | File migrated | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- ### POST /api/v1/upload -Upload a file to managed storage -POST /api/upload +Upload a file to managed storage POST /api/upload **Authentication:** Required (Bearer JWT) #### Responses -| Status | Description | -|--------|-------------| -| 200 | File uploaded | -| 400 | Bad request | -| 401 | Unauthorized | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | File uploaded | +| 400 | Bad request | +| 401 | Unauthorized | +| 500 | Internal server error | --- - diff --git a/docs/api/users.md b/docs/api/users.md index 0cd7087..c76a411 100644 --- a/docs/api/users.md +++ b/docs/api/users.md @@ -12,11 +12,11 @@ List all users (admin only) #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of users | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +| ------ | ------------- | +| 200 | List of users | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -35,13 +35,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | User created | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 500 | Internal server error | +| Status | Description | +| ------ | --------------------- | +| 200 | User created | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 500 | Internal server error | --- @@ -53,18 +53,18 @@ Get a specific user by ID #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | User ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | User details | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------ | +| 200 | User details | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -76,9 +76,9 @@ Update a user #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | User ID | #### Request Body @@ -89,13 +89,13 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | User updated | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------ | +| 200 | User updated | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -107,18 +107,18 @@ Delete a user (admin only) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | User ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | User deleted | -| 401 | Unauthorized | -| 403 | Forbidden | -| 404 | Not found | +| Status | Description | +| ------ | ------------ | +| 200 | User deleted | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not found | --- @@ -130,17 +130,17 @@ Get user's accessible libraries #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | User ID | #### Responses -| Status | Description | -|--------|-------------| -| 200 | User libraries | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +| ------ | -------------- | +| 200 | User libraries | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -152,9 +152,9 @@ Grant library access to a user (admin only) #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | User ID | #### Request Body @@ -164,12 +164,12 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Access granted | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +| ------ | -------------- | +| 200 | Access granted | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -184,9 +184,9 @@ slashes that conflict with URL routing. #### Parameters -| Name | In | Required | Description | -|------|----|----------|-------------| -| `id` | path | Yes | User ID | +| Name | In | Required | Description | +| ---- | ---- | -------- | ----------- | +| `id` | path | Yes | User ID | #### Request Body @@ -196,12 +196,11 @@ See `docs/api/openapi.json` for the full schema. #### Responses -| Status | Description | -|--------|-------------| -| 200 | Access revoked | -| 400 | Bad request | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +| ------ | -------------- | +| 200 | Access revoked | +| 400 | Bad request | +| 401 | Unauthorized | +| 403 | Forbidden | --- - diff --git a/docs/api/webhooks.md b/docs/api/webhooks.md index 9005323..da0e1f4 100644 --- a/docs/api/webhooks.md +++ b/docs/api/webhooks.md @@ -10,11 +10,11 @@ Webhook configuration #### Responses -| Status | Description | -|--------|-------------| -| 200 | List of configured webhooks | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +| ------ | --------------------------- | +| 200 | List of configured webhooks | +| 401 | Unauthorized | +| 403 | Forbidden | --- @@ -24,11 +24,10 @@ Webhook configuration #### Responses -| Status | Description | -|--------|-------------| -| 200 | Test webhook sent | -| 401 | Unauthorized | -| 403 | Forbidden | +| Status | Description | +| ------ | ----------------- | +| 200 | Test webhook sent | +| 401 | Unauthorized | +| 403 | Forbidden | --- - diff --git a/examples/plugins/heif-support/README.md b/examples/plugins/heif-support/README.md index b64a002..d3e866d 100644 --- a/examples/plugins/heif-support/README.md +++ b/examples/plugins/heif-support/README.md @@ -1,18 +1,23 @@ # HEIF/HEIC Support Plugin -This example plugin adds support for HEIF (High Efficiency Image Format) and HEIC (HEIF Container) to Pinakes. +This example plugin adds support for HEIF (High Efficiency Image Format) and +HEIC (HEIF Container) to Pinakes. ## Overview -HEIF is a modern image format that provides better compression than JPEG while maintaining higher quality. This plugin enables Pinakes to: +HEIF is a modern image format that provides better compression than JPEG while +maintaining higher quality. This plugin enables Pinakes to: + - Recognize HEIF/HEIC files as a media type - Extract metadata from HEIF images - Generate thumbnails from HEIF images ## Features -- **Media Type Registration**: Registers `.heif`, `.heic`, `.hif` extensions as image media types -- **EXIF Extraction**: Extracts EXIF metadata including camera info, GPS coordinates, timestamps +- **Media Type Registration**: Registers `.heif`, `.heic`, `.hif` extensions as + image media types +- **EXIF Extraction**: Extracts EXIF metadata including camera info, GPS + coordinates, timestamps - **Thumbnail Generation**: Generates thumbnails in JPEG, PNG, or WebP format - **Resource Limits**: Configurable memory and CPU limits for safe processing - **Large Image Support**: Handles images up to 8192x8192 pixels @@ -95,6 +100,7 @@ impl ThumbnailGenerator for HeifPlugin { ## Dependencies The plugin uses the following Rust crates (compiled to WASM): + - `libheif-rs`: HEIF decoding and encoding - `image`: Image processing and thumbnail generation - `kamadak-exif`: EXIF metadata parsing @@ -159,15 +165,19 @@ pinakes plugin install /path/to/heif-support The plugin can be configured through the `config` section in `plugin.toml`: ### EXIF Extraction + - `extract_exif`: Enable EXIF metadata extraction (default: true) ### Thumbnail Generation + - `generate_thumbnails`: Enable thumbnail generation (default: true) - `thumbnail_quality`: JPEG quality for thumbnails, 1-100 (default: 85) - `thumbnail_format`: Output format - "jpeg", "png", or "webp" (default: "jpeg") ### Resource Limits -- `max_memory_mb`: Maximum memory the plugin can use in megabytes (default: 256, set in `[capabilities]`) + +- `max_memory_mb`: Maximum memory the plugin can use in megabytes (default: 256, + set in `[capabilities]`) - `max_width`: Maximum image width to process (default: 8192) - `max_height`: Maximum image height to process (default: 8192) @@ -188,6 +198,7 @@ The plugin can be configured through the `config` section in `plugin.toml`: ### Sandboxing The plugin runs in a WASM sandbox with: + - No access to host filesystem beyond granted paths - No network access - No arbitrary code execution @@ -210,6 +221,7 @@ The plugin runs in a WASM sandbox with: ## Error Handling The plugin handles: + - **Corrupted Files**: Returns descriptive error - **Unsupported Variants**: Gracefully skips unsupported HEIF features - **Memory Limits**: Fails safely if image too large diff --git a/examples/plugins/markdown-metadata/README.md b/examples/plugins/markdown-metadata/README.md index 91f2f4c..39044a4 100644 --- a/examples/plugins/markdown-metadata/README.md +++ b/examples/plugins/markdown-metadata/README.md @@ -1,10 +1,12 @@ # Markdown Metadata Extractor Plugin -This example plugin demonstrates how to create a metadata extractor plugin for Pinakes. +This example plugin demonstrates how to create a metadata extractor plugin for +Pinakes. ## Overview The Markdown Metadata Extractor enhances Pinakes' built-in markdown support by: + - Parsing YAML and TOML frontmatter - Extracting metadata from frontmatter fields - Converting frontmatter tags to Pinakes media tags @@ -12,8 +14,10 @@ The Markdown Metadata Extractor enhances Pinakes' built-in markdown support by: ## Features -- **Frontmatter Parsing**: Supports both YAML (`---`) and TOML (`+++`) frontmatter formats -- **Tag Extraction**: Automatically extracts tags from frontmatter and applies them to media items +- **Frontmatter Parsing**: Supports both YAML (`---`) and TOML (`+++`) + frontmatter formats +- **Tag Extraction**: Automatically extracts tags from frontmatter and applies + them to media items - **Custom Fields**: Preserves all frontmatter fields as custom metadata - **Configuration**: Configurable via `plugin.toml` config section @@ -88,6 +92,7 @@ The plugin can be configured through the `config` section in `plugin.toml`: ## Security This plugin has minimal capabilities: + - **Filesystem**: No write access, read access only to files being processed - **Network**: Disabled - **Environment**: No access diff --git a/flake.lock b/flake.lock index 52fc03b..c1c549c 100644 Binary files a/flake.lock and b/flake.lock differ diff --git a/flake.nix b/flake.nix index 5e4017d..d9ca783 100644 --- a/flake.nix +++ b/flake.nix @@ -21,5 +21,38 @@ in { default = pkgs.callPackage ./nix/shell.nix {}; }); + + formatter = forEachSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + in + pkgs.writeShellApplication { + name = "nix3-fmt-wrapper"; + + runtimeInputs = [ + pkgs.alejandra + pkgs.fd + pkgs.prettier + pkgs.deno + pkgs.taplo + pkgs.sql-formatter + ]; + + text = '' + # Format Nix with Alejandra + fd "$@" -t f -e nix -x alejandra -q '{}' + + # Format TOML with Taplo + fd "$@" -t f -e toml -x taplo fmt '{}' + + # Format CSS with Prettier + fd "$@" -t f -e css -x prettier --write '{}' + + # Format SQL with sql-format + fd "$@" -t f -e sql -x sql-formatter --fix '{}' -l postgresql + + # Format Markdown with Deno + fd "$@" -t f -e md -x deno fmt -q '{}' + ''; + }); }; } diff --git a/migrations/postgres/V10__incremental_scan.sql b/migrations/postgres/V10__incremental_scan.sql index 8fdc2cb..85b196e 100644 --- a/migrations/postgres/V10__incremental_scan.sql +++ b/migrations/postgres/V10__incremental_scan.sql @@ -1,19 +1,19 @@ -- Add file_mtime column to media_items table for incremental scanning -- Stores Unix timestamp in seconds of the file's modification time - -ALTER TABLE media_items ADD COLUMN file_mtime BIGINT; +ALTER TABLE media_items +ADD COLUMN file_mtime BIGINT; -- Create index for quick mtime lookups -CREATE INDEX IF NOT EXISTS idx_media_items_file_mtime ON media_items(file_mtime); +CREATE INDEX IF NOT EXISTS idx_media_items_file_mtime ON media_items (file_mtime); -- Create a scan_history table to track when each directory was last scanned CREATE TABLE IF NOT EXISTS scan_history ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - directory TEXT NOT NULL UNIQUE, - last_scan_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - files_scanned INTEGER NOT NULL DEFAULT 0, - files_changed INTEGER NOT NULL DEFAULT 0, - scan_duration_ms INTEGER + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + directory TEXT NOT NULL UNIQUE, + last_scan_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + files_scanned INTEGER NOT NULL DEFAULT 0, + files_changed INTEGER NOT NULL DEFAULT 0, + scan_duration_ms INTEGER ); -CREATE INDEX IF NOT EXISTS idx_scan_history_directory ON scan_history(directory); +CREATE INDEX IF NOT EXISTS idx_scan_history_directory ON scan_history (directory); diff --git a/migrations/postgres/V11__session_persistence.sql b/migrations/postgres/V11__session_persistence.sql index 8603d0b..d9b5f69 100644 --- a/migrations/postgres/V11__session_persistence.sql +++ b/migrations/postgres/V11__session_persistence.sql @@ -1,18 +1,17 @@ -- Session persistence for database-backed sessions -- Replaces in-memory session storage - CREATE TABLE IF NOT EXISTS sessions ( - session_token TEXT PRIMARY KEY NOT NULL, - user_id TEXT, - username TEXT NOT NULL, - role TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL, - expires_at TIMESTAMPTZ NOT NULL, - last_accessed TIMESTAMPTZ NOT NULL + session_token TEXT PRIMARY KEY NOT NULL, + user_id TEXT, + username TEXT NOT NULL, + role TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + last_accessed TIMESTAMPTZ NOT NULL ); -- Index for efficient cleanup of expired sessions -CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at); +CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions (expires_at); -- Index for listing sessions by username -CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions(username); +CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions (username); diff --git a/migrations/postgres/V12__book_management.sql b/migrations/postgres/V12__book_management.sql index 2452032..71e29f5 100644 --- a/migrations/postgres/V12__book_management.sql +++ b/migrations/postgres/V12__book_management.sql @@ -1,60 +1,61 @@ -- V12: Book Management Schema (PostgreSQL) -- Adds comprehensive book metadata tracking, authors, and identifiers - -- Book metadata (supplements media_items for EPUB/PDF/MOBI) CREATE TABLE book_metadata ( - media_id UUID PRIMARY KEY REFERENCES media_items(id) ON DELETE CASCADE, - isbn TEXT, - isbn13 TEXT, -- Normalized ISBN-13 for lookups - publisher TEXT, - language TEXT, -- ISO 639-1 code - page_count INTEGER, - publication_date DATE, - series_name TEXT, - series_index DOUBLE PRECISION, -- Supports 1.5, etc. - format TEXT, -- 'epub', 'pdf', 'mobi', 'azw3' - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + media_id UUID PRIMARY KEY REFERENCES media_items (id) ON DELETE CASCADE, + isbn TEXT, + isbn13 TEXT, -- Normalized ISBN-13 for lookups + publisher TEXT, + language TEXT, -- ISO 639-1 code + page_count INTEGER, + publication_date DATE, + series_name TEXT, + series_index DOUBLE PRECISION, -- Supports 1.5, etc. + format TEXT, -- 'epub', 'pdf', 'mobi', 'azw3' + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -CREATE INDEX idx_book_isbn13 ON book_metadata(isbn13); -CREATE INDEX idx_book_series ON book_metadata(series_name, series_index); -CREATE INDEX idx_book_publisher ON book_metadata(publisher); -CREATE INDEX idx_book_language ON book_metadata(language); +CREATE INDEX idx_book_isbn13 ON book_metadata (isbn13); + +CREATE INDEX idx_book_series ON book_metadata (series_name, series_index); + +CREATE INDEX idx_book_publisher ON book_metadata (publisher); + +CREATE INDEX idx_book_language ON book_metadata (language); -- Multiple authors per book (many-to-many) CREATE TABLE book_authors ( - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - author_name TEXT NOT NULL, - author_sort TEXT, -- "Last, First" for sorting - role TEXT NOT NULL DEFAULT 'author', -- author, translator, editor, illustrator - position INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (media_id, author_name, role) + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + author_name TEXT NOT NULL, + author_sort TEXT, -- "Last, First" for sorting + role TEXT NOT NULL DEFAULT 'author', -- author, translator, editor, illustrator + position INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (media_id, author_name, role) ); -CREATE INDEX idx_book_authors_name ON book_authors(author_name); -CREATE INDEX idx_book_authors_sort ON book_authors(author_sort); +CREATE INDEX idx_book_authors_name ON book_authors (author_name); + +CREATE INDEX idx_book_authors_sort ON book_authors (author_sort); -- Multiple identifiers (ISBN variants, ASIN, DOI, etc.) CREATE TABLE book_identifiers ( - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - identifier_type TEXT NOT NULL, -- isbn, isbn13, asin, doi, lccn, oclc - identifier_value TEXT NOT NULL, - PRIMARY KEY (media_id, identifier_type, identifier_value) + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + identifier_type TEXT NOT NULL, -- isbn, isbn13, asin, doi, lccn, oclc + identifier_value TEXT NOT NULL, + PRIMARY KEY (media_id, identifier_type, identifier_value) ); -CREATE INDEX idx_book_identifiers ON book_identifiers(identifier_type, identifier_value); +CREATE INDEX idx_book_identifiers ON book_identifiers (identifier_type, identifier_value); -- Trigger to update updated_at on book_metadata changes -CREATE OR REPLACE FUNCTION update_book_metadata_timestamp() -RETURNS TRIGGER AS $$ +CREATE OR REPLACE FUNCTION update_book_metadata_timestamp () RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -CREATE TRIGGER update_book_metadata_timestamp - BEFORE UPDATE ON book_metadata - FOR EACH ROW - EXECUTE FUNCTION update_book_metadata_timestamp(); +CREATE TRIGGER update_book_metadata_timestamp BEFORE +UPDATE ON book_metadata FOR EACH ROW +EXECUTE FUNCTION update_book_metadata_timestamp (); diff --git a/migrations/postgres/V13__photo_metadata.sql b/migrations/postgres/V13__photo_metadata.sql index f1365cd..7c66bb8 100644 --- a/migrations/postgres/V13__photo_metadata.sql +++ b/migrations/postgres/V13__photo_metadata.sql @@ -1,15 +1,40 @@ -- V13: Enhanced photo metadata support -- Add photo-specific fields to media_items table +ALTER TABLE media_items +ADD COLUMN date_taken TIMESTAMPTZ; -ALTER TABLE media_items ADD COLUMN date_taken TIMESTAMPTZ; -ALTER TABLE media_items ADD COLUMN latitude DOUBLE PRECISION; -ALTER TABLE media_items ADD COLUMN longitude DOUBLE PRECISION; -ALTER TABLE media_items ADD COLUMN camera_make TEXT; -ALTER TABLE media_items ADD COLUMN camera_model TEXT; -ALTER TABLE media_items ADD COLUMN rating INTEGER CHECK (rating >= 0 AND rating <= 5); +ALTER TABLE media_items +ADD COLUMN latitude DOUBLE PRECISION; + +ALTER TABLE media_items +ADD COLUMN longitude DOUBLE PRECISION; + +ALTER TABLE media_items +ADD COLUMN camera_make TEXT; + +ALTER TABLE media_items +ADD COLUMN camera_model TEXT; + +ALTER TABLE media_items +ADD COLUMN rating INTEGER CHECK ( + rating >= 0 + AND rating <= 5 +); -- Indexes for photo queries -CREATE INDEX idx_media_date_taken ON media_items(date_taken) WHERE date_taken IS NOT NULL; -CREATE INDEX idx_media_location ON media_items(latitude, longitude) WHERE latitude IS NOT NULL AND longitude IS NOT NULL; -CREATE INDEX idx_media_camera ON media_items(camera_make) WHERE camera_make IS NOT NULL; -CREATE INDEX idx_media_rating ON media_items(rating) WHERE rating IS NOT NULL; +CREATE INDEX idx_media_date_taken ON media_items (date_taken) +WHERE + date_taken IS NOT NULL; + +CREATE INDEX idx_media_location ON media_items (latitude, longitude) +WHERE + latitude IS NOT NULL + AND longitude IS NOT NULL; + +CREATE INDEX idx_media_camera ON media_items (camera_make) +WHERE + camera_make IS NOT NULL; + +CREATE INDEX idx_media_rating ON media_items (rating) +WHERE + rating IS NOT NULL; diff --git a/migrations/postgres/V14__perceptual_hash.sql b/migrations/postgres/V14__perceptual_hash.sql index 4bdc677..1d3c634 100644 --- a/migrations/postgres/V14__perceptual_hash.sql +++ b/migrations/postgres/V14__perceptual_hash.sql @@ -1,7 +1,9 @@ -- V14: Perceptual hash for duplicate detection -- Add perceptual hash column for image similarity detection - -ALTER TABLE media_items ADD COLUMN perceptual_hash TEXT; +ALTER TABLE media_items +ADD COLUMN perceptual_hash TEXT; -- Index for perceptual hash lookups -CREATE INDEX idx_media_phash ON media_items(perceptual_hash) WHERE perceptual_hash IS NOT NULL; +CREATE INDEX idx_media_phash ON media_items (perceptual_hash) +WHERE + perceptual_hash IS NOT NULL; diff --git a/migrations/postgres/V15__managed_storage.sql b/migrations/postgres/V15__managed_storage.sql index 56ef8f4..e3fb615 100644 --- a/migrations/postgres/V15__managed_storage.sql +++ b/migrations/postgres/V15__managed_storage.sql @@ -1,30 +1,33 @@ -- V15: Managed File Storage -- Adds server-side content-addressable storage for uploaded files - -- Add storage mode to media_items (external = file on disk, managed = in content-addressable storage) -ALTER TABLE media_items ADD COLUMN storage_mode TEXT NOT NULL DEFAULT 'external'; +ALTER TABLE media_items +ADD COLUMN storage_mode TEXT NOT NULL DEFAULT 'external'; -- Original filename for managed uploads (preserved separately from file_name which may be normalized) -ALTER TABLE media_items ADD COLUMN original_filename TEXT; +ALTER TABLE media_items +ADD COLUMN original_filename TEXT; -- When the file was uploaded to managed storage -ALTER TABLE media_items ADD COLUMN uploaded_at TIMESTAMPTZ; +ALTER TABLE media_items +ADD COLUMN uploaded_at TIMESTAMPTZ; -- Storage key for looking up the blob (usually same as content_hash for deduplication) -ALTER TABLE media_items ADD COLUMN storage_key TEXT; +ALTER TABLE media_items +ADD COLUMN storage_key TEXT; -- Managed blobs table - tracks deduplicated file storage CREATE TABLE managed_blobs ( - content_hash TEXT PRIMARY KEY NOT NULL, - file_size BIGINT NOT NULL, - mime_type TEXT NOT NULL, - reference_count INTEGER NOT NULL DEFAULT 1, - stored_at TIMESTAMPTZ NOT NULL, - last_verified TIMESTAMPTZ + content_hash TEXT PRIMARY KEY NOT NULL, + file_size BIGINT NOT NULL, + mime_type TEXT NOT NULL, + reference_count INTEGER NOT NULL DEFAULT 1, + stored_at TIMESTAMPTZ NOT NULL, + last_verified TIMESTAMPTZ ); -- Index for finding managed media items -CREATE INDEX idx_media_storage_mode ON media_items(storage_mode); +CREATE INDEX idx_media_storage_mode ON media_items (storage_mode); -- Index for finding orphaned blobs (reference_count = 0) -CREATE INDEX idx_blobs_reference_count ON managed_blobs(reference_count); +CREATE INDEX idx_blobs_reference_count ON managed_blobs (reference_count); diff --git a/migrations/postgres/V16__sync_system.sql b/migrations/postgres/V16__sync_system.sql index 8a87823..8647e12 100644 --- a/migrations/postgres/V16__sync_system.sql +++ b/migrations/postgres/V16__sync_system.sql @@ -1,91 +1,100 @@ -- V16: Cross-Device Sync System -- Adds device registration, change tracking, and chunked upload support - -- Sync devices table CREATE TABLE sync_devices ( - id TEXT PRIMARY KEY NOT NULL, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - name TEXT NOT NULL, - device_type TEXT NOT NULL, - client_version TEXT NOT NULL, - os_info TEXT, - device_token_hash TEXT NOT NULL UNIQUE, - last_sync_at TIMESTAMPTZ, - last_seen_at TIMESTAMPTZ NOT NULL, - sync_cursor BIGINT DEFAULT 0, - enabled BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, + name TEXT NOT NULL, + device_type TEXT NOT NULL, + client_version TEXT NOT NULL, + os_info TEXT, + device_token_hash TEXT NOT NULL UNIQUE, + last_sync_at TIMESTAMPTZ, + last_seen_at TIMESTAMPTZ NOT NULL, + sync_cursor BIGINT DEFAULT 0, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL ); -CREATE INDEX idx_sync_devices_user ON sync_devices(user_id); -CREATE INDEX idx_sync_devices_token ON sync_devices(device_token_hash); +CREATE INDEX idx_sync_devices_user ON sync_devices (user_id); + +CREATE INDEX idx_sync_devices_token ON sync_devices (device_token_hash); -- Sync log table - tracks all changes for sync CREATE TABLE sync_log ( - id TEXT PRIMARY KEY NOT NULL, - sequence BIGSERIAL UNIQUE NOT NULL, - change_type TEXT NOT NULL, - media_id TEXT REFERENCES media_items(id) ON DELETE SET NULL, - path TEXT NOT NULL, - content_hash TEXT, - file_size BIGINT, - metadata_json TEXT, - changed_by_device TEXT REFERENCES sync_devices(id) ON DELETE SET NULL, - timestamp TIMESTAMPTZ NOT NULL + id TEXT PRIMARY KEY NOT NULL, + sequence BIGSERIAL UNIQUE NOT NULL, + change_type TEXT NOT NULL, + media_id TEXT REFERENCES media_items (id) ON DELETE SET NULL, + path TEXT NOT NULL, + content_hash TEXT, + file_size BIGINT, + metadata_json TEXT, + changed_by_device TEXT REFERENCES sync_devices (id) ON DELETE SET NULL, + timestamp TIMESTAMPTZ NOT NULL ); -CREATE INDEX idx_sync_log_sequence ON sync_log(sequence); -CREATE INDEX idx_sync_log_path ON sync_log(path); -CREATE INDEX idx_sync_log_timestamp ON sync_log(timestamp); +CREATE INDEX idx_sync_log_sequence ON sync_log (sequence); + +CREATE INDEX idx_sync_log_path ON sync_log (path); + +CREATE INDEX idx_sync_log_timestamp ON sync_log (timestamp); -- Sequence counter for sync log CREATE TABLE sync_sequence ( - id INTEGER PRIMARY KEY CHECK (id = 1), - current_value BIGINT NOT NULL DEFAULT 0 + id INTEGER PRIMARY KEY CHECK (id = 1), + current_value BIGINT NOT NULL DEFAULT 0 ); -INSERT INTO sync_sequence (id, current_value) VALUES (1, 0); + +INSERT INTO + sync_sequence (id, current_value) +VALUES + (1, 0); -- Device sync state - tracks sync status per device per file CREATE TABLE device_sync_state ( - device_id TEXT NOT NULL REFERENCES sync_devices(id) ON DELETE CASCADE, - path TEXT NOT NULL, - local_hash TEXT, - server_hash TEXT, - local_mtime BIGINT, - server_mtime BIGINT, - sync_status TEXT NOT NULL, - last_synced_at TIMESTAMPTZ, - conflict_info_json TEXT, - PRIMARY KEY (device_id, path) + device_id TEXT NOT NULL REFERENCES sync_devices (id) ON DELETE CASCADE, + path TEXT NOT NULL, + local_hash TEXT, + server_hash TEXT, + local_mtime BIGINT, + server_mtime BIGINT, + sync_status TEXT NOT NULL, + last_synced_at TIMESTAMPTZ, + conflict_info_json TEXT, + PRIMARY KEY (device_id, path) ); -CREATE INDEX idx_device_sync_status ON device_sync_state(device_id, sync_status); +CREATE INDEX idx_device_sync_status ON device_sync_state (device_id, sync_status); -- Upload sessions for chunked uploads CREATE TABLE upload_sessions ( - id TEXT PRIMARY KEY NOT NULL, - device_id TEXT NOT NULL REFERENCES sync_devices(id) ON DELETE CASCADE, - target_path TEXT NOT NULL, - expected_hash TEXT NOT NULL, - expected_size BIGINT NOT NULL, - chunk_size BIGINT NOT NULL, - chunk_count BIGINT NOT NULL, - status TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL, - expires_at TIMESTAMPTZ NOT NULL, - last_activity TIMESTAMPTZ NOT NULL + id TEXT PRIMARY KEY NOT NULL, + device_id TEXT NOT NULL REFERENCES sync_devices (id) ON DELETE CASCADE, + target_path TEXT NOT NULL, + expected_hash TEXT NOT NULL, + expected_size BIGINT NOT NULL, + chunk_size BIGINT NOT NULL, + chunk_count BIGINT NOT NULL, + status TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + last_activity TIMESTAMPTZ NOT NULL ); -CREATE INDEX idx_upload_sessions_device ON upload_sessions(device_id); -CREATE INDEX idx_upload_sessions_status ON upload_sessions(status); -CREATE INDEX idx_upload_sessions_expires ON upload_sessions(expires_at); +CREATE INDEX idx_upload_sessions_device ON upload_sessions (device_id); + +CREATE INDEX idx_upload_sessions_status ON upload_sessions (status); + +CREATE INDEX idx_upload_sessions_expires ON upload_sessions (expires_at); -- Upload chunks - tracks received chunks CREATE TABLE upload_chunks ( - upload_id TEXT NOT NULL REFERENCES upload_sessions(id) ON DELETE CASCADE, - chunk_index BIGINT NOT NULL, - offset BIGINT NOT NULL, + upload_id TEXT NOT NULL REFERENCES upload_sessions (id) ON DELETE CASCADE, + chunk_index BIGINT NOT NULL, + offset + BIGINT NOT NULL, size BIGINT NOT NULL, hash TEXT NOT NULL, received_at TIMESTAMPTZ NOT NULL, @@ -94,17 +103,20 @@ CREATE TABLE upload_chunks ( -- Sync conflicts CREATE TABLE sync_conflicts ( - id TEXT PRIMARY KEY NOT NULL, - device_id TEXT NOT NULL REFERENCES sync_devices(id) ON DELETE CASCADE, - path TEXT NOT NULL, - local_hash TEXT NOT NULL, - local_mtime BIGINT NOT NULL, - server_hash TEXT NOT NULL, - server_mtime BIGINT NOT NULL, - detected_at TIMESTAMPTZ NOT NULL, - resolved_at TIMESTAMPTZ, - resolution TEXT + id TEXT PRIMARY KEY NOT NULL, + device_id TEXT NOT NULL REFERENCES sync_devices (id) ON DELETE CASCADE, + path TEXT NOT NULL, + local_hash TEXT NOT NULL, + local_mtime BIGINT NOT NULL, + server_hash TEXT NOT NULL, + server_mtime BIGINT NOT NULL, + detected_at TIMESTAMPTZ NOT NULL, + resolved_at TIMESTAMPTZ, + resolution TEXT ); -CREATE INDEX idx_sync_conflicts_device ON sync_conflicts(device_id); -CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts(device_id) WHERE resolved_at IS NULL; +CREATE INDEX idx_sync_conflicts_device ON sync_conflicts (device_id); + +CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts (device_id) +WHERE + resolved_at IS NULL; diff --git a/migrations/postgres/V17__enhanced_sharing.sql b/migrations/postgres/V17__enhanced_sharing.sql index 2107b5c..b068ae7 100644 --- a/migrations/postgres/V17__enhanced_sharing.sql +++ b/migrations/postgres/V17__enhanced_sharing.sql @@ -1,68 +1,85 @@ -- V17: Enhanced Sharing System -- Replaces simple share_links with comprehensive sharing capabilities - -- Enhanced shares table CREATE TABLE shares ( - id TEXT PRIMARY KEY NOT NULL, - target_type TEXT NOT NULL CHECK (target_type IN ('media', 'collection', 'tag', 'saved_search')), - target_id TEXT NOT NULL, - owner_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - recipient_type TEXT NOT NULL CHECK (recipient_type IN ('public_link', 'user', 'group', 'federated')), - recipient_user_id TEXT REFERENCES users(id) ON DELETE CASCADE, - recipient_group_id TEXT, - recipient_federated_handle TEXT, - recipient_federated_server TEXT, - public_token TEXT UNIQUE, - public_password_hash TEXT, - perm_view BOOLEAN NOT NULL DEFAULT TRUE, - perm_download BOOLEAN NOT NULL DEFAULT FALSE, - perm_edit BOOLEAN NOT NULL DEFAULT FALSE, - perm_delete BOOLEAN NOT NULL DEFAULT FALSE, - perm_reshare BOOLEAN NOT NULL DEFAULT FALSE, - perm_add BOOLEAN NOT NULL DEFAULT FALSE, - note TEXT, - expires_at TIMESTAMPTZ, - access_count BIGINT NOT NULL DEFAULT 0, - last_accessed TIMESTAMPTZ, - inherit_to_children BOOLEAN NOT NULL DEFAULT TRUE, - parent_share_id TEXT REFERENCES shares(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - UNIQUE(owner_id, target_type, target_id, recipient_type, recipient_user_id) + id TEXT PRIMARY KEY NOT NULL, + target_type TEXT NOT NULL CHECK ( + target_type IN ('media', 'collection', 'tag', 'saved_search') + ), + target_id TEXT NOT NULL, + owner_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, + recipient_type TEXT NOT NULL CHECK ( + recipient_type IN ('public_link', 'user', 'group', 'federated') + ), + recipient_user_id TEXT REFERENCES users (id) ON DELETE CASCADE, + recipient_group_id TEXT, + recipient_federated_handle TEXT, + recipient_federated_server TEXT, + public_token TEXT UNIQUE, + public_password_hash TEXT, + perm_view BOOLEAN NOT NULL DEFAULT TRUE, + perm_download BOOLEAN NOT NULL DEFAULT FALSE, + perm_edit BOOLEAN NOT NULL DEFAULT FALSE, + perm_delete BOOLEAN NOT NULL DEFAULT FALSE, + perm_reshare BOOLEAN NOT NULL DEFAULT FALSE, + perm_add BOOLEAN NOT NULL DEFAULT FALSE, + note TEXT, + expires_at TIMESTAMPTZ, + access_count BIGINT NOT NULL DEFAULT 0, + last_accessed TIMESTAMPTZ, + inherit_to_children BOOLEAN NOT NULL DEFAULT TRUE, + parent_share_id TEXT REFERENCES shares (id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL, + UNIQUE ( + owner_id, + target_type, + target_id, + recipient_type, + recipient_user_id + ) ); -CREATE INDEX idx_shares_owner ON shares(owner_id); -CREATE INDEX idx_shares_recipient_user ON shares(recipient_user_id); -CREATE INDEX idx_shares_target ON shares(target_type, target_id); -CREATE INDEX idx_shares_token ON shares(public_token); -CREATE INDEX idx_shares_expires ON shares(expires_at); +CREATE INDEX idx_shares_owner ON shares (owner_id); + +CREATE INDEX idx_shares_recipient_user ON shares (recipient_user_id); + +CREATE INDEX idx_shares_target ON shares (target_type, target_id); + +CREATE INDEX idx_shares_token ON shares (public_token); + +CREATE INDEX idx_shares_expires ON shares (expires_at); -- Share activity log CREATE TABLE share_activity ( - id TEXT PRIMARY KEY NOT NULL, - share_id TEXT NOT NULL REFERENCES shares(id) ON DELETE CASCADE, - actor_id TEXT REFERENCES users(id) ON DELETE SET NULL, - actor_ip TEXT, - action TEXT NOT NULL, - details TEXT, - timestamp TIMESTAMPTZ NOT NULL + id TEXT PRIMARY KEY NOT NULL, + share_id TEXT NOT NULL REFERENCES shares (id) ON DELETE CASCADE, + actor_id TEXT REFERENCES users (id) ON DELETE SET NULL, + actor_ip TEXT, + action TEXT NOT NULL, + details TEXT, + timestamp TIMESTAMPTZ NOT NULL ); -CREATE INDEX idx_share_activity_share ON share_activity(share_id); -CREATE INDEX idx_share_activity_timestamp ON share_activity(timestamp); +CREATE INDEX idx_share_activity_share ON share_activity (share_id); + +CREATE INDEX idx_share_activity_timestamp ON share_activity (timestamp); -- Share notifications CREATE TABLE share_notifications ( - id TEXT PRIMARY KEY NOT NULL, - user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - share_id TEXT NOT NULL REFERENCES shares(id) ON DELETE CASCADE, - notification_type TEXT NOT NULL, - is_read BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ NOT NULL + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL REFERENCES users (id) ON DELETE CASCADE, + share_id TEXT NOT NULL REFERENCES shares (id) ON DELETE CASCADE, + notification_type TEXT NOT NULL, + is_read BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL ); -CREATE INDEX idx_share_notifications_user ON share_notifications(user_id); -CREATE INDEX idx_share_notifications_unread ON share_notifications(user_id) WHERE is_read = FALSE; +CREATE INDEX idx_share_notifications_user ON share_notifications (user_id); + +CREATE INDEX idx_share_notifications_unread ON share_notifications (user_id) +WHERE + is_read = FALSE; -- Migrate existing share_links to new shares table DO $$ diff --git a/migrations/postgres/V18__file_management.sql b/migrations/postgres/V18__file_management.sql index 30403c3..6059e65 100644 --- a/migrations/postgres/V18__file_management.sql +++ b/migrations/postgres/V18__file_management.sql @@ -1,11 +1,13 @@ -- V18: File Management (Rename, Move, Trash) -- Adds soft delete support for trash/recycle bin functionality - -- Add deleted_at column for soft delete (trash) -ALTER TABLE media_items ADD COLUMN deleted_at TIMESTAMPTZ; +ALTER TABLE media_items +ADD COLUMN deleted_at TIMESTAMPTZ; -- Index for efficient trash queries -CREATE INDEX idx_media_deleted_at ON media_items(deleted_at); +CREATE INDEX idx_media_deleted_at ON media_items (deleted_at); -- Partial index for listing non-deleted items (most common query pattern) -CREATE INDEX idx_media_not_deleted ON media_items(id) WHERE deleted_at IS NULL; +CREATE INDEX idx_media_not_deleted ON media_items (id) +WHERE + deleted_at IS NULL; diff --git a/migrations/postgres/V19__markdown_links.sql b/migrations/postgres/V19__markdown_links.sql index b0d5475..084726e 100644 --- a/migrations/postgres/V19__markdown_links.sql +++ b/migrations/postgres/V19__markdown_links.sql @@ -1,35 +1,35 @@ -- V19: Markdown Links (Obsidian-style bidirectional links) -- Adds support for wikilinks, markdown links, embeds, and backlink tracking - -- Table for storing extracted markdown links CREATE TABLE IF NOT EXISTS markdown_links ( - id TEXT PRIMARY KEY NOT NULL, - source_media_id TEXT NOT NULL, - target_path TEXT NOT NULL, -- raw link target (wikilink or path) - target_media_id TEXT, -- resolved media_id (nullable if unresolved) - link_type TEXT NOT NULL, -- 'wikilink', 'markdown_link', 'embed' - link_text TEXT, -- display text for the link - line_number INTEGER, -- line number in source file - context TEXT, -- surrounding text for preview - created_at TIMESTAMPTZ NOT NULL, - FOREIGN KEY (source_media_id) REFERENCES media_items(id) ON DELETE CASCADE, - FOREIGN KEY (target_media_id) REFERENCES media_items(id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + source_media_id TEXT NOT NULL, + target_path TEXT NOT NULL, -- raw link target (wikilink or path) + target_media_id TEXT, -- resolved media_id (nullable if unresolved) + link_type TEXT NOT NULL, -- 'wikilink', 'markdown_link', 'embed' + link_text TEXT, -- display text for the link + line_number INTEGER, -- line number in source file + context TEXT, -- surrounding text for preview + created_at TIMESTAMPTZ NOT NULL, + FOREIGN KEY (source_media_id) REFERENCES media_items (id) ON DELETE CASCADE, + FOREIGN KEY (target_media_id) REFERENCES media_items (id) ON DELETE SET NULL ); -- Index for efficient outgoing link queries (what does this note link to?) -CREATE INDEX idx_links_source ON markdown_links(source_media_id); +CREATE INDEX idx_links_source ON markdown_links (source_media_id); -- Index for efficient backlink queries (what links to this note?) -CREATE INDEX idx_links_target ON markdown_links(target_media_id); +CREATE INDEX idx_links_target ON markdown_links (target_media_id); -- Index for path-based resolution (finding unresolved links) -CREATE INDEX idx_links_target_path ON markdown_links(target_path); +CREATE INDEX idx_links_target_path ON markdown_links (target_path); -- Index for link type filtering -CREATE INDEX idx_links_type ON markdown_links(link_type); +CREATE INDEX idx_links_type ON markdown_links (link_type); -- Track when links were last extracted from a media item -ALTER TABLE media_items ADD COLUMN links_extracted_at TIMESTAMPTZ; +ALTER TABLE media_items +ADD COLUMN links_extracted_at TIMESTAMPTZ; -- Index for finding media items that need link extraction -CREATE INDEX idx_media_links_extracted ON media_items(links_extracted_at); +CREATE INDEX idx_media_links_extracted ON media_items (links_extracted_at); diff --git a/migrations/postgres/V1__initial_schema.sql b/migrations/postgres/V1__initial_schema.sql index cd6a0c8..c1c49af 100644 --- a/migrations/postgres/V1__initial_schema.sql +++ b/migrations/postgres/V1__initial_schema.sql @@ -1,73 +1,75 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + CREATE EXTENSION IF NOT EXISTS pg_trgm; -CREATE TABLE IF NOT EXISTS root_dirs ( - path TEXT PRIMARY KEY NOT NULL -); +CREATE TABLE IF NOT EXISTS root_dirs (path TEXT PRIMARY KEY NOT NULL); CREATE TABLE IF NOT EXISTS media_items ( - id UUID PRIMARY KEY NOT NULL, - path TEXT NOT NULL UNIQUE, - file_name TEXT NOT NULL, - media_type TEXT NOT NULL, - content_hash TEXT NOT NULL UNIQUE, - file_size BIGINT NOT NULL, - title TEXT, - artist TEXT, - album TEXT, - genre TEXT, - year INTEGER, - duration_secs DOUBLE PRECISION, - description TEXT, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL + id UUID PRIMARY KEY NOT NULL, + path TEXT NOT NULL UNIQUE, + file_name TEXT NOT NULL, + media_type TEXT NOT NULL, + content_hash TEXT NOT NULL UNIQUE, + file_size BIGINT NOT NULL, + title TEXT, + artist TEXT, + album TEXT, + genre TEXT, + year INTEGER, + duration_secs DOUBLE PRECISION, + description TEXT, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL ); CREATE TABLE IF NOT EXISTS tags ( - id UUID PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - parent_id UUID REFERENCES tags(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ NOT NULL + id UUID PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + parent_id UUID REFERENCES tags (id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL ); -CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name_parent ON tags(name, COALESCE(parent_id, '00000000-0000-0000-0000-000000000000')); +CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name_parent ON tags ( + name, + COALESCE(parent_id, '00000000-0000-0000-0000-000000000000') +); CREATE TABLE IF NOT EXISTS media_tags ( - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE, - PRIMARY KEY (media_id, tag_id) + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + tag_id UUID NOT NULL REFERENCES tags (id) ON DELETE CASCADE, + PRIMARY KEY (media_id, tag_id) ); CREATE TABLE IF NOT EXISTS collections ( - id UUID PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - description TEXT, - kind TEXT NOT NULL, - filter_query TEXT, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL + id UUID PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT, + kind TEXT NOT NULL, + filter_query TEXT, + created_at TIMESTAMPTZ NOT NULL, + updated_at TIMESTAMPTZ NOT NULL ); CREATE TABLE IF NOT EXISTS collection_members ( - collection_id UUID NOT NULL REFERENCES collections(id) ON DELETE CASCADE, - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - position INTEGER NOT NULL DEFAULT 0, - added_at TIMESTAMPTZ NOT NULL, - PRIMARY KEY (collection_id, media_id) + collection_id UUID NOT NULL REFERENCES collections (id) ON DELETE CASCADE, + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + position INTEGER NOT NULL DEFAULT 0, + added_at TIMESTAMPTZ NOT NULL, + PRIMARY KEY (collection_id, media_id) ); CREATE TABLE IF NOT EXISTS audit_log ( - id UUID PRIMARY KEY NOT NULL, - media_id UUID REFERENCES media_items(id) ON DELETE SET NULL, - action TEXT NOT NULL, - details TEXT, - timestamp TIMESTAMPTZ NOT NULL + id UUID PRIMARY KEY NOT NULL, + media_id UUID REFERENCES media_items (id) ON DELETE SET NULL, + action TEXT NOT NULL, + details TEXT, + timestamp TIMESTAMPTZ NOT NULL ); CREATE TABLE IF NOT EXISTS custom_fields ( - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - field_name TEXT NOT NULL, - field_type TEXT NOT NULL, - field_value TEXT NOT NULL, - PRIMARY KEY (media_id, field_name) + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + field_name TEXT NOT NULL, + field_type TEXT NOT NULL, + field_value TEXT NOT NULL, + PRIMARY KEY (media_id, field_name) ); diff --git a/migrations/postgres/V2__fts_indexes.sql b/migrations/postgres/V2__fts_indexes.sql index 510fce6..543fdce 100644 --- a/migrations/postgres/V2__fts_indexes.sql +++ b/migrations/postgres/V2__fts_indexes.sql @@ -1,11 +1,12 @@ -ALTER TABLE media_items ADD COLUMN IF NOT EXISTS search_vector tsvector - GENERATED ALWAYS AS ( - setweight(to_tsvector('english', COALESCE(title, '')), 'A') || - setweight(to_tsvector('english', COALESCE(artist, '')), 'B') || - setweight(to_tsvector('english', COALESCE(album, '')), 'B') || - setweight(to_tsvector('english', COALESCE(genre, '')), 'C') || - setweight(to_tsvector('english', COALESCE(description, '')), 'C') || - setweight(to_tsvector('english', COALESCE(file_name, '')), 'D') - ) STORED; +ALTER TABLE media_items +ADD COLUMN IF NOT EXISTS search_vector tsvector GENERATED ALWAYS AS ( + setweight(to_tsvector('english', COALESCE(title, '')), 'A') || setweight(to_tsvector('english', COALESCE(artist, '')), 'B') || setweight(to_tsvector('english', COALESCE(album, '')), 'B') || setweight(to_tsvector('english', COALESCE(genre, '')), 'C') || setweight( + to_tsvector('english', COALESCE(description, '')), + 'C' + ) || setweight( + to_tsvector('english', COALESCE(file_name, '')), + 'D' + ) +) STORED; -CREATE INDEX IF NOT EXISTS idx_media_search ON media_items USING GIN(search_vector); +CREATE INDEX IF NOT EXISTS idx_media_search ON media_items USING GIN (search_vector); diff --git a/migrations/postgres/V3__audit_indexes.sql b/migrations/postgres/V3__audit_indexes.sql index d8c423a..85ffa06 100644 --- a/migrations/postgres/V3__audit_indexes.sql +++ b/migrations/postgres/V3__audit_indexes.sql @@ -1,8 +1,15 @@ -CREATE INDEX IF NOT EXISTS idx_audit_media_id ON audit_log(media_id); -CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp); -CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action); -CREATE INDEX IF NOT EXISTS idx_media_content_hash ON media_items(content_hash); -CREATE INDEX IF NOT EXISTS idx_media_media_type ON media_items(media_type); -CREATE INDEX IF NOT EXISTS idx_media_created_at ON media_items(created_at); -CREATE INDEX IF NOT EXISTS idx_media_title_trgm ON media_items USING GIN(title gin_trgm_ops); -CREATE INDEX IF NOT EXISTS idx_media_artist_trgm ON media_items USING GIN(artist gin_trgm_ops); +CREATE INDEX IF NOT EXISTS idx_audit_media_id ON audit_log (media_id); + +CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log (timestamp); + +CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action); + +CREATE INDEX IF NOT EXISTS idx_media_content_hash ON media_items (content_hash); + +CREATE INDEX IF NOT EXISTS idx_media_media_type ON media_items (media_type); + +CREATE INDEX IF NOT EXISTS idx_media_created_at ON media_items (created_at); + +CREATE INDEX IF NOT EXISTS idx_media_title_trgm ON media_items USING GIN (title gin_trgm_ops); + +CREATE INDEX IF NOT EXISTS idx_media_artist_trgm ON media_items USING GIN (artist gin_trgm_ops); diff --git a/migrations/postgres/V4__thumbnail_path.sql b/migrations/postgres/V4__thumbnail_path.sql index 9021884..4c23b5b 100644 --- a/migrations/postgres/V4__thumbnail_path.sql +++ b/migrations/postgres/V4__thumbnail_path.sql @@ -1 +1,2 @@ -ALTER TABLE media_items ADD COLUMN thumbnail_path TEXT; +ALTER TABLE media_items +ADD COLUMN thumbnail_path TEXT; diff --git a/migrations/postgres/V5__integrity_and_saved_searches.sql b/migrations/postgres/V5__integrity_and_saved_searches.sql index f2807f4..fd62baf 100644 --- a/migrations/postgres/V5__integrity_and_saved_searches.sql +++ b/migrations/postgres/V5__integrity_and_saved_searches.sql @@ -1,12 +1,15 @@ -- Integrity tracking columns -ALTER TABLE media_items ADD COLUMN last_verified_at TIMESTAMPTZ; -ALTER TABLE media_items ADD COLUMN integrity_status TEXT DEFAULT 'unverified'; +ALTER TABLE media_items +ADD COLUMN last_verified_at TIMESTAMPTZ; + +ALTER TABLE media_items +ADD COLUMN integrity_status TEXT DEFAULT 'unverified'; -- Saved searches CREATE TABLE IF NOT EXISTS saved_searches ( - id UUID PRIMARY KEY, - name TEXT NOT NULL, - query TEXT NOT NULL, - sort_order TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + name TEXT NOT NULL, + query TEXT NOT NULL, + sort_order TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); diff --git a/migrations/postgres/V6__plugin_system.sql b/migrations/postgres/V6__plugin_system.sql index bd52a1a..b40cf41 100644 --- a/migrations/postgres/V6__plugin_system.sql +++ b/migrations/postgres/V6__plugin_system.sql @@ -1,15 +1,16 @@ -- Plugin registry table CREATE TABLE plugin_registry ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - version TEXT NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT TRUE, - config_json TEXT, - manifest_json TEXT, - installed_at TIMESTAMP WITH TIME ZONE NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + version TEXT NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + config_json TEXT, + manifest_json TEXT, + installed_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL ); -- Index for quick lookups -CREATE INDEX idx_plugin_registry_enabled ON plugin_registry(enabled); -CREATE INDEX idx_plugin_registry_name ON plugin_registry(name); +CREATE INDEX idx_plugin_registry_enabled ON plugin_registry (enabled); + +CREATE INDEX idx_plugin_registry_name ON plugin_registry (name); diff --git a/migrations/postgres/V7__user_management.sql b/migrations/postgres/V7__user_management.sql index a9eb12f..c405c80 100644 --- a/migrations/postgres/V7__user_management.sql +++ b/migrations/postgres/V7__user_management.sql @@ -1,35 +1,37 @@ -- Users table CREATE TABLE users ( - id UUID PRIMARY KEY, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - role JSONB NOT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL + id UUID PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role JSONB NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL ); -- User profiles table CREATE TABLE user_profiles ( - user_id UUID PRIMARY KEY, - avatar_path TEXT, - bio TEXT, - preferences_json JSONB NOT NULL DEFAULT '{}', - created_at TIMESTAMP WITH TIME ZONE NOT NULL, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) + user_id UUID PRIMARY KEY, + avatar_path TEXT, + bio TEXT, + preferences_json JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) ); -- User library access table CREATE TABLE user_libraries ( - user_id UUID NOT NULL, - root_path TEXT NOT NULL, - permission JSONB NOT NULL, - granted_at TIMESTAMP WITH TIME ZONE NOT NULL, - PRIMARY KEY (user_id, root_path), - FOREIGN KEY (user_id) REFERENCES users(id) + user_id UUID NOT NULL, + root_path TEXT NOT NULL, + permission JSONB NOT NULL, + granted_at TIMESTAMP WITH TIME ZONE NOT NULL, + PRIMARY KEY (user_id, root_path), + FOREIGN KEY (user_id) REFERENCES users (id) ); -- Indexes for efficient lookups -CREATE INDEX idx_users_username ON users(username); -CREATE INDEX idx_user_libraries_user_id ON user_libraries(user_id); -CREATE INDEX idx_user_libraries_root_path ON user_libraries(root_path); +CREATE INDEX idx_users_username ON users (username); + +CREATE INDEX idx_user_libraries_user_id ON user_libraries (user_id); + +CREATE INDEX idx_user_libraries_root_path ON user_libraries (root_path); diff --git a/migrations/postgres/V8__media_server_features.sql b/migrations/postgres/V8__media_server_features.sql index 7d22838..2594bd4 100644 --- a/migrations/postgres/V8__media_server_features.sql +++ b/migrations/postgres/V8__media_server_features.sql @@ -1,131 +1,136 @@ -- Ratings CREATE TABLE IF NOT EXISTS ratings ( - id UUID PRIMARY KEY, - user_id UUID NOT NULL, - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - stars INTEGER NOT NULL CHECK (stars >= 1 AND stars <= 5), - review_text TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE(user_id, media_id) + id UUID PRIMARY KEY, + user_id UUID NOT NULL, + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + stars INTEGER NOT NULL CHECK ( + stars >= 1 + AND stars <= 5 + ), + review_text TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (user_id, media_id) ); -- Comments CREATE TABLE IF NOT EXISTS comments ( - id UUID PRIMARY KEY, - user_id UUID NOT NULL, - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - parent_comment_id UUID REFERENCES comments(id) ON DELETE CASCADE, - text TEXT NOT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + user_id UUID NOT NULL, + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + parent_comment_id UUID REFERENCES comments (id) ON DELETE CASCADE, + text TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Favorites CREATE TABLE IF NOT EXISTS favorites ( - user_id UUID NOT NULL, - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (user_id, media_id) + user_id UUID NOT NULL, + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (user_id, media_id) ); -- Share links CREATE TABLE IF NOT EXISTS share_links ( - id UUID PRIMARY KEY, - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - created_by UUID NOT NULL, - token TEXT NOT NULL UNIQUE, - password_hash TEXT, - expires_at TIMESTAMPTZ, - view_count INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + created_by UUID NOT NULL, + token TEXT NOT NULL UNIQUE, + password_hash TEXT, + expires_at TIMESTAMPTZ, + view_count INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Playlists CREATE TABLE IF NOT EXISTS playlists ( - id UUID PRIMARY KEY, - owner_id UUID NOT NULL, - name TEXT NOT NULL, - description TEXT, - is_public BOOLEAN NOT NULL DEFAULT FALSE, - is_smart BOOLEAN NOT NULL DEFAULT FALSE, - filter_query TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + owner_id UUID NOT NULL, + name TEXT NOT NULL, + description TEXT, + is_public BOOLEAN NOT NULL DEFAULT FALSE, + is_smart BOOLEAN NOT NULL DEFAULT FALSE, + filter_query TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Playlist items CREATE TABLE IF NOT EXISTS playlist_items ( - playlist_id UUID NOT NULL REFERENCES playlists(id) ON DELETE CASCADE, - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - position INTEGER NOT NULL DEFAULT 0, - added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (playlist_id, media_id) + playlist_id UUID NOT NULL REFERENCES playlists (id) ON DELETE CASCADE, + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + position INTEGER NOT NULL DEFAULT 0, + added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + PRIMARY KEY (playlist_id, media_id) ); -- Usage events CREATE TABLE IF NOT EXISTS usage_events ( - id UUID PRIMARY KEY, - media_id UUID REFERENCES media_items(id) ON DELETE SET NULL, - user_id UUID, - event_type TEXT NOT NULL, - timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), - duration_secs DOUBLE PRECISION, - context_json JSONB + id UUID PRIMARY KEY, + media_id UUID REFERENCES media_items (id) ON DELETE SET NULL, + user_id UUID, + event_type TEXT NOT NULL, + timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), + duration_secs DOUBLE PRECISION, + context_json JSONB ); -CREATE INDEX IF NOT EXISTS idx_usage_events_media ON usage_events(media_id); -CREATE INDEX IF NOT EXISTS idx_usage_events_user ON usage_events(user_id); -CREATE INDEX IF NOT EXISTS idx_usage_events_timestamp ON usage_events(timestamp); +CREATE INDEX IF NOT EXISTS idx_usage_events_media ON usage_events (media_id); + +CREATE INDEX IF NOT EXISTS idx_usage_events_user ON usage_events (user_id); + +CREATE INDEX IF NOT EXISTS idx_usage_events_timestamp ON usage_events (timestamp); -- Watch history / progress CREATE TABLE IF NOT EXISTS watch_history ( - id UUID PRIMARY KEY, - user_id UUID NOT NULL, - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - progress_secs DOUBLE PRECISION NOT NULL DEFAULT 0, - last_watched TIMESTAMPTZ NOT NULL DEFAULT NOW(), - UNIQUE(user_id, media_id) + id UUID PRIMARY KEY, + user_id UUID NOT NULL, + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + progress_secs DOUBLE PRECISION NOT NULL DEFAULT 0, + last_watched TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (user_id, media_id) ); -- Subtitles CREATE TABLE IF NOT EXISTS subtitles ( - id UUID PRIMARY KEY, - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - language TEXT, - format TEXT NOT NULL, - file_path TEXT, - is_embedded BOOLEAN NOT NULL DEFAULT FALSE, - track_index INTEGER, - offset_ms INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + language TEXT, + format TEXT NOT NULL, + file_path TEXT, + is_embedded BOOLEAN NOT NULL DEFAULT FALSE, + track_index INTEGER, + offset_ms INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -CREATE INDEX IF NOT EXISTS idx_subtitles_media ON subtitles(media_id); +CREATE INDEX IF NOT EXISTS idx_subtitles_media ON subtitles (media_id); -- External metadata (enrichment) CREATE TABLE IF NOT EXISTS external_metadata ( - id UUID PRIMARY KEY, - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - source TEXT NOT NULL, - external_id TEXT, - metadata_json JSONB NOT NULL DEFAULT '{}', - confidence DOUBLE PRECISION NOT NULL DEFAULT 0.0, - last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW() + id UUID PRIMARY KEY, + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + source TEXT NOT NULL, + external_id TEXT, + metadata_json JSONB NOT NULL DEFAULT '{}', + confidence DOUBLE PRECISION NOT NULL DEFAULT 0.0, + last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -CREATE INDEX IF NOT EXISTS idx_external_metadata_media ON external_metadata(media_id); +CREATE INDEX IF NOT EXISTS idx_external_metadata_media ON external_metadata (media_id); -- Transcode sessions CREATE TABLE IF NOT EXISTS transcode_sessions ( - id UUID PRIMARY KEY, - media_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - user_id UUID, - profile TEXT NOT NULL, - cache_path TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - progress DOUBLE PRECISION NOT NULL DEFAULT 0.0, - error_message TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - expires_at TIMESTAMPTZ + id UUID PRIMARY KEY, + media_id UUID NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + user_id UUID, + profile TEXT NOT NULL, + cache_path TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + progress DOUBLE PRECISION NOT NULL DEFAULT 0.0, + error_message TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ ); -CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions(media_id); +CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions (media_id); diff --git a/migrations/postgres/V9__fix_indexes_and_constraints.sql b/migrations/postgres/V9__fix_indexes_and_constraints.sql index a65fda3..9446a94 100644 --- a/migrations/postgres/V9__fix_indexes_and_constraints.sql +++ b/migrations/postgres/V9__fix_indexes_and_constraints.sql @@ -1,19 +1,26 @@ -- Drop redundant indexes (already covered by UNIQUE constraints) DROP INDEX IF EXISTS idx_users_username; + DROP INDEX IF EXISTS idx_user_libraries_user_id; -- Add missing indexes for comments table -CREATE INDEX IF NOT EXISTS idx_comments_media ON comments(media_id); -CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments(parent_comment_id); +CREATE INDEX IF NOT EXISTS idx_comments_media ON comments (media_id); + +CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments (parent_comment_id); -- Remove duplicates before adding unique constraint DELETE FROM external_metadata e1 -WHERE EXISTS ( - SELECT 1 FROM external_metadata e2 - WHERE e1.media_id = e2.media_id - AND e1.source = e2.source - AND e1.ctid < e2.ctid -); +WHERE + EXISTS ( + SELECT + 1 + FROM + external_metadata e2 + WHERE + e1.media_id = e2.media_id + AND e1.source = e2.source + AND e1.ctid < e2.ctid + ); -- Add unique constraint for external_metadata (idempotent) DO $$ diff --git a/migrations/sqlite/V10__incremental_scan.sql b/migrations/sqlite/V10__incremental_scan.sql index 76db5c9..a070c7f 100644 --- a/migrations/sqlite/V10__incremental_scan.sql +++ b/migrations/sqlite/V10__incremental_scan.sql @@ -1,19 +1,19 @@ -- Add file_mtime column to media_items table for incremental scanning -- Stores Unix timestamp in seconds of the file's modification time - -ALTER TABLE media_items ADD COLUMN file_mtime INTEGER; +ALTER TABLE media_items +ADD COLUMN file_mtime INTEGER; -- Create index for quick mtime lookups -CREATE INDEX IF NOT EXISTS idx_media_items_file_mtime ON media_items(file_mtime); +CREATE INDEX IF NOT EXISTS idx_media_items_file_mtime ON media_items (file_mtime); -- Create a scan_history table to track when each directory was last scanned CREATE TABLE IF NOT EXISTS scan_history ( - id TEXT PRIMARY KEY, - directory TEXT NOT NULL UNIQUE, - last_scan_at TEXT NOT NULL, - files_scanned INTEGER NOT NULL DEFAULT 0, - files_changed INTEGER NOT NULL DEFAULT 0, - scan_duration_ms INTEGER + id TEXT PRIMARY KEY, + directory TEXT NOT NULL UNIQUE, + last_scan_at TEXT NOT NULL, + files_scanned INTEGER NOT NULL DEFAULT 0, + files_changed INTEGER NOT NULL DEFAULT 0, + scan_duration_ms INTEGER ); -CREATE INDEX IF NOT EXISTS idx_scan_history_directory ON scan_history(directory); +CREATE INDEX IF NOT EXISTS idx_scan_history_directory ON scan_history (directory); diff --git a/migrations/sqlite/V11__session_persistence.sql b/migrations/sqlite/V11__session_persistence.sql index b4e2753..e5e7a94 100644 --- a/migrations/sqlite/V11__session_persistence.sql +++ b/migrations/sqlite/V11__session_persistence.sql @@ -1,18 +1,17 @@ -- Session persistence for database-backed sessions -- Replaces in-memory session storage - CREATE TABLE IF NOT EXISTS sessions ( - session_token TEXT PRIMARY KEY NOT NULL, - user_id TEXT, - username TEXT NOT NULL, - role TEXT NOT NULL, - created_at TEXT NOT NULL, - expires_at TEXT NOT NULL, - last_accessed TEXT NOT NULL + session_token TEXT PRIMARY KEY NOT NULL, + user_id TEXT, + username TEXT NOT NULL, + role TEXT NOT NULL, + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL, + last_accessed TEXT NOT NULL ); -- Index for efficient cleanup of expired sessions -CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at); +CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions (expires_at); -- Index for listing sessions by username -CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions(username); +CREATE INDEX IF NOT EXISTS idx_sessions_username ON sessions (username); diff --git a/migrations/sqlite/V12__book_management.sql b/migrations/sqlite/V12__book_management.sql index 9823b87..9b18100 100644 --- a/migrations/sqlite/V12__book_management.sql +++ b/migrations/sqlite/V12__book_management.sql @@ -1,54 +1,62 @@ -- V12: Book Management Schema -- Adds comprehensive book metadata tracking, authors, and identifiers - -- Book metadata (supplements media_items for EPUB/PDF/MOBI) CREATE TABLE book_metadata ( - media_id TEXT PRIMARY KEY REFERENCES media_items(id) ON DELETE CASCADE, - isbn TEXT, - isbn13 TEXT, -- Normalized ISBN-13 for lookups - publisher TEXT, - language TEXT, -- ISO 639-1 code - page_count INTEGER, - publication_date TEXT, -- ISO 8601 date string - series_name TEXT, - series_index REAL, -- Supports 1.5, etc. - format TEXT, -- 'epub', 'pdf', 'mobi', 'azw3' - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) + media_id TEXT PRIMARY KEY REFERENCES media_items (id) ON DELETE CASCADE, + isbn TEXT, + isbn13 TEXT, -- Normalized ISBN-13 for lookups + publisher TEXT, + language TEXT, -- ISO 639-1 code + page_count INTEGER, + publication_date TEXT, -- ISO 8601 date string + series_name TEXT, + series_index REAL, -- Supports 1.5, etc. + format TEXT, -- 'epub', 'pdf', 'mobi', 'azw3' + created_at TEXT NOT NULL DEFAULT (datetime ('now')), + updated_at TEXT NOT NULL DEFAULT (datetime ('now')) ) STRICT; -CREATE INDEX idx_book_isbn13 ON book_metadata(isbn13); -CREATE INDEX idx_book_series ON book_metadata(series_name, series_index); -CREATE INDEX idx_book_publisher ON book_metadata(publisher); -CREATE INDEX idx_book_language ON book_metadata(language); +CREATE INDEX idx_book_isbn13 ON book_metadata (isbn13); + +CREATE INDEX idx_book_series ON book_metadata (series_name, series_index); + +CREATE INDEX idx_book_publisher ON book_metadata (publisher); + +CREATE INDEX idx_book_language ON book_metadata (language); -- Multiple authors per book (many-to-many) CREATE TABLE book_authors ( - media_id TEXT NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - author_name TEXT NOT NULL, - author_sort TEXT, -- "Last, First" for sorting - role TEXT NOT NULL DEFAULT 'author', -- author, translator, editor, illustrator - position INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (media_id, author_name, role) + media_id TEXT NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + author_name TEXT NOT NULL, + author_sort TEXT, -- "Last, First" for sorting + role TEXT NOT NULL DEFAULT 'author', -- author, translator, editor, illustrator + position INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (media_id, author_name, role) ) STRICT; -CREATE INDEX idx_book_authors_name ON book_authors(author_name); -CREATE INDEX idx_book_authors_sort ON book_authors(author_sort); +CREATE INDEX idx_book_authors_name ON book_authors (author_name); + +CREATE INDEX idx_book_authors_sort ON book_authors (author_sort); -- Multiple identifiers (ISBN variants, ASIN, DOI, etc.) CREATE TABLE book_identifiers ( - media_id TEXT NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, - identifier_type TEXT NOT NULL, -- isbn, isbn13, asin, doi, lccn, oclc - identifier_value TEXT NOT NULL, - PRIMARY KEY (media_id, identifier_type, identifier_value) + media_id TEXT NOT NULL REFERENCES media_items (id) ON DELETE CASCADE, + identifier_type TEXT NOT NULL, -- isbn, isbn13, asin, doi, lccn, oclc + identifier_value TEXT NOT NULL, + PRIMARY KEY (media_id, identifier_type, identifier_value) ) STRICT; -CREATE INDEX idx_book_identifiers ON book_identifiers(identifier_type, identifier_value); +CREATE INDEX idx_book_identifiers ON book_identifiers (identifier_type, identifier_value); -- Trigger to update updated_at on book_metadata changes CREATE TRIGGER update_book_metadata_timestamp - AFTER UPDATE ON book_metadata - FOR EACH ROW +AFTER +UPDATE ON book_metadata FOR EACH ROW BEGIN - UPDATE book_metadata SET updated_at = datetime('now') WHERE media_id = NEW.media_id; +UPDATE book_metadata +SET + updated_at = datetime ('now') +WHERE + media_id = NEW.media_id; + END; diff --git a/migrations/sqlite/V13__photo_metadata.sql b/migrations/sqlite/V13__photo_metadata.sql index 616b2fa..640374f 100644 --- a/migrations/sqlite/V13__photo_metadata.sql +++ b/migrations/sqlite/V13__photo_metadata.sql @@ -1,15 +1,40 @@ -- V13: Enhanced photo metadata support -- Add photo-specific fields to media_items table +ALTER TABLE media_items +ADD COLUMN date_taken TIMESTAMP; -ALTER TABLE media_items ADD COLUMN date_taken TIMESTAMP; -ALTER TABLE media_items ADD COLUMN latitude REAL; -ALTER TABLE media_items ADD COLUMN longitude REAL; -ALTER TABLE media_items ADD COLUMN camera_make TEXT; -ALTER TABLE media_items ADD COLUMN camera_model TEXT; -ALTER TABLE media_items ADD COLUMN rating INTEGER CHECK (rating >= 0 AND rating <= 5); +ALTER TABLE media_items +ADD COLUMN latitude REAL; + +ALTER TABLE media_items +ADD COLUMN longitude REAL; + +ALTER TABLE media_items +ADD COLUMN camera_make TEXT; + +ALTER TABLE media_items +ADD COLUMN camera_model TEXT; + +ALTER TABLE media_items +ADD COLUMN rating INTEGER CHECK ( + rating >= 0 + AND rating <= 5 +); -- Indexes for photo queries -CREATE INDEX idx_media_date_taken ON media_items(date_taken) WHERE date_taken IS NOT NULL; -CREATE INDEX idx_media_location ON media_items(latitude, longitude) WHERE latitude IS NOT NULL AND longitude IS NOT NULL; -CREATE INDEX idx_media_camera ON media_items(camera_make) WHERE camera_make IS NOT NULL; -CREATE INDEX idx_media_rating ON media_items(rating) WHERE rating IS NOT NULL; +CREATE INDEX idx_media_date_taken ON media_items (date_taken) +WHERE + date_taken IS NOT NULL; + +CREATE INDEX idx_media_location ON media_items (latitude, longitude) +WHERE + latitude IS NOT NULL + AND longitude IS NOT NULL; + +CREATE INDEX idx_media_camera ON media_items (camera_make) +WHERE + camera_make IS NOT NULL; + +CREATE INDEX idx_media_rating ON media_items (rating) +WHERE + rating IS NOT NULL; diff --git a/migrations/sqlite/V14__perceptual_hash.sql b/migrations/sqlite/V14__perceptual_hash.sql index 4bdc677..1d3c634 100644 --- a/migrations/sqlite/V14__perceptual_hash.sql +++ b/migrations/sqlite/V14__perceptual_hash.sql @@ -1,7 +1,9 @@ -- V14: Perceptual hash for duplicate detection -- Add perceptual hash column for image similarity detection - -ALTER TABLE media_items ADD COLUMN perceptual_hash TEXT; +ALTER TABLE media_items +ADD COLUMN perceptual_hash TEXT; -- Index for perceptual hash lookups -CREATE INDEX idx_media_phash ON media_items(perceptual_hash) WHERE perceptual_hash IS NOT NULL; +CREATE INDEX idx_media_phash ON media_items (perceptual_hash) +WHERE + perceptual_hash IS NOT NULL; diff --git a/migrations/sqlite/V15__managed_storage.sql b/migrations/sqlite/V15__managed_storage.sql index b7f2a9d..1f10c7b 100644 --- a/migrations/sqlite/V15__managed_storage.sql +++ b/migrations/sqlite/V15__managed_storage.sql @@ -1,30 +1,33 @@ -- V15: Managed File Storage -- Adds server-side content-addressable storage for uploaded files - -- Add storage mode to media_items (external = file on disk, managed = in content-addressable storage) -ALTER TABLE media_items ADD COLUMN storage_mode TEXT NOT NULL DEFAULT 'external'; +ALTER TABLE media_items +ADD COLUMN storage_mode TEXT NOT NULL DEFAULT 'external'; -- Original filename for managed uploads (preserved separately from file_name which may be normalized) -ALTER TABLE media_items ADD COLUMN original_filename TEXT; +ALTER TABLE media_items +ADD COLUMN original_filename TEXT; -- When the file was uploaded to managed storage -ALTER TABLE media_items ADD COLUMN uploaded_at TEXT; +ALTER TABLE media_items +ADD COLUMN uploaded_at TEXT; -- Storage key for looking up the blob (usually same as content_hash for deduplication) -ALTER TABLE media_items ADD COLUMN storage_key TEXT; +ALTER TABLE media_items +ADD COLUMN storage_key TEXT; -- Managed blobs table - tracks deduplicated file storage CREATE TABLE managed_blobs ( - content_hash TEXT PRIMARY KEY NOT NULL, - file_size INTEGER NOT NULL, - mime_type TEXT NOT NULL, - reference_count INTEGER NOT NULL DEFAULT 1, - stored_at TEXT NOT NULL, - last_verified TEXT + content_hash TEXT PRIMARY KEY NOT NULL, + file_size INTEGER NOT NULL, + mime_type TEXT NOT NULL, + reference_count INTEGER NOT NULL DEFAULT 1, + stored_at TEXT NOT NULL, + last_verified TEXT ); -- Index for finding managed media items -CREATE INDEX idx_media_storage_mode ON media_items(storage_mode); +CREATE INDEX idx_media_storage_mode ON media_items (storage_mode); -- Index for finding orphaned blobs (reference_count = 0) -CREATE INDEX idx_blobs_reference_count ON managed_blobs(reference_count); +CREATE INDEX idx_blobs_reference_count ON managed_blobs (reference_count); diff --git a/migrations/sqlite/V16__sync_system.sql b/migrations/sqlite/V16__sync_system.sql index 8941500..6787034 100644 --- a/migrations/sqlite/V16__sync_system.sql +++ b/migrations/sqlite/V16__sync_system.sql @@ -1,117 +1,129 @@ -- V16: Cross-Device Sync System -- Adds device registration, change tracking, and chunked upload support - -- Sync devices table CREATE TABLE sync_devices ( - id TEXT PRIMARY KEY NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - device_type TEXT NOT NULL, - client_version TEXT NOT NULL, - os_info TEXT, - device_token_hash TEXT NOT NULL UNIQUE, - last_sync_at TEXT, - last_seen_at TEXT NOT NULL, - sync_cursor INTEGER DEFAULT 0, - enabled INTEGER NOT NULL DEFAULT 1, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + device_type TEXT NOT NULL, + client_version TEXT NOT NULL, + os_info TEXT, + device_token_hash TEXT NOT NULL UNIQUE, + last_sync_at TEXT, + last_seen_at TEXT NOT NULL, + sync_cursor INTEGER DEFAULT 0, + enabled INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); -CREATE INDEX idx_sync_devices_user ON sync_devices(user_id); -CREATE INDEX idx_sync_devices_token ON sync_devices(device_token_hash); +CREATE INDEX idx_sync_devices_user ON sync_devices (user_id); + +CREATE INDEX idx_sync_devices_token ON sync_devices (device_token_hash); -- Sync log table - tracks all changes for sync CREATE TABLE sync_log ( - id TEXT PRIMARY KEY NOT NULL, - sequence INTEGER NOT NULL UNIQUE, - change_type TEXT NOT NULL, - media_id TEXT, - path TEXT NOT NULL, - content_hash TEXT, - file_size INTEGER, - metadata_json TEXT, - changed_by_device TEXT, - timestamp TEXT NOT NULL, - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE SET NULL, - FOREIGN KEY (changed_by_device) REFERENCES sync_devices(id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + sequence INTEGER NOT NULL UNIQUE, + change_type TEXT NOT NULL, + media_id TEXT, + path TEXT NOT NULL, + content_hash TEXT, + file_size INTEGER, + metadata_json TEXT, + changed_by_device TEXT, + timestamp TEXT NOT NULL, + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE SET NULL, + FOREIGN KEY (changed_by_device) REFERENCES sync_devices (id) ON DELETE SET NULL ); -CREATE INDEX idx_sync_log_sequence ON sync_log(sequence); -CREATE INDEX idx_sync_log_path ON sync_log(path); -CREATE INDEX idx_sync_log_timestamp ON sync_log(timestamp); +CREATE INDEX idx_sync_log_sequence ON sync_log (sequence); + +CREATE INDEX idx_sync_log_path ON sync_log (path); + +CREATE INDEX idx_sync_log_timestamp ON sync_log (timestamp); -- Sequence counter for sync log CREATE TABLE sync_sequence ( - id INTEGER PRIMARY KEY CHECK (id = 1), - current_value INTEGER NOT NULL DEFAULT 0 + id INTEGER PRIMARY KEY CHECK (id = 1), + current_value INTEGER NOT NULL DEFAULT 0 ); -INSERT INTO sync_sequence (id, current_value) VALUES (1, 0); + +INSERT INTO + sync_sequence (id, current_value) +VALUES + (1, 0); -- Device sync state - tracks sync status per device per file CREATE TABLE device_sync_state ( - device_id TEXT NOT NULL, - path TEXT NOT NULL, - local_hash TEXT, - server_hash TEXT, - local_mtime INTEGER, - server_mtime INTEGER, - sync_status TEXT NOT NULL, - last_synced_at TEXT, - conflict_info_json TEXT, - PRIMARY KEY (device_id, path), - FOREIGN KEY (device_id) REFERENCES sync_devices(id) ON DELETE CASCADE + device_id TEXT NOT NULL, + path TEXT NOT NULL, + local_hash TEXT, + server_hash TEXT, + local_mtime INTEGER, + server_mtime INTEGER, + sync_status TEXT NOT NULL, + last_synced_at TEXT, + conflict_info_json TEXT, + PRIMARY KEY (device_id, path), + FOREIGN KEY (device_id) REFERENCES sync_devices (id) ON DELETE CASCADE ); -CREATE INDEX idx_device_sync_status ON device_sync_state(device_id, sync_status); +CREATE INDEX idx_device_sync_status ON device_sync_state (device_id, sync_status); -- Upload sessions for chunked uploads CREATE TABLE upload_sessions ( - id TEXT PRIMARY KEY NOT NULL, - device_id TEXT NOT NULL, - target_path TEXT NOT NULL, - expected_hash TEXT NOT NULL, - expected_size INTEGER NOT NULL, - chunk_size INTEGER NOT NULL, - chunk_count INTEGER NOT NULL, - status TEXT NOT NULL, - created_at TEXT NOT NULL, - expires_at TEXT NOT NULL, - last_activity TEXT NOT NULL, - FOREIGN KEY (device_id) REFERENCES sync_devices(id) ON DELETE CASCADE + id TEXT PRIMARY KEY NOT NULL, + device_id TEXT NOT NULL, + target_path TEXT NOT NULL, + expected_hash TEXT NOT NULL, + expected_size INTEGER NOT NULL, + chunk_size INTEGER NOT NULL, + chunk_count INTEGER NOT NULL, + status TEXT NOT NULL, + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL, + last_activity TEXT NOT NULL, + FOREIGN KEY (device_id) REFERENCES sync_devices (id) ON DELETE CASCADE ); -CREATE INDEX idx_upload_sessions_device ON upload_sessions(device_id); -CREATE INDEX idx_upload_sessions_status ON upload_sessions(status); -CREATE INDEX idx_upload_sessions_expires ON upload_sessions(expires_at); +CREATE INDEX idx_upload_sessions_device ON upload_sessions (device_id); + +CREATE INDEX idx_upload_sessions_status ON upload_sessions (status); + +CREATE INDEX idx_upload_sessions_expires ON upload_sessions (expires_at); -- Upload chunks - tracks received chunks CREATE TABLE upload_chunks ( - upload_id TEXT NOT NULL, - chunk_index INTEGER NOT NULL, - offset INTEGER NOT NULL, + upload_id TEXT NOT NULL, + chunk_index INTEGER NOT NULL, + offset + INTEGER NOT NULL, size INTEGER NOT NULL, hash TEXT NOT NULL, received_at TEXT NOT NULL, PRIMARY KEY (upload_id, chunk_index), - FOREIGN KEY (upload_id) REFERENCES upload_sessions(id) ON DELETE CASCADE + FOREIGN KEY (upload_id) REFERENCES upload_sessions (id) ON DELETE CASCADE ); -- Sync conflicts CREATE TABLE sync_conflicts ( - id TEXT PRIMARY KEY NOT NULL, - device_id TEXT NOT NULL, - path TEXT NOT NULL, - local_hash TEXT NOT NULL, - local_mtime INTEGER NOT NULL, - server_hash TEXT NOT NULL, - server_mtime INTEGER NOT NULL, - detected_at TEXT NOT NULL, - resolved_at TEXT, - resolution TEXT, - FOREIGN KEY (device_id) REFERENCES sync_devices(id) ON DELETE CASCADE + id TEXT PRIMARY KEY NOT NULL, + device_id TEXT NOT NULL, + path TEXT NOT NULL, + local_hash TEXT NOT NULL, + local_mtime INTEGER NOT NULL, + server_hash TEXT NOT NULL, + server_mtime INTEGER NOT NULL, + detected_at TEXT NOT NULL, + resolved_at TEXT, + resolution TEXT, + FOREIGN KEY (device_id) REFERENCES sync_devices (id) ON DELETE CASCADE ); -CREATE INDEX idx_sync_conflicts_device ON sync_conflicts(device_id); -CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts(device_id, resolved_at) WHERE resolved_at IS NULL; +CREATE INDEX idx_sync_conflicts_device ON sync_conflicts (device_id); + +CREATE INDEX idx_sync_conflicts_unresolved ON sync_conflicts (device_id, resolved_at) +WHERE + resolved_at IS NULL; diff --git a/migrations/sqlite/V17__enhanced_sharing.sql b/migrations/sqlite/V17__enhanced_sharing.sql index 1cd17f3..ac7d93e 100644 --- a/migrations/sqlite/V17__enhanced_sharing.sql +++ b/migrations/sqlite/V17__enhanced_sharing.sql @@ -1,85 +1,133 @@ -- V17: Enhanced Sharing System -- Replaces simple share_links with comprehensive sharing capabilities - -- Enhanced shares table CREATE TABLE shares ( - id TEXT PRIMARY KEY NOT NULL, - target_type TEXT NOT NULL CHECK (target_type IN ('media', 'collection', 'tag', 'saved_search')), - target_id TEXT NOT NULL, - owner_id TEXT NOT NULL, - recipient_type TEXT NOT NULL CHECK (recipient_type IN ('public_link', 'user', 'group', 'federated')), - recipient_user_id TEXT, - recipient_group_id TEXT, - recipient_federated_handle TEXT, - recipient_federated_server TEXT, - public_token TEXT UNIQUE, - public_password_hash TEXT, - perm_view INTEGER NOT NULL DEFAULT 1, - perm_download INTEGER NOT NULL DEFAULT 0, - perm_edit INTEGER NOT NULL DEFAULT 0, - perm_delete INTEGER NOT NULL DEFAULT 0, - perm_reshare INTEGER NOT NULL DEFAULT 0, - perm_add INTEGER NOT NULL DEFAULT 0, - note TEXT, - expires_at TEXT, - access_count INTEGER NOT NULL DEFAULT 0, - last_accessed TEXT, - inherit_to_children INTEGER NOT NULL DEFAULT 1, - parent_share_id TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (recipient_user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (parent_share_id) REFERENCES shares(id) ON DELETE CASCADE, - UNIQUE(owner_id, target_type, target_id, recipient_type, recipient_user_id) + id TEXT PRIMARY KEY NOT NULL, + target_type TEXT NOT NULL CHECK ( + target_type IN ('media', 'collection', 'tag', 'saved_search') + ), + target_id TEXT NOT NULL, + owner_id TEXT NOT NULL, + recipient_type TEXT NOT NULL CHECK ( + recipient_type IN ('public_link', 'user', 'group', 'federated') + ), + recipient_user_id TEXT, + recipient_group_id TEXT, + recipient_federated_handle TEXT, + recipient_federated_server TEXT, + public_token TEXT UNIQUE, + public_password_hash TEXT, + perm_view INTEGER NOT NULL DEFAULT 1, + perm_download INTEGER NOT NULL DEFAULT 0, + perm_edit INTEGER NOT NULL DEFAULT 0, + perm_delete INTEGER NOT NULL DEFAULT 0, + perm_reshare INTEGER NOT NULL DEFAULT 0, + perm_add INTEGER NOT NULL DEFAULT 0, + note TEXT, + expires_at TEXT, + access_count INTEGER NOT NULL DEFAULT 0, + last_accessed TEXT, + inherit_to_children INTEGER NOT NULL DEFAULT 1, + parent_share_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (owner_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (recipient_user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (parent_share_id) REFERENCES shares (id) ON DELETE CASCADE, + UNIQUE ( + owner_id, + target_type, + target_id, + recipient_type, + recipient_user_id + ) ); -CREATE INDEX idx_shares_owner ON shares(owner_id); -CREATE INDEX idx_shares_recipient_user ON shares(recipient_user_id); -CREATE INDEX idx_shares_target ON shares(target_type, target_id); -CREATE INDEX idx_shares_token ON shares(public_token); -CREATE INDEX idx_shares_expires ON shares(expires_at); +CREATE INDEX idx_shares_owner ON shares (owner_id); + +CREATE INDEX idx_shares_recipient_user ON shares (recipient_user_id); + +CREATE INDEX idx_shares_target ON shares (target_type, target_id); + +CREATE INDEX idx_shares_token ON shares (public_token); + +CREATE INDEX idx_shares_expires ON shares (expires_at); -- Share activity log CREATE TABLE share_activity ( - id TEXT PRIMARY KEY NOT NULL, - share_id TEXT NOT NULL, - actor_id TEXT, - actor_ip TEXT, - action TEXT NOT NULL, - details TEXT, - timestamp TEXT NOT NULL, - FOREIGN KEY (share_id) REFERENCES shares(id) ON DELETE CASCADE, - FOREIGN KEY (actor_id) REFERENCES users(id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + share_id TEXT NOT NULL, + actor_id TEXT, + actor_ip TEXT, + action TEXT NOT NULL, + details TEXT, + timestamp TEXT NOT NULL, + FOREIGN KEY (share_id) REFERENCES shares (id) ON DELETE CASCADE, + FOREIGN KEY (actor_id) REFERENCES users (id) ON DELETE SET NULL ); -CREATE INDEX idx_share_activity_share ON share_activity(share_id); -CREATE INDEX idx_share_activity_timestamp ON share_activity(timestamp); +CREATE INDEX idx_share_activity_share ON share_activity (share_id); + +CREATE INDEX idx_share_activity_timestamp ON share_activity (timestamp); -- Share notifications CREATE TABLE share_notifications ( - id TEXT PRIMARY KEY NOT NULL, - user_id TEXT NOT NULL, - share_id TEXT NOT NULL, - notification_type TEXT NOT NULL, - is_read INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (share_id) REFERENCES shares(id) ON DELETE CASCADE + id TEXT PRIMARY KEY NOT NULL, + user_id TEXT NOT NULL, + share_id TEXT NOT NULL, + notification_type TEXT NOT NULL, + is_read INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (share_id) REFERENCES shares (id) ON DELETE CASCADE ); -CREATE INDEX idx_share_notifications_user ON share_notifications(user_id); -CREATE INDEX idx_share_notifications_unread ON share_notifications(user_id, is_read) WHERE is_read = 0; +CREATE INDEX idx_share_notifications_user ON share_notifications (user_id); + +CREATE INDEX idx_share_notifications_unread ON share_notifications (user_id, is_read) +WHERE + is_read = 0; -- Migrate existing share_links to new shares table (if share_links exists) -INSERT OR IGNORE INTO shares ( - id, target_type, target_id, owner_id, recipient_type, - public_token, public_password_hash, perm_view, perm_download, - access_count, expires_at, created_at, updated_at +INSERT +OR IGNORE INTO shares ( + id, + target_type, + target_id, + owner_id, + recipient_type, + public_token, + public_password_hash, + perm_view, + perm_download, + access_count, + expires_at, + created_at, + updated_at ) SELECT - id, 'media', media_id, created_by, 'public_link', - token, password_hash, 1, 1, - view_count, expires_at, created_at, created_at -FROM share_links -WHERE EXISTS (SELECT 1 FROM sqlite_master WHERE type='table' AND name='share_links'); + id, + 'media', + media_id, + created_by, + 'public_link', + token, + password_hash, + 1, + 1, + view_count, + expires_at, + created_at, + created_at +FROM + share_links +WHERE + EXISTS ( + SELECT + 1 + FROM + sqlite_master + WHERE + type = 'table' + AND name = 'share_links' + ); diff --git a/migrations/sqlite/V18__file_management.sql b/migrations/sqlite/V18__file_management.sql index 0af0738..08599dd 100644 --- a/migrations/sqlite/V18__file_management.sql +++ b/migrations/sqlite/V18__file_management.sql @@ -1,11 +1,13 @@ -- V18: File Management (Rename, Move, Trash) -- Adds soft delete support for trash/recycle bin functionality - -- Add deleted_at column for soft delete (trash) -ALTER TABLE media_items ADD COLUMN deleted_at TEXT; +ALTER TABLE media_items +ADD COLUMN deleted_at TEXT; -- Index for efficient trash queries -CREATE INDEX idx_media_deleted_at ON media_items(deleted_at); +CREATE INDEX idx_media_deleted_at ON media_items (deleted_at); -- Index for listing non-deleted items (most common query pattern) -CREATE INDEX idx_media_not_deleted ON media_items(id) WHERE deleted_at IS NULL; +CREATE INDEX idx_media_not_deleted ON media_items (id) +WHERE + deleted_at IS NULL; diff --git a/migrations/sqlite/V19__markdown_links.sql b/migrations/sqlite/V19__markdown_links.sql index 7cbdda3..214e40a 100644 --- a/migrations/sqlite/V19__markdown_links.sql +++ b/migrations/sqlite/V19__markdown_links.sql @@ -1,35 +1,35 @@ -- V19: Markdown Links (Obsidian-style bidirectional links) -- Adds support for wikilinks, markdown links, embeds, and backlink tracking - -- Table for storing extracted markdown links CREATE TABLE IF NOT EXISTS markdown_links ( - id TEXT PRIMARY KEY NOT NULL, - source_media_id TEXT NOT NULL, - target_path TEXT NOT NULL, -- raw link target (wikilink or path) - target_media_id TEXT, -- resolved media_id (nullable if unresolved) - link_type TEXT NOT NULL, -- 'wikilink', 'markdown_link', 'embed' - link_text TEXT, -- display text for the link - line_number INTEGER, -- line number in source file - context TEXT, -- surrounding text for preview - created_at TEXT NOT NULL, - FOREIGN KEY (source_media_id) REFERENCES media_items(id) ON DELETE CASCADE, - FOREIGN KEY (target_media_id) REFERENCES media_items(id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + source_media_id TEXT NOT NULL, + target_path TEXT NOT NULL, -- raw link target (wikilink or path) + target_media_id TEXT, -- resolved media_id (nullable if unresolved) + link_type TEXT NOT NULL, -- 'wikilink', 'markdown_link', 'embed' + link_text TEXT, -- display text for the link + line_number INTEGER, -- line number in source file + context TEXT, -- surrounding text for preview + created_at TEXT NOT NULL, + FOREIGN KEY (source_media_id) REFERENCES media_items (id) ON DELETE CASCADE, + FOREIGN KEY (target_media_id) REFERENCES media_items (id) ON DELETE SET NULL ); -- Index for efficient outgoing link queries (what does this note link to?) -CREATE INDEX idx_links_source ON markdown_links(source_media_id); +CREATE INDEX idx_links_source ON markdown_links (source_media_id); -- Index for efficient backlink queries (what links to this note?) -CREATE INDEX idx_links_target ON markdown_links(target_media_id); +CREATE INDEX idx_links_target ON markdown_links (target_media_id); -- Index for path-based resolution (finding unresolved links) -CREATE INDEX idx_links_target_path ON markdown_links(target_path); +CREATE INDEX idx_links_target_path ON markdown_links (target_path); -- Index for link type filtering -CREATE INDEX idx_links_type ON markdown_links(link_type); +CREATE INDEX idx_links_type ON markdown_links (link_type); -- Track when links were last extracted from a media item -ALTER TABLE media_items ADD COLUMN links_extracted_at TEXT; +ALTER TABLE media_items +ADD COLUMN links_extracted_at TEXT; -- Index for finding media items that need link extraction -CREATE INDEX idx_media_links_extracted ON media_items(links_extracted_at); +CREATE INDEX idx_media_links_extracted ON media_items (links_extracted_at); diff --git a/migrations/sqlite/V1__initial_schema.sql b/migrations/sqlite/V1__initial_schema.sql index 5b16abf..b6ccd0e 100644 --- a/migrations/sqlite/V1__initial_schema.sql +++ b/migrations/sqlite/V1__initial_schema.sql @@ -1,77 +1,75 @@ -CREATE TABLE IF NOT EXISTS root_dirs ( - path TEXT PRIMARY KEY NOT NULL -); +CREATE TABLE IF NOT EXISTS root_dirs (path TEXT PRIMARY KEY NOT NULL); CREATE TABLE IF NOT EXISTS media_items ( - id TEXT PRIMARY KEY NOT NULL, - path TEXT NOT NULL UNIQUE, - file_name TEXT NOT NULL, - media_type TEXT NOT NULL, - content_hash TEXT NOT NULL UNIQUE, - file_size INTEGER NOT NULL, - title TEXT, - artist TEXT, - album TEXT, - genre TEXT, - year INTEGER, - duration_secs REAL, - description TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL + id TEXT PRIMARY KEY NOT NULL, + path TEXT NOT NULL UNIQUE, + file_name TEXT NOT NULL, + media_type TEXT NOT NULL, + content_hash TEXT NOT NULL UNIQUE, + file_size INTEGER NOT NULL, + title TEXT, + artist TEXT, + album TEXT, + genre TEXT, + year INTEGER, + duration_secs REAL, + description TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS tags ( - id TEXT PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - parent_id TEXT, - created_at TEXT NOT NULL, - FOREIGN KEY (parent_id) REFERENCES tags(id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + parent_id TEXT, + created_at TEXT NOT NULL, + FOREIGN KEY (parent_id) REFERENCES tags (id) ON DELETE SET NULL ); -CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name_parent ON tags(name, parent_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_name_parent ON tags (name, parent_id); CREATE TABLE IF NOT EXISTS media_tags ( - media_id TEXT NOT NULL, - tag_id TEXT NOT NULL, - PRIMARY KEY (media_id, tag_id), - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE, - FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE + media_id TEXT NOT NULL, + tag_id TEXT NOT NULL, + PRIMARY KEY (media_id, tag_id), + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS collections ( - id TEXT PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - description TEXT, - kind TEXT NOT NULL, - filter_query TEXT, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT, + kind TEXT NOT NULL, + filter_query TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS collection_members ( - collection_id TEXT NOT NULL, - media_id TEXT NOT NULL, - position INTEGER NOT NULL DEFAULT 0, - added_at TEXT NOT NULL, - PRIMARY KEY (collection_id, media_id), - FOREIGN KEY (collection_id) REFERENCES collections(id) ON DELETE CASCADE, - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE + collection_id TEXT NOT NULL, + media_id TEXT NOT NULL, + position INTEGER NOT NULL DEFAULT 0, + added_at TEXT NOT NULL, + PRIMARY KEY (collection_id, media_id), + FOREIGN KEY (collection_id) REFERENCES collections (id) ON DELETE CASCADE, + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS audit_log ( - id TEXT PRIMARY KEY NOT NULL, - media_id TEXT, - action TEXT NOT NULL, - details TEXT, - timestamp TEXT NOT NULL, - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE SET NULL + id TEXT PRIMARY KEY NOT NULL, + media_id TEXT, + action TEXT NOT NULL, + details TEXT, + timestamp TEXT NOT NULL, + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS custom_fields ( - media_id TEXT NOT NULL, - field_name TEXT NOT NULL, - field_type TEXT NOT NULL, - field_value TEXT NOT NULL, - PRIMARY KEY (media_id, field_name), - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE + media_id TEXT NOT NULL, + field_name TEXT NOT NULL, + field_type TEXT NOT NULL, + field_value TEXT NOT NULL, + PRIMARY KEY (media_id, field_name), + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE ); diff --git a/migrations/sqlite/V2__fts5_indexes.sql b/migrations/sqlite/V2__fts5_indexes.sql index 00c5597..01270a0 100644 --- a/migrations/sqlite/V2__fts5_indexes.sql +++ b/migrations/sqlite/V2__fts5_indexes.sql @@ -1,27 +1,114 @@ -CREATE VIRTUAL TABLE IF NOT EXISTS media_fts USING fts5( +CREATE VIRTUAL TABLE IF NOT EXISTS media_fts USING fts5 ( + title, + artist, + album, + genre, + description, + file_name, + content = 'media_items', + content_rowid = 'rowid' +); + +CREATE TRIGGER IF NOT EXISTS media_fts_insert +AFTER INSERT ON media_items +BEGIN +INSERT INTO + media_fts ( + rowid, title, artist, album, genre, description, - file_name, - content='media_items', - content_rowid='rowid' -); + file_name + ) +VALUES + ( + new.rowid, + new.title, + new.artist, + new.album, + new.genre, + new.description, + new.file_name + ); -CREATE TRIGGER IF NOT EXISTS media_fts_insert AFTER INSERT ON media_items BEGIN - INSERT INTO media_fts(rowid, title, artist, album, genre, description, file_name) - VALUES (new.rowid, new.title, new.artist, new.album, new.genre, new.description, new.file_name); END; -CREATE TRIGGER IF NOT EXISTS media_fts_update AFTER UPDATE ON media_items BEGIN - INSERT INTO media_fts(media_fts, rowid, title, artist, album, genre, description, file_name) - VALUES ('delete', old.rowid, old.title, old.artist, old.album, old.genre, old.description, old.file_name); - INSERT INTO media_fts(rowid, title, artist, album, genre, description, file_name) - VALUES (new.rowid, new.title, new.artist, new.album, new.genre, new.description, new.file_name); +CREATE TRIGGER IF NOT EXISTS media_fts_update +AFTER +UPDATE ON media_items +BEGIN +INSERT INTO + media_fts ( + media_fts, + rowid, + title, + artist, + album, + genre, + description, + file_name + ) +VALUES + ( + 'delete', + old.rowid, + old.title, + old.artist, + old.album, + old.genre, + old.description, + old.file_name + ); + +INSERT INTO + media_fts ( + rowid, + title, + artist, + album, + genre, + description, + file_name + ) +VALUES + ( + new.rowid, + new.title, + new.artist, + new.album, + new.genre, + new.description, + new.file_name + ); + END; -CREATE TRIGGER IF NOT EXISTS media_fts_delete AFTER DELETE ON media_items BEGIN - INSERT INTO media_fts(media_fts, rowid, title, artist, album, genre, description, file_name) - VALUES ('delete', old.rowid, old.title, old.artist, old.album, old.genre, old.description, old.file_name); +CREATE TRIGGER IF NOT EXISTS media_fts_delete +AFTER DELETE ON media_items +BEGIN +INSERT INTO + media_fts ( + media_fts, + rowid, + title, + artist, + album, + genre, + description, + file_name + ) +VALUES + ( + 'delete', + old.rowid, + old.title, + old.artist, + old.album, + old.genre, + old.description, + old.file_name + ); + END; diff --git a/migrations/sqlite/V3__audit_indexes.sql b/migrations/sqlite/V3__audit_indexes.sql index 1c741fe..5372307 100644 --- a/migrations/sqlite/V3__audit_indexes.sql +++ b/migrations/sqlite/V3__audit_indexes.sql @@ -1,6 +1,11 @@ -CREATE INDEX IF NOT EXISTS idx_audit_media_id ON audit_log(media_id); -CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp); -CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log(action); -CREATE INDEX IF NOT EXISTS idx_media_content_hash ON media_items(content_hash); -CREATE INDEX IF NOT EXISTS idx_media_media_type ON media_items(media_type); -CREATE INDEX IF NOT EXISTS idx_media_created_at ON media_items(created_at); +CREATE INDEX IF NOT EXISTS idx_audit_media_id ON audit_log (media_id); + +CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log (timestamp); + +CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action); + +CREATE INDEX IF NOT EXISTS idx_media_content_hash ON media_items (content_hash); + +CREATE INDEX IF NOT EXISTS idx_media_media_type ON media_items (media_type); + +CREATE INDEX IF NOT EXISTS idx_media_created_at ON media_items (created_at); diff --git a/migrations/sqlite/V4__thumbnail_path.sql b/migrations/sqlite/V4__thumbnail_path.sql index 9021884..4c23b5b 100644 --- a/migrations/sqlite/V4__thumbnail_path.sql +++ b/migrations/sqlite/V4__thumbnail_path.sql @@ -1 +1,2 @@ -ALTER TABLE media_items ADD COLUMN thumbnail_path TEXT; +ALTER TABLE media_items +ADD COLUMN thumbnail_path TEXT; diff --git a/migrations/sqlite/V5__integrity_and_saved_searches.sql b/migrations/sqlite/V5__integrity_and_saved_searches.sql index 650da16..a8b05ea 100644 --- a/migrations/sqlite/V5__integrity_and_saved_searches.sql +++ b/migrations/sqlite/V5__integrity_and_saved_searches.sql @@ -1,12 +1,15 @@ -- Integrity tracking columns -ALTER TABLE media_items ADD COLUMN last_verified_at TEXT; -ALTER TABLE media_items ADD COLUMN integrity_status TEXT DEFAULT 'unverified'; +ALTER TABLE media_items +ADD COLUMN last_verified_at TEXT; + +ALTER TABLE media_items +ADD COLUMN integrity_status TEXT DEFAULT 'unverified'; -- Saved searches CREATE TABLE IF NOT EXISTS saved_searches ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - query TEXT NOT NULL, - sort_order TEXT, - created_at TEXT NOT NULL + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + query TEXT NOT NULL, + sort_order TEXT, + created_at TEXT NOT NULL ); diff --git a/migrations/sqlite/V6__plugin_system.sql b/migrations/sqlite/V6__plugin_system.sql index f4e7790..c675177 100644 --- a/migrations/sqlite/V6__plugin_system.sql +++ b/migrations/sqlite/V6__plugin_system.sql @@ -1,15 +1,16 @@ -- Plugin registry table CREATE TABLE plugin_registry ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - version TEXT NOT NULL, - enabled BOOLEAN NOT NULL DEFAULT TRUE, - config_json TEXT, - manifest_json TEXT, - installed_at TEXT NOT NULL, - updated_at TEXT NOT NULL + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + version TEXT NOT NULL, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + config_json TEXT, + manifest_json TEXT, + installed_at TEXT NOT NULL, + updated_at TEXT NOT NULL ); -- Index for quick lookups -CREATE INDEX idx_plugin_registry_enabled ON plugin_registry(enabled); -CREATE INDEX idx_plugin_registry_name ON plugin_registry(name); +CREATE INDEX idx_plugin_registry_enabled ON plugin_registry (enabled); + +CREATE INDEX idx_plugin_registry_name ON plugin_registry (name); diff --git a/migrations/sqlite/V7__user_management.sql b/migrations/sqlite/V7__user_management.sql index 6584f03..8042e10 100644 --- a/migrations/sqlite/V7__user_management.sql +++ b/migrations/sqlite/V7__user_management.sql @@ -1,35 +1,37 @@ -- Users table CREATE TABLE users ( - id TEXT PRIMARY KEY, - username TEXT UNIQUE NOT NULL, - password_hash TEXT NOT NULL, - role TEXT NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL + id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL ); -- User profiles table CREATE TABLE user_profiles ( - user_id TEXT PRIMARY KEY, - avatar_path TEXT, - bio TEXT, - preferences_json TEXT NOT NULL DEFAULT '{}', - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY (user_id) REFERENCES users(id) + user_id TEXT PRIMARY KEY, + avatar_path TEXT, + bio TEXT, + preferences_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) ); -- User library access table CREATE TABLE user_libraries ( - user_id TEXT NOT NULL, - root_path TEXT NOT NULL, - permission TEXT NOT NULL, - granted_at TEXT NOT NULL, - PRIMARY KEY (user_id, root_path), - FOREIGN KEY (user_id) REFERENCES users(id) + user_id TEXT NOT NULL, + root_path TEXT NOT NULL, + permission TEXT NOT NULL, + granted_at TEXT NOT NULL, + PRIMARY KEY (user_id, root_path), + FOREIGN KEY (user_id) REFERENCES users (id) ); -- Indexes for efficient lookups -CREATE INDEX idx_users_username ON users(username); -CREATE INDEX idx_user_libraries_user_id ON user_libraries(user_id); -CREATE INDEX idx_user_libraries_root_path ON user_libraries(root_path); +CREATE INDEX idx_users_username ON users (username); + +CREATE INDEX idx_user_libraries_user_id ON user_libraries (user_id); + +CREATE INDEX idx_user_libraries_root_path ON user_libraries (root_path); diff --git a/migrations/sqlite/V8__media_server_features.sql b/migrations/sqlite/V8__media_server_features.sql index 50040c3..ee3dc04 100644 --- a/migrations/sqlite/V8__media_server_features.sql +++ b/migrations/sqlite/V8__media_server_features.sql @@ -1,143 +1,148 @@ -- Ratings CREATE TABLE IF NOT EXISTS ratings ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - media_id TEXT NOT NULL, - stars INTEGER NOT NULL CHECK (stars >= 1 AND stars <= 5), - review_text TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE(user_id, media_id), - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + stars INTEGER NOT NULL CHECK ( + stars >= 1 + AND stars <= 5 + ), + review_text TEXT, + created_at TEXT NOT NULL DEFAULT (datetime ('now')), + UNIQUE (user_id, media_id), + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE ); -- Comments CREATE TABLE IF NOT EXISTS comments ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - media_id TEXT NOT NULL, - parent_comment_id TEXT, - text TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE, - FOREIGN KEY (parent_comment_id) REFERENCES comments(id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + parent_comment_id TEXT, + text TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime ('now')), + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE, + FOREIGN KEY (parent_comment_id) REFERENCES comments (id) ON DELETE CASCADE ); -- Favorites CREATE TABLE IF NOT EXISTS favorites ( - user_id TEXT NOT NULL, - media_id TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - PRIMARY KEY (user_id, media_id), - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime ('now')), + PRIMARY KEY (user_id, media_id), + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE ); -- Share links CREATE TABLE IF NOT EXISTS share_links ( - id TEXT PRIMARY KEY, - media_id TEXT NOT NULL, - created_by TEXT NOT NULL, - token TEXT NOT NULL UNIQUE, - password_hash TEXT, - expires_at TEXT, - view_count INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + created_by TEXT NOT NULL, + token TEXT NOT NULL UNIQUE, + password_hash TEXT, + expires_at TEXT, + view_count INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime ('now')), + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE ); -- Playlists CREATE TABLE IF NOT EXISTS playlists ( - id TEXT PRIMARY KEY, - owner_id TEXT NOT NULL, - name TEXT NOT NULL, - description TEXT, - is_public INTEGER NOT NULL DEFAULT 0, - is_smart INTEGER NOT NULL DEFAULT 0, - filter_query TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) + id TEXT PRIMARY KEY, + owner_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + is_public INTEGER NOT NULL DEFAULT 0, + is_smart INTEGER NOT NULL DEFAULT 0, + filter_query TEXT, + created_at TEXT NOT NULL DEFAULT (datetime ('now')), + updated_at TEXT NOT NULL DEFAULT (datetime ('now')) ); -- Playlist items CREATE TABLE IF NOT EXISTS playlist_items ( - playlist_id TEXT NOT NULL, - media_id TEXT NOT NULL, - position INTEGER NOT NULL DEFAULT 0, - added_at TEXT NOT NULL DEFAULT (datetime('now')), - PRIMARY KEY (playlist_id, media_id), - FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE + playlist_id TEXT NOT NULL, + media_id TEXT NOT NULL, + position INTEGER NOT NULL DEFAULT 0, + added_at TEXT NOT NULL DEFAULT (datetime ('now')), + PRIMARY KEY (playlist_id, media_id), + FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE, + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE ); -- Usage events CREATE TABLE IF NOT EXISTS usage_events ( - id TEXT PRIMARY KEY, - media_id TEXT, - user_id TEXT, - event_type TEXT NOT NULL, - timestamp TEXT NOT NULL DEFAULT (datetime('now')), - duration_secs REAL, - context_json TEXT, - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE SET NULL + id TEXT PRIMARY KEY, + media_id TEXT, + user_id TEXT, + event_type TEXT NOT NULL, + timestamp TEXT NOT NULL DEFAULT (datetime ('now')), + duration_secs REAL, + context_json TEXT, + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE SET NULL ); -CREATE INDEX IF NOT EXISTS idx_usage_events_media ON usage_events(media_id); -CREATE INDEX IF NOT EXISTS idx_usage_events_user ON usage_events(user_id); -CREATE INDEX IF NOT EXISTS idx_usage_events_timestamp ON usage_events(timestamp); +CREATE INDEX IF NOT EXISTS idx_usage_events_media ON usage_events (media_id); + +CREATE INDEX IF NOT EXISTS idx_usage_events_user ON usage_events (user_id); + +CREATE INDEX IF NOT EXISTS idx_usage_events_timestamp ON usage_events (timestamp); -- Watch history / progress CREATE TABLE IF NOT EXISTS watch_history ( - id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - media_id TEXT NOT NULL, - progress_secs REAL NOT NULL DEFAULT 0, - last_watched TEXT NOT NULL DEFAULT (datetime('now')), - UNIQUE(user_id, media_id), - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + media_id TEXT NOT NULL, + progress_secs REAL NOT NULL DEFAULT 0, + last_watched TEXT NOT NULL DEFAULT (datetime ('now')), + UNIQUE (user_id, media_id), + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE ); -- Subtitles CREATE TABLE IF NOT EXISTS subtitles ( - id TEXT PRIMARY KEY, - media_id TEXT NOT NULL, - language TEXT, - format TEXT NOT NULL, - file_path TEXT, - is_embedded INTEGER NOT NULL DEFAULT 0, - track_index INTEGER, - offset_ms INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + language TEXT, + format TEXT NOT NULL, + file_path TEXT, + is_embedded INTEGER NOT NULL DEFAULT 0, + track_index INTEGER, + offset_ms INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime ('now')), + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS idx_subtitles_media ON subtitles(media_id); +CREATE INDEX IF NOT EXISTS idx_subtitles_media ON subtitles (media_id); -- External metadata (enrichment) CREATE TABLE IF NOT EXISTS external_metadata ( - id TEXT PRIMARY KEY, - media_id TEXT NOT NULL, - source TEXT NOT NULL, - external_id TEXT, - metadata_json TEXT NOT NULL DEFAULT '{}', - confidence REAL NOT NULL DEFAULT 0.0, - last_updated TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + source TEXT NOT NULL, + external_id TEXT, + metadata_json TEXT NOT NULL DEFAULT '{}', + confidence REAL NOT NULL DEFAULT 0.0, + last_updated TEXT NOT NULL DEFAULT (datetime ('now')), + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS idx_external_metadata_media ON external_metadata(media_id); +CREATE INDEX IF NOT EXISTS idx_external_metadata_media ON external_metadata (media_id); -- Transcode sessions CREATE TABLE IF NOT EXISTS transcode_sessions ( - id TEXT PRIMARY KEY, - media_id TEXT NOT NULL, - user_id TEXT, - profile TEXT NOT NULL, - cache_path TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - progress REAL NOT NULL DEFAULT 0.0, - error_message TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - expires_at TEXT, - FOREIGN KEY (media_id) REFERENCES media_items(id) ON DELETE CASCADE + id TEXT PRIMARY KEY, + media_id TEXT NOT NULL, + user_id TEXT, + profile TEXT NOT NULL, + cache_path TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + progress REAL NOT NULL DEFAULT 0.0, + error_message TEXT, + created_at TEXT NOT NULL DEFAULT (datetime ('now')), + expires_at TEXT, + FOREIGN KEY (media_id) REFERENCES media_items (id) ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions(media_id); +CREATE INDEX IF NOT EXISTS idx_transcode_sessions_media ON transcode_sessions (media_id); diff --git a/migrations/sqlite/V9__fix_indexes_and_constraints.sql b/migrations/sqlite/V9__fix_indexes_and_constraints.sql index 432f35a..dd1bfa0 100644 --- a/migrations/sqlite/V9__fix_indexes_and_constraints.sql +++ b/migrations/sqlite/V9__fix_indexes_and_constraints.sql @@ -1,18 +1,25 @@ -- Drop redundant indexes (already covered by UNIQUE constraints) DROP INDEX IF EXISTS idx_users_username; + DROP INDEX IF EXISTS idx_user_libraries_user_id; -- Add missing indexes for comments table -CREATE INDEX IF NOT EXISTS idx_comments_media ON comments(media_id); -CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments(parent_comment_id); +CREATE INDEX IF NOT EXISTS idx_comments_media ON comments (media_id); + +CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments (parent_comment_id); -- Remove duplicates before adding unique index (keep the first row) DELETE FROM external_metadata -WHERE rowid NOT IN ( - SELECT MIN(rowid) - FROM external_metadata - GROUP BY media_id, source -); +WHERE + rowid NOT IN ( + SELECT + MIN(rowid) + FROM + external_metadata + GROUP BY + media_id, + source + ); -- Add unique index for external_metadata to prevent duplicates -CREATE UNIQUE INDEX IF NOT EXISTS uq_external_metadata_source ON external_metadata(media_id, source); +CREATE UNIQUE INDEX IF NOT EXISTS uq_external_metadata_source ON external_metadata (media_id, source); diff --git a/nix/shell.nix b/nix/shell.nix index 6e706fc..4eab514 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -68,9 +68,6 @@ in echo "sccache setup complete!" fi - - # Start the daemon early for slightly faster startup. - "$SCCACHE_BIN" --start-server >/dev/null 2>&1 || true ''; env = {