-
Eventos
-
-
Cargando eventos…
-
-
-
{{ ev.ts }} — {{ ev.type }}
-
- User: {{ ev.attrs['User-Name'] || ev.attrs['User-Name*0'] }}
- — NAS: {{ ev.attrs['NAS-IP-Address'] }}
- — STA: {{ ev.attrs['Calling-Station-Id'] }}
-
-
Decision: {{ ev.decision }}
-
Error: {{ ev.error }}
-
+
Cargando usuarios…
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+const filteredRequests = computed(() => {
+ return requests.value.filter(ev => {
+ if (eventFilters.type && ev.type !== eventFilters.type) return false;
+ if (eventFilters.text) {
+ const t = eventFilters.text.toLowerCase();
+ const blob = JSON.stringify(ev).toLowerCase();
+ if (!blob.includes(t)) return false;
+ }
+ return true;
+ });
+});
+
+const filteredUsers = computed(() => {
+ return users.value.filter(u => {
+ if (userFilters.text && !u.username.toLowerCase().includes(userFilters.text.toLowerCase())) return false;
+ if (userFilters.status === 'active' && u.disabled) return false;
+ if (userFilters.status === 'disabled' && !u.disabled) return false;
+ return true;
+ });
+});
+
+function copyRequests() {
+ const txt = JSON.stringify(requests.value, null, 2);
+ navigator.clipboard?.writeText(txt);
+}
+
+function toggleTheme() {
+ theme.value = theme.value === 'light' ? 'dark' : 'light';
+ localStorage.setItem('theme', theme.value);
+ applyTheme();
+}
+function applyTheme() {
+ document.documentElement.classList.toggle('light', theme.value === 'light');
+}
+
+function openAddUser() { /* placeholder for advanced modal */ }
+function openAddGuest() { /* placeholder for advanced modal */ }
+function openSettings() { /* placeholder for advanced modal */ }
+
diff --git a/frontend/src/components/EventCard.js b/frontend/src/components/EventCard.js
new file mode 100644
index 0000000..43c48d0
--- /dev/null
+++ b/frontend/src/components/EventCard.js
@@ -0,0 +1,27 @@
+import { defineComponent, h } from 'vue';
+import htm from 'htm';
+const html = htm.bind(h);
+
+export default defineComponent({
+ name: 'EventCard',
+ props: { ev: { type: Object, required: true } },
+ setup(props) {
+ return () => {
+ const a = props.ev.attrs || {};
+ return html`
+
+ ${props.ev.type}
+ ${props.ev.ts}
+ ${props.ev.decision ? html`Decision: ${props.ev.decision}` : ''}
+ ${props.ev.error ? html`Error` : ''}
+
+
+ ${a['User-Name'] || a['User-Name*0'] ? html`User: ${a['User-Name'] || a['User-Name*0']}` : ''}
+ ${a['NAS-IP-Address'] ? html` — NAS: ${a['NAS-IP-Address']}` : ''}
+ ${a['Calling-Station-Id'] ? html` — STA: ${a['Calling-Station-Id']}` : ''}
+
+
`;
+ };
+ }
+});
+
diff --git a/frontend/src/components/Modal.vue b/frontend/src/components/Modal.vue
new file mode 100644
index 0000000..025172d
--- /dev/null
+++ b/frontend/src/components/Modal.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
diff --git a/frontend/src/components/UserCard.js b/frontend/src/components/UserCard.js
new file mode 100644
index 0000000..62d21c5
--- /dev/null
+++ b/frontend/src/components/UserCard.js
@@ -0,0 +1,25 @@
+import { defineComponent, h } from 'vue';
+import htm from 'htm';
+const html = htm.bind(h);
+
+export default defineComponent({
+ name: 'UserCard',
+ props: { item: { type: Object, required: true }, mode: { type: String, default: 'user' } },
+ emits: ['toggleDisable', 'remove'],
+ setup(props, { emit }) {
+ function toggle() { emit('toggleDisable', props.item); }
+ function remove() { emit('remove', props.item); }
+ return () => html`
+
+ ${props.mode === 'user' ? props.item.username : (props.item.device || props.item.username)}
+ VLAN ${props.item.vlan}
+ ${props.item.disabled ? html`deshabilitado` : html`activo`}
+
+
+
+
+
${props.item.devices ? props.item.devices.length : 0} dispositivos
+
`;
+ }
+});
+
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 0e698cc..1570ac0 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -1,5 +1,6 @@
import { createApp } from 'vue';
import App from './App.vue';
+import './styles.css';
-createApp(App).mount('#app');
-
+const app = createApp(App);
+app.mount('#app');
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
new file mode 100644
index 0000000..4e3a22d
--- /dev/null
+++ b/frontend/src/styles.css
@@ -0,0 +1,75 @@
+:root {
+ --bg: 15 15 18;
+ --fg: 235 235 240;
+ --muted: 180 180 190;
+ --accent: 80 160 255;
+ --card: 28 28 34 / 0.55;
+ --border: 255 255 255 / 0.12;
+ --glass-blur: 14px;
+ --radius: 14px;
+}
+:root.light {
+ --bg: 245 245 248;
+ --fg: 20 20 22;
+ --muted: 110 110 120;
+ --accent: 18 108 242;
+ --card: 255 255 255 / 0.6;
+ --border: 0 0 0 / 0.08;
+}
+* { box-sizing: border-box; }
+html, body, #app { height: 100%; }
+html, body { margin: 0; padding: 0; background: rgb(var(--bg)); color: rgb(var(--fg)); }
+body { font: 14px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; }
+button { cursor: pointer; }
+a { color: inherit; }
+
+/* Top bar */
+.topbar {
+ position: sticky; top: 0; z-index: 10;
+ display: flex; flex-wrap: wrap; align-items: center;
+ gap: 10px; padding: 10px 14px; backdrop-filter: blur(var(--glass-blur));
+ background: linear-gradient(180deg, rgba(var(--card)), rgba(var(--card)) 60%, rgba(0,0,0,0));
+ border-bottom: 1px solid rgba(var(--border));
+}
+.title { font-size: 16px; font-weight: 700; letter-spacing: .2px; flex: 1 1 auto; }
+.actions { display: inline-flex; flex-wrap: wrap; gap: 8px; align-items: center; }
+.icon-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 10px; border-radius: 10px; border: 1px solid rgba(var(--border)); background: rgba(var(--card)); color: inherit; transition: transform .12s ease, background .2s;
+ backdrop-filter: blur(var(--glass-blur)); }
+.icon-btn:hover { transform: translateY(-1px); background: rgba(var(--card)); }
+.icon { width: 16px; height: 16px; opacity: .9; }
+
+/* Layout */
+.shell { height: calc(100vh - 54px); display: grid; grid-template-columns: 360px 1fr; gap: 12px; padding: 12px; }
+.panel { border: 1px solid rgba(var(--border)); background: rgba(var(--card)); border-radius: var(--radius); backdrop-filter: blur(var(--glass-blur)); overflow: hidden; display: flex; flex-direction: column; min-height: 0; }
+.panel-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid rgba(var(--border)); }
+.panel-title { font-weight: 600; }
+.panel-actions { display: inline-flex; flex-wrap: wrap; gap: 6px; }
+.scroll { overflow: auto; padding: 10px; }
+
+/* Cards */
+.card { border: 1px solid rgba(var(--border)); background: rgba(var(--card)); border-radius: 12px; padding: 10px; transition: transform .12s ease, box-shadow .2s ease; box-shadow: 0 4px 14px rgba(0,0,0,.08); }
+.card:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(0,0,0,.12); }
+.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; }
+
+/* Responsive */
+@media (max-width: 980px) {
+ .shell { grid-template-columns: 1fr; }
+}
+
+/* Collapse helpers: keep headers visible when collapsed */
+.panel.collapsed .scroll { display: none; }
+
+/* Modal */
+.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.35); backdrop-filter: blur(4px); display: grid; place-items: center; z-index: 20; animation: fadeIn .15s ease;
+}
+.modal { width: min(680px, 92vw); border-radius: 14px; border: 1px solid rgba(var(--border)); background: rgba(var(--card)); padding: 14px; box-shadow: 0 10px 32px rgba(0,0,0,.2); }
+.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
+.modal-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
+@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
+
+/* Small bits */
+.chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; border: 1px solid rgba(var(--border)); border-radius: 999px; background: rgba(var(--card)); font-size: 12px; }
+.muted { color: rgb(var(--muted)); }
+.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
+.spacer { flex: 1; }
+.toggle { padding: 6px 10px; border-radius: 8px; border: 1px solid rgba(var(--border)); background: rgba(var(--card)); }