190 lines
12 KiB
HTML
190 lines
12 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block title %}Global URL Registry - BZOD Admin{% endblock %}
|
|
|
|
{% block active_urls %}active{% endblock %}
|
|
|
|
{% block header_title %}Global URL Registry{% endblock %}
|
|
|
|
{% block content %}
|
|
{% if let Some(err) = error %}
|
|
<div class="alert alert-error">
|
|
{{ err }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="card registered-links-shell" style="padding: 0; overflow: hidden;">
|
|
<div class="registered-links-header">
|
|
<h3 style="font-size: 1.1rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
|
Global URL Registry
|
|
</h3>
|
|
{% if let Some(tag) = tag_filter %}
|
|
<span class="badge badge-healthy" style="text-transform: none;">
|
|
Filtered by tag: {{ tag }}
|
|
<a href="/admin/urls" style="color: inherit; text-decoration: none; margin-left: 0.5rem; font-weight: bold;">×</a>
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table class="registered-links-table">
|
|
<colgroup>
|
|
<col class="col-short" style="width: 22%;">
|
|
<col class="col-destination" style="width: 26%;">
|
|
<col class="col-owner" style="width: 14%;">
|
|
<col class="col-health" style="width: 10%;">
|
|
<col class="col-qr" style="width: 8%;">
|
|
<col class="col-actions" style="width: 20%;">
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th>Short Link</th>
|
|
<th>Destination</th>
|
|
<th>Owner / Tenant</th>
|
|
<th>Status / Health</th>
|
|
<th>QR Code</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% if urls.is_empty() %}
|
|
<tr>
|
|
<td colspan="6" style="text-align: center; color: var(--text-secondary); padding: 3rem;">
|
|
No shortened URLs registered across tenants.
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
{% for row in urls %}
|
|
<tr>
|
|
<td data-label="Short Link">
|
|
<div class="short-link-wrap">
|
|
<div class="short-link-primary">
|
|
<a href="/{{ row.url.code }}" target="_blank" class="short-link-anchor">
|
|
{{ base_url.replace("https://", "").replace("http://", "") }}/{{ row.url.code }}
|
|
</a>
|
|
{% if row.url.is_password_protected() %}
|
|
<span title="Password Protected" aria-label="Password protected link" style="color: var(--warning-color); display: inline-flex; align-items: center;">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if let Some(title) = row.url.title.as_deref() %}
|
|
{% if !title.is_empty() %}
|
|
<div class="short-link-title">{{ title }}</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
<div class="short-link-meta">Created {{ row.url.created_at[0..10] }}</div>
|
|
</div>
|
|
</td>
|
|
<td data-label="Destination">
|
|
<div class="destination-block">
|
|
<a href="{{ row.url.destination }}" target="_blank" class="destination-link" title="{{ row.url.destination }}" aria-label="Destination URL: {{ row.url.destination }}">
|
|
{{ row.url.destination }}
|
|
</a>
|
|
{% if let Some(desc) = row.url.description.as_deref() %}
|
|
{% if !desc.is_empty() %}
|
|
<div class="destination-description" title="{{ desc }}">{{ desc }}</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% if let Some(expires_at) = row.url.expires_at.as_deref() %}
|
|
{% if !expires_at.is_empty() %}
|
|
<div class="destination-meta expiry">
|
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>
|
|
Expires: {{ expires_at[0..10] }}
|
|
{% if row.url.expired %}
|
|
<span class="badge badge-dead" style="font-size: 0.6rem; padding: 0.1rem 0.25rem; margin-left: 0.25rem;">Expired</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
|
|
{% if let Some(max) = row.url.max_access_count %}
|
|
<div class="destination-meta access">
|
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
|
|
Clicks: {{ row.url.access_count }} / {{ max }}
|
|
{% if row.url.is_access_exhausted() %}
|
|
<span class="badge badge-dead" style="font-size: 0.6rem; padding: 0.1rem 0.25rem; margin-left: 0.25rem;">Limit Reached</span>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
{% if row.url.access_count > 0 %}
|
|
<div class="destination-meta neutral">
|
|
Clicks: {{ row.url.access_count }}
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td data-label="Owner / Tenant">
|
|
<div>
|
|
{% if let Some(u) = row.owner_username.as_deref() %}
|
|
<strong style="color: var(--text-primary);">{{ u }}</strong>
|
|
{% else %}
|
|
<span class="text-muted">Unassigned</span>
|
|
{% endif %}
|
|
<div style="font-family: monospace; font-size: 0.72rem; color: var(--text-muted);">{{ row.owner_tenant_id }}</div>
|
|
</div>
|
|
</td>
|
|
<td data-label="Status / Health" class="status-cell">
|
|
<span class="badge badge-{{ row.url.status }}">
|
|
{{ row.url.status }}
|
|
</span>
|
|
</td>
|
|
{% let code = row.url.code.as_str() %}
|
|
{% include "components/qr_preview.html" %}
|
|
<td data-label="Actions" class="actions-cell">
|
|
<div class="action-stack" style="display: flex; gap: 0.35rem; flex-wrap: wrap;">
|
|
<a href="/admin/moderation" class="btn btn-secondary" style="padding: 0.3rem 0.5rem; font-size: 0.75rem; display: inline-flex; align-items: center; gap: 0.2rem;">
|
|
🛡️ Moderate
|
|
</a>
|
|
<a href="/admin/slugs" class="btn btn-secondary" style="padding: 0.3rem 0.5rem; font-size: 0.75rem; display: inline-flex; align-items: center; gap: 0.2rem;">
|
|
🔄 Transfer
|
|
</a>
|
|
<a href="/admin/analytics/url/{{ row.url.id }}" class="btn btn-secondary" style="padding: 0.3rem 0.5rem; font-size: 0.75rem; display: inline-flex; align-items: center; gap: 0.2rem;">
|
|
📊 Analytics
|
|
</a>
|
|
<form action="/admin/urls/delete/{{ row.url.id }}" method="POST" onsubmit="return confirm('Are you sure you want to retire this link?');" style="display: inline;">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
|
<button type="submit" class="btn btn-danger" style="padding: 0.3rem 0.5rem; font-size: 0.75rem;">
|
|
Retire
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="pagination">
|
|
{% if current_page > 1 %}
|
|
<a href="/admin/urls?page=1{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;"><< First</a>
|
|
<a href="/admin/urls?page={{ current_page - 1 }}{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">< Prev</a>
|
|
{% else %}
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem; font-size: 0.85rem;"><< First</span>
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem; font-size: 0.85rem;">< Prev</span>
|
|
{% endif %}
|
|
|
|
{% for p in visible_pages %}
|
|
{% if self.is_current(p) %}
|
|
<span class="btn btn-primary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem; font-weight: bold;">[{{ p }}]</span>
|
|
{% else %}
|
|
<a href="/admin/urls?page={{ p }}{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">{{ p }}</a>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if current_page < total_pages %}
|
|
<a href="/admin/urls?page={{ current_page + 1 }}{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">Next ></a>
|
|
<a href="/admin/urls?page={{ total_pages }}{% if let Some(t) = tag_filter %}&tag={{ t }}{% endif %}" class="btn btn-secondary" style="padding: 0.4rem 0.8rem; font-size: 0.85rem;">Last >></a>
|
|
{% else %}
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem; font-size: 0.85rem;">Next ></span>
|
|
<span class="btn btn-secondary" style="opacity: 0.5; cursor: not-allowed; padding: 0.4rem 0.8rem; font-size: 0.85rem;">Last >></span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|