diff --git a/web/src/App.vue b/web/src/App.vue
index 9166780..2afaef6 100644
--- a/web/src/App.vue
+++ b/web/src/App.vue
@@ -58,6 +58,7 @@ const navLinks = [
{ to: '/bundles', label: 'Bundles' },
{ to: '/sources', label: 'Sources' },
{ to: '/context', label: 'Context' },
+ { to: '/blocklist', label: 'Blocklist' },
]
const isDark = ref(document.documentElement.classList.contains('dark'))
diff --git a/web/src/router/index.ts b/web/src/router/index.ts
index ee1903d..b2c7f97 100644
--- a/web/src/router/index.ts
+++ b/web/src/router/index.ts
@@ -7,6 +7,7 @@ import IncidentsView from '@/views/IncidentsView.vue'
import BundlesView from '@/views/BundlesView.vue'
import SettingsView from '@/views/SettingsView.vue'
import ContextView from '@/views/ContextView.vue'
+import BlocklistView from '@/views/BlocklistView.vue'
export default createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -19,6 +20,7 @@ export default createRouter({
{ path: '/bundles', component: BundlesView },
{ path: '/sources', component: SourcesView },
{ path: '/context', component: ContextView },
+ { path: '/blocklist', component: BlocklistView },
{ path: '/settings', component: SettingsView },
],
})
diff --git a/web/src/views/BlocklistView.vue b/web/src/views/BlocklistView.vue
new file mode 100644
index 0000000..ad23a36
--- /dev/null
+++ b/web/src/views/BlocklistView.vue
@@ -0,0 +1,333 @@
+
+
+
+
+
+
+
Blocklist
+
+ Review destinations flagged from router logs and push approved entries to Pi-hole.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading…
+
+
+
No pending candidates. Run a scan to find new destinations.
+
No candidates with status "{{ activeTab }}".
+
+
+
+
+
+
+ {{ group.name }} ({{ deviceIp }})
+
+
+
+
+ {{ c.domain_or_ip }}
+ {{ c.matched_rule }}
+ {{ c.hit_count }} hit{{ c.hit_count !== 1 ? 's' : '' }}
+ {{ shortDate(c.first_seen) }} → {{ shortDate(c.last_seen) }}
+
+
+
+
+
+
+
+
+
+ Approved
+
+
+
+
+
+ Blocked
+
+
+
+
+
+ {{ c.status }}
+
+
+
+
+
+
+
+
+
+
+
{{ errors[c.id] }}
+
+
+
+
+
+
+
{{ scanError }}
+
+
+
+
diff --git a/web/src/views/SettingsView.vue b/web/src/views/SettingsView.vue
index 99a8cd2..463152a 100644
--- a/web/src/views/SettingsView.vue
+++ b/web/src/views/SettingsView.vue
@@ -175,6 +175,84 @@
+
+
+
Pi-hole Blocklist
+
+ Push flagged IoT destinations to your Pi-hole instance.
+ Find your API key (v5) or app password (v6) in Pi-hole's admin settings.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ piholeStatus.msg }}
+
+
+
+
({ entry_point_style: 'topbar', llm_url: '', llm_model: '', llm_api_key: '', severity_overrides: [] })
+const prefs = ref({ entry_point_style: 'topbar', llm_url: '', llm_model: '', llm_api_key: '', severity_overrides: [], pihole_url: '', pihole_version: 'v6', pihole_api_key: '', router_source_ids: '', device_names: '' })
const saveStatus = ref<{ ok: boolean; msg: string } | null>(null)
const showAddOverride = ref(false)
const showApiKey = ref(false)
+const showPiholeKey = ref(false)
+const piholeStatus = ref<{ ok: boolean; msg: string } | null>(null)
const newRule = ref({ name: '', pattern: '', override_severity: 'WARN', enabled: true })
const entryPointBtnRefs = ref([])
@@ -304,4 +389,34 @@ async function addOverride() {
showAddOverride.value = false
await saveOverrides()
}
+
+async function savePihole() {
+ saveStatus.value = null
+ try {
+ await patch({
+ pihole_url: prefs.value.pihole_url,
+ pihole_version: prefs.value.pihole_version,
+ pihole_api_key: prefs.value.pihole_api_key,
+ router_source_ids: prefs.value.router_source_ids,
+ device_names: prefs.value.device_names,
+ })
+ saveStatus.value = { ok: true, msg: 'Pi-hole settings saved' }
+ setTimeout(() => { saveStatus.value = null }, 2000)
+ } catch {
+ saveStatus.value = { ok: false, msg: 'Save failed — check server connection' }
+ }
+}
+
+async function testPihole() {
+ piholeStatus.value = null
+ try {
+ const res = await fetch(`${BASE}/api/blocklist/test`, { method: 'POST' })
+ const data = await res.json()
+ piholeStatus.value = data.ok
+ ? { ok: true, msg: `Connected — Pi-hole ${data.version}, ${data.domain_count} domains` }
+ : { ok: false, msg: data.error ?? 'Connection failed' }
+ } catch {
+ piholeStatus.value = { ok: false, msg: 'Network error' }
+ }
+}