Merge pull request #337 from nguyenhuy158/fix/mobile-ui-optimization
Some checks failed
Docker Image Build and Push / build_self_hosted (push) Has been cancelled
Docker Image Build and Push / build_github_hosted_fallback (push) Has been cancelled

Optimize UI for mobile devices

testing in unstable
This commit is contained in:
Chris 2026-03-28 16:20:24 +01:00 committed by GitHub
commit fb440e8aa1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 136 additions and 97 deletions

File diff suppressed because one or more lines are too long

View file

@ -1593,8 +1593,8 @@ function renderIdPTable(idps) {
};
let tableHTML = `
<table class="table table-zebra table-sm policy-table w-full">
<colgroup>
<table class="table table-zebra table-sm policy-table w-full table-responsive">
<colgroup class="hidden md:table-column-group">
<col class="col-primary">
<col class="col-secondary">
<col class="col-tertiary">
@ -1621,18 +1621,18 @@ function renderIdPTable(idps) {
tableHTML += `
<tr>
<td class="p-3">
<td class="p-3" data-label="${t('js.table.provider')}">
<div class="flex items-center gap-3">
<span class="inline-flex items-center justify-center">${icon}</span>
<div class="font-medium">${idpData.name}</div>
</div>
</td>
<td class="p-3 text-xs opacity-70">
<td class="p-3 text-xs opacity-70" data-label="${t('js.table.cloudflare_id')}">
${idpData.cloudflare_id ? `<span class="tooltip" data-tip="${idpData.cloudflare_id}"><code>${idpData.cloudflare_id.slice(0, 8)}...</code></span>` : '-'}
</td>
<td class="p-3 text-sm opacity-80">${idpData.type}</td>
<td class="p-3">${statusBadge}</td>
<td class="p-3 text-right">
<td class="p-3 text-sm opacity-80" data-label="${t('js.table.connector')}">${idpData.type}</td>
<td class="p-3" data-label="${t('js.table.status')}">${statusBadge}</td>
<td class="p-3 text-right" data-label="${t('js.table.actions')}">
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">

View file

@ -28,12 +28,12 @@
</select>
</div>
</div>
<div class="flex gap-2 mt-4 sm:mt-0">
<button id="sync-cloudflare-btn" class="btn btn-sm btn-secondary">
<div class="flex flex-col sm:flex-row gap-2 mt-4 sm:mt-0 w-full sm:w-auto">
<button id="sync-cloudflare-btn" class="btn btn-sm btn-secondary w-full sm:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
{{ t('policies.sync_from_cloudflare') }}
</button>
<button id="create-access-group-btn" class="btn btn-sm btn-primary">
<button id="create-access-group-btn" class="btn btn-sm btn-primary w-full sm:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
{{ t('policies.create_new_group') }}
</button>
@ -42,8 +42,8 @@
{% if access_groups and access_groups.items() %}
<div class="overflow-x-auto table-container">
<table class="table table-zebra policy-table w-full" id="access-groups-table">
<colgroup>
<table class="table table-zebra policy-table w-full table-responsive" id="access-groups-table">
<colgroup class="hidden md:table-column-group">
<col class="col-primary">
<col class="col-secondary">
<col class="col-tertiary">
@ -68,7 +68,7 @@
{% set policy_type_label = 'system' %}
{% endif %}
<tr data-policy-type="{{ policy_type_label }}" data-group-id="{{ group_id }}">
<td class="px-4 py-3 cell-top">
<td class="px-4 py-3 cell-top" data-label="{{ t('policies.display_name') }}">
<div class="font-medium flex items-center gap-2">
{{ details.display_name }}
</div>
@ -84,15 +84,15 @@
</button>
{% endif %}
</td>
<td class="px-4 py-3 cell-top"><code class="badge badge-sm badge-outline">{{ group_id }}</code></td>
<td class="px-4 py-3 text-xs opacity-80 cell-top">
<td class="px-4 py-3 cell-top" data-label="{{ t('policies.group_id_label') }}"><code class="badge badge-sm badge-outline">{{ group_id }}</code></td>
<td class="px-4 py-3 text-xs opacity-80 cell-top" data-label="{{ t('policies.policy_summary') }}">
{% if details.policies %}
{{ t('policies.rules_defined', count=details.policies | length) }}
{% else %}
<span class="italic opacity-60">{{ t('policies.no_rules') }}</span>
{% endif %}
</td>
<td class="px-4 py-3">
<td class="px-4 py-3" data-label="{{ t('policies.policy_type') }}">
{% if details.external_policy %}
<span class="badge badge-sm badge-secondary df-badge">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" /></svg>
@ -110,7 +110,7 @@
</span>
{% endif %}
</td>
<td class="px-4 py-3 text-right align-top">
<td class="px-4 py-3 text-right align-top" data-label="{{ t('common.actions') }}">
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
@ -404,28 +404,30 @@
--policy-col-actions: 12%;
}
.policy-table {
table-layout: fixed;
}
@media (min-width: 769px) {
.policy-table {
table-layout: fixed;
}
.policy-table col.col-primary {
width: var(--policy-col-primary);
}
.policy-table col.col-primary {
width: var(--policy-col-primary);
}
.policy-table col.col-secondary {
width: var(--policy-col-secondary);
}
.policy-table col.col-secondary {
width: var(--policy-col-secondary);
}
.policy-table col.col-tertiary {
width: var(--policy-col-tertiary);
}
.policy-table col.col-tertiary {
width: var(--policy-col-tertiary);
}
.policy-table col.col-status {
width: var(--policy-col-status);
}
.policy-table col.col-status {
width: var(--policy-col-status);
}
.policy-table col.col-actions {
width: var(--policy-col-actions);
.policy-table col.col-actions {
width: var(--policy-col-actions);
}
}
.policy-table th,
@ -911,8 +913,8 @@
return;
}
let html = '<div class="overflow-x-auto table-container"><table class="table table-zebra policy-table w-full">';
html += '<colgroup>';
let html = '<div class="overflow-x-auto table-container"><table class="table table-zebra policy-table w-full table-responsive">';
html += '<colgroup class="hidden md:table-column-group">';
html += '<col class="col-primary">';
html += '<col class="col-secondary">';
html += '<col class="col-tertiary">';
@ -929,14 +931,14 @@
zonePolicies.forEach(zone => {
html += '<tr>';
html += `<td class="px-4 py-3 font-medium">${zone.zone_name}</td>`;
html += `<td class="px-4 py-3 font-medium" data-label="{{ t('policies.zone_name') }}">${zone.zone_name}</td>`;
if (zone.zone_id) {
html += `<td class="px-4 py-3 text-xs opacity-70"><span class=\"tooltip\" data-tip=\"${zone.zone_id}\"><code>${zone.zone_id.slice(0, 8)}...</code></span></td>`;
html += `<td class="px-4 py-3 text-xs opacity-70" data-label="{{ t('policies.zone_id') }}"><span class=\"tooltip\" data-tip=\"${zone.zone_id}\"><code>${zone.zone_id.slice(0, 8)}...</code></span></td>`;
} else {
html += '<td class="px-4 py-3 text-xs opacity-70">-</td>';
html += '<td class="px-4 py-3 text-xs opacity-70" data-label="{{ t('policies.zone_id') }}">-</td>';
}
html += `<td class="px-4 py-3"><code class="text-sm">*.${zone.zone_name}</code></td>`;
html += '<td class="px-4 py-3">';
html += `<td class="px-4 py-3" data-label="{{ t('policies.wildcard_hostname') }}"><code class="text-sm">*.${zone.zone_name}</code></td>`;
html += '<td class="px-4 py-3" data-label="{{ t('common.status') }}">';
if (zone.has_default_policy) {
html += '<span class="badge badge-sm badge-success df-badge">';
@ -946,7 +948,7 @@
html += '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /></svg>{{ t('policies.not_protected') }}</span>';
}
html += '</td><td class="px-4 py-3 text-right">';
html += '</td><td class="px-4 py-3 text-right" data-label="{{ t('common.actions') }}">';
if (!zone.has_default_policy) {
html += `<div class="dropdown dropdown-end">

View file

@ -10,9 +10,9 @@
<div class="flex flex-col sm:flex-row items-center justify-between border-b border-base-300 pb-3 mb-6">
<h1 class="card-title text-2xl sm:text-3xl">{{ t('agents.agents_management') }}</h1>
<div class="flex items-center gap-2 mt-4 sm:mt-0">
<button id="btn-force-reconcile" class="btn btn-ghost btn-sm">{{ t('agents.force_reconciliation') }}</button>
<button id="btn-generate-key" class="btn btn-primary btn-sm">
<div class="flex flex-col sm:flex-row items-center gap-2 mt-4 sm:mt-0 w-full sm:w-auto">
<button id="btn-force-reconcile" class="btn btn-ghost btn-sm w-full sm:w-auto">{{ t('agents.force_reconciliation') }}</button>
<button id="btn-generate-key" class="btn btn-primary btn-sm w-full sm:w-auto">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v-2.25L15.188 8.43c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" /></svg>
{{ t('agents.generate_new_api_key') }}
</button>
@ -20,8 +20,8 @@
</div>
<div class="overflow-x-auto min-w-0 agents-table-container">
<table class="table table-zebra w-full">
<colgroup>
<table class="table table-zebra w-full table-responsive">
<colgroup class="hidden md:table-column-group">
<col style="width:10%">
<col style="width:13%">
<col style="width:6%">
@ -81,8 +81,8 @@
<div class="card-body">
<h2 class="card-title text-xl border-b border-base-300 pb-3 mb-6">{{ t('agents.active_api_keys') }}</h2>
<div class="overflow-x-auto active-keys-table-container">
<table class="table table-zebra table-sm w-full api-keys-table">
<colgroup>
<table class="table table-zebra table-sm w-full api-keys-table table-responsive">
<colgroup class="hidden md:table-column-group">
<col class="col-key">
<col class="col-owner">
<col class="col-created">
@ -127,8 +127,8 @@
</div>
</div>
<div class="overflow-x-auto revoked-keys-table-container">
<table class="table table-zebra table-sm w-full api-keys-table">
<colgroup>
<table class="table table-zebra table-sm w-full api-keys-table table-responsive">
<colgroup class="hidden md:table-column-group">
<col class="col-key">
<col class="col-owner">
<col class="col-created">
@ -700,27 +700,27 @@
}
tr.innerHTML = `
<td class="px-4 py-3 whitespace-nowrap">
<td class="px-4 py-3 whitespace-nowrap" data-label="${t('agents.agent_id')}">
<span class="font-mono text-xs text-base-content/50 cursor-help" title="${agentId}">${agentId.substring(0, 8)}…</span>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<td class="px-4 py-3 whitespace-nowrap" data-label="${t('agents.display_name')}">
<span class="text-sm font-medium">${agent.display_name || na}</span>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<td class="px-4 py-3 whitespace-nowrap" data-label="${t('agents.version')}">
<span class="text-sm text-base-content/60">${agent.version || na}</span>
</td>
<td class="px-4 py-3">
<td class="px-4 py-3" data-label="${t('agents.status')}">
<div class="flex flex-wrap gap-1">${statusHtml}</div>
</td>
<td class="px-4 py-3 text-center">${ledStripHtml}</td>
<td class="px-4 py-3 whitespace-nowrap">
<td class="px-4 py-3 text-center" data-label="${t('agents.heart_beat')}">${ledStripHtml}</td>
<td class="px-4 py-3 whitespace-nowrap" data-label="${t('agents.assigned_tunnel')}">
<span class="text-sm">${agent.assigned_tunnel_name || na}</span>
</td>
<td class="px-4 py-3">${migrationHtml}${migrationStatus && (migrationStatus.has_conflicts || migrationStatus.has_orphaned) ? `<button class="btn btn-xs btn-info ml-2 btn-migration-assistant" data-agent-id="${agentId}" title="Open Migration Assistant">Resolve</button>` : ''}</td>
<td class="px-4 py-3 whitespace-nowrap">${cloudflaredVersion}</td>
<td class="px-4 py-3 whitespace-nowrap">${originIp}</td>
<td class="px-4 py-3">${getTunnelStatusBadge(agent.tunnel_status)}</td>
<td class="px-4 py-3 text-center">
<td class="px-4 py-3" data-label="${t('agents.migration')}">${migrationHtml}${migrationStatus && (migrationStatus.has_conflicts || migrationStatus.has_orphaned) ? `<button class="btn btn-xs btn-info ml-2 btn-migration-assistant" data-agent-id="${agentId}" title="Open Migration Assistant">Resolve</button>` : ''}</td>
<td class="px-4 py-3 whitespace-nowrap" data-label="${t('agents.cloudflared_version')}">${cloudflaredVersion}</td>
<td class="px-4 py-3 whitespace-nowrap" data-label="${t('agents.origin_ip')}">${originIp}</td>
<td class="px-4 py-3" data-label="${t('agents.tunnel_status')}">${getTunnelStatusBadge(agent.tunnel_status)}</td>
<td class="px-4 py-3 text-center" data-label="${t('common.actions')}">
<button class="btn btn-ghost btn-sm agent-action-trigger"
data-agent-id="${agentId}"
data-enrolled="${agent.status === 'enrolled'}"
@ -781,14 +781,14 @@
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="px-4 py-3 whitespace-nowrap font-mono break-all">${keyId.substring(0, 8)}...</td>
<td class="px-4 py-3 whitespace-nowrap">${owner}</td>
<td class="px-4 py-3 whitespace-nowrap">${createdAt}</td>
<td class="px-4 py-3 whitespace-nowrap">
<td class="px-4 py-3 whitespace-nowrap font-mono break-all" data-label="${t('agents.key_partial')}">${keyId.substring(0, 8)}...</td>
<td class="px-4 py-3 whitespace-nowrap" data-label="${t('agents.owner')}">${owner}</td>
<td class="px-4 py-3 whitespace-nowrap" data-label="${t('agents.created_at')}">${createdAt}</td>
<td class="px-4 py-3 whitespace-nowrap" data-label="${t('common.status')}">
<span class="badge badge-success badge-sm">Active</span>
<br><small class="text-xs opacity-60">${lastUsed}</small>
</td>
<td class="px-4 py-3 text-right">
<td class="px-4 py-3 text-right" data-label="${t('common.actions')}">
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
@ -857,22 +857,22 @@
const tr = document.createElement('tr');
tr.className = 'opacity-60 italic';
tr.innerHTML = `
<td class="px-4 py-3 font-mono break-all text-sm">
<td class="px-4 py-3 font-mono break-all text-sm" data-label="${t('agents.full_api_key')}">
<div class="bg-base-200 p-2 rounded border-2 border-dashed">
<span class="text-xs opacity-75">Full Key:</span><br>
<code class="text-xs">${keyId}</code>
<button class="btn btn-xs btn-ghost ml-2 btn-copy-revoked-key" data-key="${keyId}">Copy</button>
</div>
</td>
<td class="px-4 py-3 whitespace-nowrap line-through">${owner}</td>
<td class="px-4 py-3 whitespace-nowrap">
<td class="px-4 py-3 whitespace-nowrap line-through" data-label="${t('agents.owner')}">${owner}</td>
<td class="px-4 py-3 whitespace-nowrap" data-label="${t('agents.revoked_at')}">
<span class="badge badge-error badge-sm">Revoked</span>
<br><small class="text-xs">${revokedAt}</small>
</td>
<td class="px-4 py-3 whitespace-nowrap">
<td class="px-4 py-3 whitespace-nowrap" data-label="${t('agents.auto_delete')}">
${autoDeleteInfo}
</td>
<td class="px-4 py-3 text-right">
<td class="px-4 py-3 text-right" data-label="${t('common.actions')}">
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">

View file

@ -30,6 +30,43 @@
#log-output::-webkit-scrollbar-thumb:hover { background-color: rgba(156, 163, 175, 0.7); }
html[data-theme="dark"] #log-output::-webkit-scrollbar-thumb { background-color: rgba(107, 114, 128, 0.5); }
html[data-theme="dark"] #log-output::-webkit-scrollbar-thumb:hover { background-color: rgba(107, 114, 128, 0.7); }
/* Mobile Responsive Table Support */
@media (max-width: 768px) {
.table-responsive thead { display: none; }
.table-responsive tr {
display: block;
margin-bottom: 1rem;
border: 1px solid hsl(var(--bc) / 0.1);
border-radius: var(--rounded-box, 1rem);
background-color: hsl(var(--b1));
padding: 0.5rem;
box-shadow: var(--shadow);
}
.table-responsive td {
display: flex;
justify-content: flex-end;
align-items: center;
text-align: right;
padding: 0.5rem 1rem !important;
border: none !important;
position: relative;
min-height: 2.5rem;
}
.table-responsive td::before {
content: attr(data-label);
position: absolute;
left: 1rem;
font-weight: 700;
text-transform: uppercase;
font-size: 0.65rem;
opacity: 0.6;
}
.table-responsive td:last-child { border-bottom: 0; }
/* Specific overrides for status badges and links */
.table-responsive td > * { max-width: 60%; }
}
</style>
</head>

View file

@ -165,8 +165,8 @@
</p>
{% if all_account_tunnels is defined and all_account_tunnels %}
<div class="overflow-x-auto -mx-6 sm:-mx-8">
<table class="table table-sm w-full">
<thead>
<table class="table table-sm w-full table-responsive">
<thead class="hidden md:table-header-group">
<tr>
<th class="w-12">+/-</th>
<th>{{ t('settings.tunnel_name') }}</th>
@ -179,21 +179,21 @@
<tbody>
{% for tunnel in all_account_tunnels %}
<tr>
<td class="text-center">
<td class="text-center" data-label="Expand">
<button type="button" class="btn btn-xs btn-ghost btn-circle tunnel-dns-toggle"
data-tunnel-id="{{ tunnel.id | e }}" aria-expanded="false" aria-controls="dns-records-{{ tunnel.id | e }}" title="{{ t('settings.toggle_dns_records') }}">
<svg class="w-4 h-4 expand-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
<svg class="w-4 h-4 collapse-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"></path></svg>
</button>
</td>
<td class="text-sm font-medium">{{ tunnel.name | e }}</td>
<td><code class="text-xs opacity-80">{{ tunnel.id | e }}</code></td>
<td>
<td class="text-sm font-medium" data-label="{{ t('settings.tunnel_name') }}">{{ tunnel.name | e }}</td>
<td data-label="{{ t('settings.tunnel_id') }}"><code class="text-xs opacity-80">{{ tunnel.id | e }}</code></td>
<td data-label="{{ t('common.status') }}">
{% set status_color = 'badge-success' if tunnel.status | lower == 'healthy' else ('badge-warning' if tunnel.status | lower == 'degraded' else ('badge-error' if tunnel.status | lower == 'down' else 'badge-ghost')) %}
<span class="badge {{ status_color }} badge-sm">{{ tunnel.status | capitalize | e }}</span>
</td>
<td class="text-xs opacity-70">{% if tunnel.created_at %}{{ tunnel.created_at.split('T')[0] | e }}{% else %}N/A{% endif %}</td>
<td class="text-xs">
<td class="text-xs opacity-70" data-label="{{ t('settings.created_at') }}">{% if tunnel.created_at %}{{ tunnel.created_at.split('T')[0] | e }}{% else %}N/A{% endif %}</td>
<td class="text-xs" data-label="{{ t('common.actions') }}">
<button type="button" class="btn btn-xs btn-error delete-tunnel-btn"
data-tunnel-id="{{ tunnel.id | e }}"
data-tunnel-name="{{ tunnel.name | e }}">

View file

@ -47,7 +47,7 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="12" cy="12" r="10" class="opacity-30"/><path d="M12 8h.01" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 12h1v4h1" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</h2>
<button class="btn btn-sm btn-primary ml-auto" onclick="document.getElementById('add_manual_rule_modal').showModal()">
<button class="btn btn-sm btn-primary w-full sm:w-auto mt-4 sm:mt-0" onclick="document.getElementById('add_manual_rule_modal').showModal()">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-1"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
{{ t('status.add_manual_rule') }}
</button>
@ -151,7 +151,7 @@
</div>
<div class="overflow-x-auto pb-24">
<table class="table table-zebra table-sm w-full">
<table class="table table-zebra table-sm w-full table-responsive">
<thead>
<tr>
<th class="p-3 w-auto">{{ t('common.status') }}</th>
@ -167,7 +167,7 @@
<tbody>
{% for hostname, details in rules.items()|sort %}
<tr data-rule-key="{{ hostname }}" data-rule-status="{{ details.status }}" data-rule-source="{{ details.source }}">
<td class="p-3 whitespace-nowrap" data-role="status-cell">
<td class="p-3 whitespace-nowrap" data-role="status-cell" data-label="{{ t('common.status') }}">
{% if details.source == 'manual' %}
<span class="badge badge-info badge-sm status-badge">{{ t('status.manual_rules') }}</span>
{% else %}
@ -177,7 +177,7 @@
</span>
{% endif %}
</td>
<td class="p-3 whitespace-nowrap align-top" data-role="tunnel-name" data-tunnel-name="{{ details.tunnel_name or '' }}">
<td class="p-3 whitespace-nowrap align-top" data-role="tunnel-name" data-tunnel-name="{{ details.tunnel_name or '' }}" data-label="{{ t('status.tunnel_name') }}">
<div class="flex flex-col">
<span class="text-sm opacity-90">{{ details.tunnel_name or 'N/A' }}</span>
<span class="text-xs opacity-60 mt-1">Zone: {{ details.zone_name or 'Unknown' }}</span>
@ -188,16 +188,16 @@
{% endif %}
</div>
</td>
<td class="p-3 whitespace-nowrap">
<td class="p-3 whitespace-nowrap" data-label="{{ t('status.hostname') }}">
{% set display_hostname = details.hostname if details.hostname else hostname.split('|')[0] %}
{% set display_path = details.path if details.path else (hostname.split('|')[1] if '|' in hostname else None) %}
<a href="https://{{ display_hostname }}{% if display_path %}{{ display_path }}{% endif %}" target="_blank" rel="noopener noreferrer" title="{{ t('status.open_url', hostname=display_hostname, path=display_path if display_path else '') }}" class="link link-hover link-primary text-sm">
{{ display_hostname }}
</a>
{% if display_hostname and display_hostname.startswith('*.') %}
<span class="badge badge-info badge-xs ml-2">wildcard</span>
{% endif %}
<div class="text-xs mt-1">
</a>
{% if display_hostname and display_hostname.startswith('*.') %}
<span class="badge badge-info badge-xs ml-2">wildcard</span>
{% endif %}
<div class="text-xs mt-1">
{% if details.no_tls_verify %}
<span class="badge badge-warning badge-xs" title="{{ t('status.tls_verification_disabled') }}">{{ t('status.no_tls_verify') }}</span>
{% endif %}
@ -217,16 +217,16 @@
<span class="badge badge-info badge-xs ml-1" title="{{ t('status.match_sni_to_host_description') }}">{{ t('status.match_sni_to_host') }}</span>
{% endif %}
</div>
</td>
<td class="p-3 whitespace-nowrap">
</td>
<td class="p-3 whitespace-nowrap" data-label="{{ t('status.path') }}">
{% if display_path and display_path.strip() %}
<code class="text-xs opacity-70">{{ display_path }}</code>
{% else %}
<span class="text-xs opacity-50 italic">(root)</span>
{% endif %}
</td>
<td class="p-3 whitespace-nowrap"><code class="text-xs opacity-80">{{ details.service }}</code></td>
<td class="p-3 whitespace-nowrap text-sm opacity-80" id="access-policy-display-{{ details.source if details.source else 'docker' }}-{{ hostname | replace('.', '-') }}">
<td class="p-3 whitespace-nowrap" data-label="{{ t('status.service_target') }}"><code class="text-xs opacity-80">{{ details.service }}</code></td>
<td class="p-3 whitespace-nowrap text-sm opacity-80" id="access-policy-display-{{ details.source if details.source else 'docker' }}-{{ hostname | replace('.', '-') }}" data-label="{{ t('status.access_policy') }}">
{% if details.access_policy_type == 'group' %}
{% if details.access_group_id is iterable and details.access_group_id is not string %}
{% for group_id in details.access_group_id %}
@ -252,7 +252,7 @@
<span class="badge badge-error badge-xs ml-2 animate-pulse" title="{{ t('status.docker_rule_overridden') }}">{{ t('status.rule_ui_override') }}</span>
{% endif %}
</td>
<td class="p-3 whitespace-nowrap text-sm opacity-70" data-role="expires-cell">
<td class="p-3 whitespace-nowrap text-sm opacity-70" data-role="expires-cell" data-label="{{ t('status.expires_in') }}">
{% if details.status=='pending_deletion' and details.delete_at %}
<div data-delete-at="{{ details.delete_at.isoformat() }}">
<span class="absolute-time-display"></span>
@ -262,7 +262,7 @@
<span class="text-xs opacity-60">N/A</span>
{% endif %}
</td>
<td class="p-3">
<td class="p-3" data-label="{{ t('common.actions') }}">
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">