Files
arrr-erp/app/modules/documents/templates/documents/storage_nodes.html
T
2026-06-20 15:01:44 +05:30

200 lines
13 KiB
HTML

{% extends "ui/templates/base/layout.html" %}
{% block content %}
<div class="space-y-6">
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<h1 class="text-2xl font-bold text-slate-900">Branch Local Storage</h1>
<p class="text-sm text-slate-500">Partner/branch controlled storage setup for engagement document streaming.</p>
<p class="text-xs text-slate-400 mt-1">Scope: {{ storage_scope_title or '-' }}</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="/documents" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Back to Documents</a>
<a href="/documents/storage-jobs" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Storage Jobs</a>
<a href="/documents/download-requests" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Download Requests</a>
</div>
</div>
{% if request.query_params.get('error') == 'branch_not_linked' %}
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">
Your user is not linked to a branch. Please update the Partner/Branch Manager user profile with branch_id before enabling local storage.
</div>
{% endif %}
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="text-xs text-slate-500">Storage Nodes</div>
<div class="mt-1 text-2xl font-bold text-slate-900">{{ nodes|length }}</div>
</div>
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="text-xs text-slate-500">Online / Active</div>
<div class="mt-1 text-2xl font-bold text-emerald-700">{{ nodes|selectattr('is_active')|list|length }}</div>
</div>
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="text-xs text-slate-500">Recent Storage Jobs</div>
<div class="mt-1 text-2xl font-bold text-slate-900">{{ recent_jobs|length if recent_jobs is defined else 0 }}</div>
</div>
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="text-xs text-slate-500">Recent Download Requests</div>
<div class="mt-1 text-2xl font-bold text-slate-900">{{ recent_download_requests|length if recent_download_requests is defined else 0 }}</div>
</div>
</div>
{% if request.query_params.get('created') == '1' %}
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">
Storage node created. Use the <strong>Download Agent Package</strong> button in the node row to generate the branch package. Secret and standalone .env are not displayed.
</div>
{% endif %}
{% if request.query_params.get('error') == 'env_download_disabled' %}
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
Standalone .env download is disabled for security. Download the pre-configured agent ZIP instead.
</div>
{% endif %}
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
<div class="space-y-6">
{% if can_manage_branch_storage %}
<form method="post" action="/documents/storage-nodes/auto-setup" class="rounded-2xl border bg-white p-5 space-y-4 shadow-sm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div>
<h2 class="font-bold text-slate-900">One-click branch setup</h2>
<p class="text-xs text-slate-500 mt-1">Recommended. ERP will create or reuse the branch node and immediately download the pre-configured LSA2 Windows Service ZIP. Secret is not shown on screen. If a branch already has a storage root, the existing root will be reused and will not be silently changed.</p>
</div>
<div>
<label class="text-sm font-medium">Branch</label>
{% if forced_branch_id %}
<input type="hidden" name="branch_id" value="{{ forced_branch_id }}">
<div class="mt-1 rounded-xl border bg-slate-50 p-2 text-sm text-slate-700">
{% set fb = branch_map.get(forced_branch_id) if branch_map else None %}
{{ fb.name if fb else ('Branch ID ' ~ forced_branch_id) }}{% if fb and fb.code %} ({{ fb.code }}){% endif %}
</div>
{% else %}
<select name="branch_id" class="mt-1 w-full rounded-xl border p-2">
<option value="">Firm-level / Head office storage</option>
{% for branch in branches or [] %}
<option value="{{ branch.id }}">{{ branch.name }}{% if branch.code %} ({{ branch.code }}){% endif %}</option>
{% endfor %}
</select>
{% endif %}
</div>
<div><label class="text-sm font-medium">Local Storage Root</label><input name="storage_root_path" class="mt-1 w-full rounded-xl border p-2" value="D:\AuditFirmStorage"><p class="text-xs text-amber-700 mt-1">For a new branch setup only. Existing branch nodes keep their already configured root.</p></div>
<div><label class="text-sm font-medium">Quota GB</label><input name="quota_limit_gb" class="mt-1 w-full rounded-xl border p-2" placeholder="Optional"></div>
<button class="w-full rounded-xl bg-emerald-700 text-white py-2 font-semibold">Download / Regenerate Branch Agent Package</button>
</form>
<form method="post" action="/documents/storage-nodes" class="rounded-2xl border bg-white p-5 space-y-4 shadow-sm">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div>
<h2 class="font-bold text-slate-900">Manual node creation</h2>
<p class="text-xs text-slate-500 mt-1">Use only when you want a custom node code/name.</p>
</div>
<div><label class="text-sm font-medium">Node Code</label><input name="node_code" required class="mt-1 w-full rounded-xl border p-2" placeholder="CHN-STORAGE-01"></div>
<div><label class="text-sm font-medium">Node Name</label><input name="node_name" required class="mt-1 w-full rounded-xl border p-2" placeholder="Chennai Branch Storage"></div>
<div>
<label class="text-sm font-medium">Branch</label>
{% if forced_branch_id %}
<input type="hidden" name="branch_id" value="{{ forced_branch_id }}">
<div class="mt-1 rounded-xl border bg-slate-50 p-2 text-sm text-slate-700">
{% set fb = branch_map.get(forced_branch_id) if branch_map else None %}
{{ fb.name if fb else ('Branch ID ' ~ forced_branch_id) }}{% if fb and fb.code %} ({{ fb.code }}){% endif %}
</div>
{% else %}
<select name="branch_id" class="mt-1 w-full rounded-xl border p-2">
<option value="">Firm-level / Head office storage</option>
{% for branch in branches or [] %}
<option value="{{ branch.id }}">{{ branch.name }}{% if branch.code %} ({{ branch.code }}){% endif %}</option>
{% endfor %}
</select>
{% endif %}
</div>
<div><label class="text-sm font-medium">Connector URL</label><input name="connector_url" class="mt-1 w-full rounded-xl border p-2" placeholder="Optional zero-trust tunnel URL"></div>
<div><label class="text-sm font-medium">Local Storage Root</label><input name="storage_root_path" class="mt-1 w-full rounded-xl border p-2" placeholder="D:\AuditFirmStorage"></div>
<div><label class="text-sm font-medium">Quota GB</label><input name="quota_limit_gb" class="mt-1 w-full rounded-xl border p-2" placeholder="Optional"></div>
<button class="w-full rounded-xl bg-slate-900 text-white py-2 font-semibold">Create Node</button>
</form>
{% else %}
<div class="rounded-2xl border bg-slate-50 p-5 text-sm text-slate-600">
Storage setup package generation is available to Firm Admin, Partner and Branch Manager. This screen is monitoring-only for your current role.
</div>
{% endif %}
</div>
<div class="xl:col-span-2 space-y-6">
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
<div class="p-4 border-b flex items-center justify-between">
<div>
<h2 class="font-bold text-slate-900">Configured storage nodes</h2>
<p class="text-xs text-slate-500">Partners/Branch Managers see only their managed branch. One branch should have one active node and one fixed storage root. Existing node secrets are not shown again.</p>
</div>
</div>
<table class="w-full text-sm">
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-3 text-left">Node</th><th class="p-3 text-left">Branch</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Last Seen</th><th class="p-3 text-left">Root</th><th class="p-3 text-right">Action</th></tr></thead>
<tbody>
{% for node in nodes %}
{% set b = branch_map.get(node.branch_id) if branch_map and node.branch_id else None %}
<tr class="border-t align-top">
<td class="p-3"><div class="font-semibold">{{ node.node_name }}</div><div class="text-xs text-slate-500">{{ node.node_code }}</div></td>
<td class="p-3">{{ b.name if b else (node.branch_id or 'Firm-level') }}</td>
<td class="p-3"><span class="px-2 py-1 rounded-full text-xs {{ 'bg-emerald-100 text-emerald-700' if node.is_active else 'bg-slate-100 text-slate-600' }}">{{ node.status }}</span></td>
<td class="p-3 text-xs">{{ node.last_seen_at_utc or '-' }}</td>
<td class="p-3 text-xs font-mono break-all">{{ node.storage_root_path or '-' }}</td>
<td class="p-3 text-right">
{% if can_manage_branch_storage %}
<div class="flex flex-col items-end gap-2">
{% if node.status == 'disabled_duplicate' %}
<span class="text-xs text-slate-500">Duplicate disabled</span>
{% else %}
<form method="post" action="/documents/storage-nodes/{{ node.id }}/download-agent-package">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="storage_root_path" value="{{ node.storage_root_path or 'D:\AuditFirmStorage' }}">
<button class="text-sm text-emerald-700 font-semibold">Download Agent Package</button>
</form>
<form method="post" action="/documents/storage-nodes/{{ node.id }}/toggle">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="text-sm text-blue-700 font-semibold">{{ 'Disable' if node.is_active else 'Enable' }}</button>
</form>
{% endif %}
</div>
{% else %}-{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6" class="p-6 text-center text-slate-500">No storage nodes registered yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
<div class="p-4 border-b"><h2 class="font-bold text-slate-900">Recent Storage Jobs</h2></div>
<table class="w-full text-xs">
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-2 text-left">Job</th><th class="p-2 text-left">Status</th><th class="p-2 text-left">Created</th></tr></thead>
<tbody>
{% for job in recent_jobs or [] %}
<tr class="border-t"><td class="p-2">#{{ job.id }}</td><td class="p-2">{{ job.status }}</td><td class="p-2">{{ job.created_at_utc or '-' }}</td></tr>
{% else %}
<tr><td colspan="3" class="p-4 text-center text-slate-500">No recent storage jobs.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
<div class="p-4 border-b"><h2 class="font-bold text-slate-900">Recent Download Requests</h2></div>
<table class="w-full text-xs">
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-2 text-left">Request</th><th class="p-2 text-left">Status</th><th class="p-2 text-left">Created</th></tr></thead>
<tbody>
{% for dr in recent_download_requests or [] %}
<tr class="border-t"><td class="p-2">#{{ dr.id }}</td><td class="p-2">{{ dr.request_status }}</td><td class="p-2">{{ dr.created_at_utc or '-' }}</td></tr>
{% else %}
<tr><td colspan="3" class="p-4 text-center text-slate-500">No recent download requests.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}