secubox-openwrt/package/secubox/luci-app-photoprism/htdocs/luci-static/resources/view/photoprism/overview.js
CyberMind-FR 26519e7619 feat(photoprism): Add private photo gallery with AI features
New packages:
- secubox-app-photoprism: LXC-based PhotoPrism deployment
  - Debian Bookworm container with MariaDB, FFmpeg
  - AI face recognition, object detection, places/maps
  - photoprismctl CLI: install/start/stop/index/import/emancipate
  - HAProxy integration via mitmproxy (WAF-safe)

- luci-app-photoprism: KISS-themed dashboard
  - Stats cards (photos, videos, storage)
  - Service controls and AI feature display
  - Emancipate form for public exposure
  - RPCD backend with 12 methods

docs: Update WIP.md with PhotoPrism feature

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-06 10:00:49 +01:00

449 lines
11 KiB
JavaScript

'use strict';
'require view';
'require rpc';
'require ui';
'require poll';
var callStatus = rpc.declare({
object: 'luci.photoprism',
method: 'status',
expect: {}
});
var callGetStats = rpc.declare({
object: 'luci.photoprism',
method: 'get_stats',
expect: {}
});
var callStart = rpc.declare({
object: 'luci.photoprism',
method: 'start',
expect: {}
});
var callStop = rpc.declare({
object: 'luci.photoprism',
method: 'stop',
expect: {}
});
var callInstall = rpc.declare({
object: 'luci.photoprism',
method: 'install',
expect: {}
});
var callUninstall = rpc.declare({
object: 'luci.photoprism',
method: 'uninstall',
expect: {}
});
var callIndex = rpc.declare({
object: 'luci.photoprism',
method: 'index',
expect: {}
});
var callImport = rpc.declare({
object: 'luci.photoprism',
method: 'import',
expect: {}
});
var callEmancipate = rpc.declare({
object: 'luci.photoprism',
method: 'emancipate',
params: ['domain'],
expect: {}
});
return view.extend({
css: `
:root {
--kiss-bg: #1a1a2e;
--kiss-card: #16213e;
--kiss-border: #0f3460;
--kiss-accent: #e94560;
--kiss-text: #eee;
--kiss-muted: #888;
--kiss-success: #00d26a;
--kiss-warning: #ffc107;
}
.kiss-container {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--kiss-bg);
color: var(--kiss-text);
padding: 20px;
min-height: 100vh;
}
.kiss-header {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid var(--kiss-border);
}
.kiss-header h2 {
margin: 0;
font-size: 1.8em;
color: var(--kiss-text);
}
.kiss-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8em;
font-weight: 600;
}
.kiss-badge-success { background: var(--kiss-success); color: #000; }
.kiss-badge-danger { background: var(--kiss-accent); color: #fff; }
.kiss-badge-warning { background: var(--kiss-warning); color: #000; }
.kiss-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 25px;
}
.kiss-card {
background: var(--kiss-card);
border: 1px solid var(--kiss-border);
border-radius: 10px;
padding: 20px;
}
.kiss-card h4 {
margin: 0 0 10px 0;
color: var(--kiss-muted);
font-size: 0.9em;
text-transform: uppercase;
}
.kiss-card .value {
font-size: 2em;
font-weight: 700;
color: var(--kiss-text);
}
.kiss-card .value.accent { color: var(--kiss-accent); }
.kiss-card .value.success { color: var(--kiss-success); }
.kiss-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 25px;
}
.kiss-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 0.9em;
transition: all 0.2s;
}
.kiss-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.kiss-btn-primary {
background: var(--kiss-accent);
color: #fff;
}
.kiss-btn-primary:hover:not(:disabled) {
background: #ff6b6b;
}
.kiss-btn-secondary {
background: var(--kiss-border);
color: var(--kiss-text);
}
.kiss-btn-secondary:hover:not(:disabled) {
background: #1a4a7a;
}
.kiss-btn-success {
background: var(--kiss-success);
color: #000;
}
.kiss-btn-danger {
background: #dc3545;
color: #fff;
}
.kiss-section {
margin-bottom: 25px;
}
.kiss-section h3 {
margin: 0 0 15px 0;
font-size: 1.2em;
color: var(--kiss-text);
}
.kiss-features {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.kiss-feature {
padding: 8px 15px;
background: var(--kiss-border);
border-radius: 20px;
font-size: 0.85em;
}
.kiss-feature.active {
background: var(--kiss-success);
color: #000;
}
.kiss-input-group {
display: flex;
gap: 10px;
margin-top: 15px;
}
.kiss-input {
flex: 1;
padding: 10px 15px;
border: 1px solid var(--kiss-border);
border-radius: 6px;
background: var(--kiss-bg);
color: var(--kiss-text);
font-size: 0.95em;
}
.kiss-link {
color: var(--kiss-accent);
text-decoration: none;
}
.kiss-link:hover {
text-decoration: underline;
}
.kiss-install-card {
text-align: center;
padding: 40px;
}
.kiss-install-card h3 {
margin-bottom: 15px;
}
.kiss-install-card p {
color: var(--kiss-muted);
margin-bottom: 25px;
}
`,
status: null,
stats: null,
load: function() {
return Promise.all([
callStatus(),
callGetStats()
]);
},
render: function(data) {
var self = this;
this.status = data[0] || {};
this.stats = data[1] || {};
var container = E('div', { 'class': 'kiss-container' }, [
E('style', {}, this.css),
this.renderHeader(),
this.status.installed ? this.renderDashboard() : this.renderInstallPrompt()
]);
poll.add(function() {
return Promise.all([callStatus(), callGetStats()]).then(function(results) {
self.status = results[0] || {};
self.stats = results[1] || {};
self.updateView();
});
}, 10);
return container;
},
renderHeader: function() {
var status = this.status;
var badge = !status.installed
? E('span', { 'class': 'kiss-badge kiss-badge-warning' }, 'Not Installed')
: status.running
? E('span', { 'class': 'kiss-badge kiss-badge-success' }, 'Running')
: E('span', { 'class': 'kiss-badge kiss-badge-danger' }, 'Stopped');
return E('div', { 'class': 'kiss-header' }, [
E('h2', {}, 'PhotoPrism Gallery'),
badge
]);
},
renderInstallPrompt: function() {
var self = this;
return E('div', { 'class': 'kiss-card kiss-install-card' }, [
E('h3', {}, 'PhotoPrism Not Installed'),
E('p', {}, 'Self-hosted Google Photos alternative with AI-powered face recognition, search, and albums.'),
E('button', {
'class': 'kiss-btn kiss-btn-primary',
'click': function() {
this.disabled = true;
this.textContent = 'Installing...';
callInstall().then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', {}, 'PhotoPrism installed successfully!'), 'success');
window.location.reload();
} else {
ui.addNotification(null, E('p', {}, 'Installation failed: ' + (res.output || 'Unknown error')), 'error');
}
});
}
}, 'Install PhotoPrism')
]);
},
renderDashboard: function() {
var self = this;
var status = this.status;
var stats = this.stats;
return E('div', {}, [
// Stats Grid
E('div', { 'class': 'kiss-grid', 'id': 'stats-grid' }, [
E('div', { 'class': 'kiss-card' }, [
E('h4', {}, 'Photos'),
E('div', { 'class': 'value accent', 'data-stat': 'photos' }, stats.photo_count || '0')
]),
E('div', { 'class': 'kiss-card' }, [
E('h4', {}, 'Videos'),
E('div', { 'class': 'value', 'data-stat': 'videos' }, stats.video_count || '0')
]),
E('div', { 'class': 'kiss-card' }, [
E('h4', {}, 'Originals Size'),
E('div', { 'class': 'value', 'data-stat': 'originals' }, stats.originals_size || '0')
]),
E('div', { 'class': 'kiss-card' }, [
E('h4', {}, 'Cache Size'),
E('div', { 'class': 'value', 'data-stat': 'cache' }, stats.storage_size || '0')
])
]),
// Actions
E('div', { 'class': 'kiss-section' }, [
E('h3', {}, 'Actions'),
E('div', { 'class': 'kiss-actions' }, [
E('button', {
'class': 'kiss-btn ' + (status.running ? 'kiss-btn-danger' : 'kiss-btn-success'),
'data-action': 'toggle',
'click': function() {
var fn = status.running ? callStop : callStart;
this.disabled = true;
fn().then(function() {
window.location.reload();
});
}
}, status.running ? 'Stop' : 'Start'),
E('button', {
'class': 'kiss-btn kiss-btn-secondary',
'disabled': !status.running,
'click': function() {
this.disabled = true;
this.textContent = 'Indexing...';
callIndex().then(function(res) {
ui.addNotification(null, E('p', {}, 'Indexing complete'), 'success');
window.location.reload();
});
}
}, 'Index Photos'),
E('button', {
'class': 'kiss-btn kiss-btn-secondary',
'disabled': !status.running,
'click': function() {
this.disabled = true;
this.textContent = 'Importing...';
callImport().then(function(res) {
ui.addNotification(null, E('p', {}, 'Import complete'), 'success');
window.location.reload();
});
}
}, 'Import'),
status.running ? E('a', {
'class': 'kiss-btn kiss-btn-primary',
'href': 'http://' + window.location.hostname + ':' + (status.port || 2342),
'target': '_blank'
}, 'Open Gallery') : E('span')
])
]),
// Features
E('div', { 'class': 'kiss-section' }, [
E('h3', {}, 'AI Features'),
E('div', { 'class': 'kiss-features' }, [
E('span', { 'class': 'kiss-feature ' + (status.face_recognition ? 'active' : '') }, 'Face Recognition'),
E('span', { 'class': 'kiss-feature ' + (status.object_detection ? 'active' : '') }, 'Object Detection'),
E('span', { 'class': 'kiss-feature ' + (status.places ? 'active' : '') }, 'Places / Maps')
])
]),
// Emancipate
E('div', { 'class': 'kiss-section' }, [
E('h3', {}, 'Public Exposure'),
status.domain
? E('p', {}, ['Gallery available at: ', E('a', { 'class': 'kiss-link', 'href': 'https://' + status.domain, 'target': '_blank' }, 'https://' + status.domain)])
: E('div', { 'class': 'kiss-input-group' }, [
E('input', {
'class': 'kiss-input',
'type': 'text',
'id': 'emancipate-domain',
'placeholder': 'photos.example.com'
}),
E('button', {
'class': 'kiss-btn kiss-btn-primary',
'click': function() {
var domain = document.getElementById('emancipate-domain').value;
if (!domain) {
ui.addNotification(null, E('p', {}, 'Please enter a domain'), 'warning');
return;
}
this.disabled = true;
this.textContent = 'Configuring...';
callEmancipate(domain).then(function(res) {
if (res.success) {
ui.addNotification(null, E('p', {}, 'Gallery exposed at ' + res.url), 'success');
window.location.reload();
}
});
}
}, 'Emancipate')
])
]),
// Danger Zone
E('div', { 'class': 'kiss-section' }, [
E('h3', {}, 'Danger Zone'),
E('button', {
'class': 'kiss-btn kiss-btn-danger',
'click': function() {
if (confirm('Remove PhotoPrism container? Photos will be preserved.')) {
this.disabled = true;
callUninstall().then(function() {
ui.addNotification(null, E('p', {}, 'PhotoPrism uninstalled'), 'success');
window.location.reload();
});
}
}
}, 'Uninstall')
])
]);
},
updateView: function() {
var stats = this.stats;
var photosEl = document.querySelector('[data-stat="photos"]');
var videosEl = document.querySelector('[data-stat="videos"]');
var originalsEl = document.querySelector('[data-stat="originals"]');
var cacheEl = document.querySelector('[data-stat="cache"]');
if (photosEl) photosEl.textContent = stats.photo_count || '0';
if (videosEl) videosEl.textContent = stats.video_count || '0';
if (originalsEl) originalsEl.textContent = stats.originals_size || '0';
if (cacheEl) cacheEl.textContent = stats.storage_size || '0';
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});