Files
ScanLook/templates/module_manager.html
Javier 406219547d feat: Implement modular plugin architecture
- Convert invcount to self-contained module
- Add Module Manager for install/uninstall
- Create module_registry database table
- Support hot-reloading of modules
- Move data imports into invcount module
- Update all templates and routes to new structure

Version bumped to 0.16.0
2026-02-07 01:47:49 -06:00

182 lines
6.4 KiB
HTML

{% extends "base.html" %}
{% block title %}Module Manager - ScanLook{% endblock %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="fas fa-puzzle-piece"></i> Module Manager</h1>
<a href="{{ url_for('home') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Home
</a>
</div>
<p class="lead">Install, uninstall, and manage ScanLook modules</p>
<div class="row">
{% for module in modules %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 {% if module.is_active %}border-success{% elif module.is_installed %}border-warning{% endif %}">
<div class="card-header {% if module.is_active %}bg-success text-white{% elif module.is_installed %}bg-warning text-dark{% else %}bg-light{% endif %}">
<h5 class="mb-0">
<i class="fas fa-cube"></i> {{ module.name }}
<span class="badge badge-secondary float-right">v{{ module.version }}</span>
</h5>
</div>
<div class="card-body">
<p class="card-text">{{ module.description }}</p>
<p class="mb-2">
<small class="text-muted">
<strong>Author:</strong> {{ module.author }}<br>
<strong>Module Key:</strong> <code>{{ module.module_key }}</code>
</small>
</p>
<div class="mt-3">
{% if module.is_installed and module.is_active %}
<span class="badge badge-success mb-2">
<i class="fas fa-check-circle"></i> Active
</span>
{% elif module.is_installed %}
<span class="badge badge-warning mb-2">
<i class="fas fa-pause-circle"></i> Installed (Inactive)
</span>
{% else %}
<span class="badge badge-secondary mb-2">
<i class="fas fa-times-circle"></i> Not Installed
</span>
{% endif %}
</div>
</div>
<div class="card-footer bg-light">
{% if not module.is_installed %}
<button class="btn btn-primary btn-sm btn-block" onclick="installModule('{{ module.module_key }}')">
<i class="fas fa-download"></i> Install
</button>
{% elif module.is_active %}
<button class="btn btn-warning btn-sm btn-block mb-2" onclick="deactivateModule('{{ module.module_key }}')">
<i class="fas fa-pause"></i> Deactivate
</button>
<button class="btn btn-danger btn-sm btn-block" onclick="uninstallModule('{{ module.module_key }}')">
<i class="fas fa-trash"></i> Uninstall
</button>
{% else %}
<button class="btn btn-success btn-sm btn-block mb-2" onclick="activateModule('{{ module.module_key }}')">
<i class="fas fa-play"></i> Activate
</button>
<button class="btn btn-danger btn-sm btn-block" onclick="uninstallModule('{{ module.module_key }}')">
<i class="fas fa-trash"></i> Uninstall
</button>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% if not modules %}
<div class="alert alert-info">
<i class="fas fa-info-circle"></i> No modules found in the <code>/modules</code> directory.
</div>
{% endif %}
</div>
<script>
function installModule(moduleKey) {
if (!confirm(`Install module "${moduleKey}"?\n\nThis will create database tables and activate the module.`)) {
return;
}
fetch(`/admin/modules/${moduleKey}/install`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`${data.message}\n\nPlease reload the page.`);
location.reload();
} else {
alert(`${data.message}`);
}
})
.catch(error => {
alert(`❌ Error: ${error}`);
});
}
function uninstallModule(moduleKey) {
if (!confirm(`⚠️ UNINSTALL module "${moduleKey}"?\n\nThis will DELETE all module data and cannot be undone!`)) {
return;
}
fetch(`/admin/modules/${moduleKey}/uninstall`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`${data.message}\n\nPlease reload the page.`);
location.reload();
} else {
alert(`${data.message}`);
}
})
.catch(error => {
alert(`❌ Error: ${error}`);
});
}
function activateModule(moduleKey) {
fetch(`/admin/modules/${moduleKey}/activate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`${data.message}\n\nPlease reload the page.`);
location.reload();
} else {
alert(`${data.message}`);
}
})
.catch(error => {
alert(`❌ Error: ${error}`);
});
}
function deactivateModule(moduleKey) {
if (!confirm(`Deactivate module "${moduleKey}"?\n\nUsers will lose access until reactivated.`)) {
return;
}
fetch(`/admin/modules/${moduleKey}/deactivate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`${data.message}\n\nPlease reload the page.`);
location.reload();
} else {
alert(`${data.message}`);
}
})
.catch(error => {
alert(`❌ Error: ${error}`);
});
}
</script>
{% endblock %}