diff --git a/README.md b/README.md
index 87e6c56..8e04e41 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,19 @@
# nxdns
-A self-hosted DNS sinkhole for a household LAN, written in Zig 0.16. One
-static musl binary, SQLite for state, a Raspberry Pi 5 as the reference
-target. It answers your network's DNS, blocks what you tell it to, and shows
-you what asked for what.
+nxdns is a DNS sinkhole for your LAN. It blocks the names you do not want, and
+forwards the rest over an encrypted connection.
+
+```mermaid
+flowchart LR
+ devices["Your devices"] -- "DNS query" --> nxdns["nxdns"]
+ nxdns -- "answer" --> devices
+ nxdns -- "allowed" --> upstream["Upstream resolvers
DoH / DoT"]
+ upstream -- "answer" --> nxdns
+ nxdns -- "blocked" --> sink["0.0.0.0 / NXDOMAIN"]
+```
+
+One static Zig binary. SQLite holds the state and the query log, which the web
+UI, the REST API and `/metrics` read.
## Features
diff --git a/web/src/features/blocklists/BlocklistsPage.tsx b/web/src/features/blocklists/BlocklistsPage.tsx
index 13970a6..b194762 100644
--- a/web/src/features/blocklists/BlocklistsPage.tsx
+++ b/web/src/features/blocklists/BlocklistsPage.tsx
@@ -79,17 +79,6 @@ const styles = stylex.create({
dimWhenDisabled: {
opacity: { default: 1, ":disabled": 0.5 },
},
- srOnly: {
- position: "absolute",
- width: 1,
- height: 1,
- padding: 0,
- margin: -1,
- overflow: "hidden",
- clipPath: "inset(50%)",
- whiteSpace: "nowrap",
- borderWidth: 0,
- },
});
export default function BlocklistsPage() {
@@ -170,7 +159,7 @@ export default function BlocklistsPage() {
Skipped regex |
Last updated |
- Actions
+ Actions
|
diff --git a/web/src/features/clients/ClientsPage.tsx b/web/src/features/clients/ClientsPage.tsx
index 9b760b7..1fa5dd3 100644
--- a/web/src/features/clients/ClientsPage.tsx
+++ b/web/src/features/clients/ClientsPage.tsx
@@ -96,17 +96,6 @@ const styles = stylex.create({
* The accessible name of the actions column, kept out of the visual table
* without leaving the accessibility tree.
*/
- srOnly: {
- position: "absolute",
- width: 1,
- height: 1,
- padding: 0,
- margin: -1,
- overflow: "hidden",
- clipPath: "inset(50%)",
- whiteSpace: "nowrap",
- borderWidth: 0,
- },
});
export default function ClientsPage() {
@@ -138,7 +127,7 @@ export default function ClientsPage() {
First seen |
Last seen |
- Actions
+ Actions
|
diff --git a/web/src/features/local/RecordsTab.tsx b/web/src/features/local/RecordsTab.tsx
index 667095d..97cc588 100644
--- a/web/src/features/local/RecordsTab.tsx
+++ b/web/src/features/local/RecordsTab.tsx
@@ -91,17 +91,6 @@ const styles = stylex.create({
dimWhenDisabled: {
opacity: { default: 1, ":disabled": 0.5 },
},
- srOnly: {
- position: "absolute",
- width: 1,
- height: 1,
- padding: 0,
- margin: -1,
- overflow: "hidden",
- clipPath: "inset(50%)",
- whiteSpace: "nowrap",
- borderWidth: 0,
- },
});
function RecordForm({
@@ -268,7 +257,7 @@ export default function RecordsTab() {
TTL
- Actions
+ Actions
|
diff --git a/web/src/features/local/ZonesTab.tsx b/web/src/features/local/ZonesTab.tsx
index 94505b6..eefb36c 100644
--- a/web/src/features/local/ZonesTab.tsx
+++ b/web/src/features/local/ZonesTab.tsx
@@ -87,17 +87,6 @@ const styles = stylex.create({
dimWhenDisabled: {
opacity: { default: 1, ":disabled": 0.5 },
},
- srOnly: {
- position: "absolute",
- width: 1,
- height: 1,
- padding: 0,
- margin: -1,
- overflow: "hidden",
- clipPath: "inset(50%)",
- whiteSpace: "nowrap",
- borderWidth: 0,
- },
});
function ZoneForm({
@@ -232,7 +221,7 @@ export default function ZonesTab() {
Resolver
- Actions
+ Actions
|
diff --git a/web/src/features/rules/RulesPage.tsx b/web/src/features/rules/RulesPage.tsx
index e6ce456..6946610 100644
--- a/web/src/features/rules/RulesPage.tsx
+++ b/web/src/features/rules/RulesPage.tsx
@@ -78,17 +78,6 @@ const styles = stylex.create({
submitRow: {
display: "flex",
},
- srOnly: {
- position: "absolute",
- width: 1,
- height: 1,
- padding: 0,
- margin: -1,
- overflow: "hidden",
- clipPath: "inset(50%)",
- whiteSpace: "nowrap",
- borderWidth: 0,
- },
});
export default function RulesPage() {
@@ -137,7 +126,7 @@ export default function RulesPage() {
Group |
Created |
- Actions
+ Actions
|
diff --git a/web/src/features/upstreams/UpstreamsPage.tsx b/web/src/features/upstreams/UpstreamsPage.tsx
index ae4abba..4af34f1 100644
--- a/web/src/features/upstreams/UpstreamsPage.tsx
+++ b/web/src/features/upstreams/UpstreamsPage.tsx
@@ -53,17 +53,6 @@ const styles = stylex.create({
dimWhenDisabled: {
opacity: { default: 1, ":disabled": 0.5 },
},
- srOnly: {
- position: "absolute",
- width: 1,
- height: 1,
- padding: 0,
- margin: -1,
- overflow: "hidden",
- clipPath: "inset(50%)",
- whiteSpace: "nowrap",
- borderWidth: 0,
- },
});
export default function UpstreamsPage() {
@@ -127,7 +116,7 @@ export default function UpstreamsPage() {
Enabled |
TLS name |
- Actions
+ Actions
|
diff --git a/web/src/ui/styles.ts b/web/src/ui/styles.ts
index 6046d31..b48971a 100644
--- a/web/src/ui/styles.ts
+++ b/web/src/ui/styles.ts
@@ -151,6 +151,19 @@ export const styles = stylex.create({
color: colors.dangerText,
},
+ /** Visible to a screen reader only; the element keeps its place in the a11y tree. */
+ srOnly: {
+ position: "absolute",
+ width: 1,
+ height: 1,
+ padding: 0,
+ margin: -1,
+ overflow: "hidden",
+ clipPath: "inset(50%)",
+ whiteSpace: "nowrap",
+ borderWidth: 0,
+ },
+
th: {
borderBottomWidth: 1,
borderBottomStyle: "solid",