feat: initial commit for ipv64 DNS TUI client
- Implement modular Textual TUI (main, app, widgets, config, api_client) - Integrate IPv64.net REST API client using httpx - Support record listing, creation, in-place edit, and deletion - Add flexible token resolution (.env, env export, interactive prompt) - Add bilingual README (DE/EN) and project configuration
This commit is contained in:
1 parent
fc1f055b11
commit
d15c927f55
11 files changed
+603
-1
No files matched your search
@@ -1,2 +1,55 @@
|
||||
# ipv64-tui
|
||||
# IPv64 DNS TUI Client
|
||||
|
||||
[Deutsch](#deutsch) | [English](#english)
|
||||
|
||||
---
|
||||
|
||||
## Deutsch
|
||||
|
||||
Ein moderner Terminal-Client (TUI) zur Verwaltung von DNS-Records auf [IPv64.net](https://ipv64.net), entwickelt mit **Python**, **Textual** und **HTTPX**.
|
||||
|
||||
### Funktionen
|
||||
|
||||
- **Übersichtliche Oberfläche:** Anzeige aller Domains und DNS-Einträge in interaktiven Tabellen.
|
||||
- **Vollständige DNS-Verwaltung:** Anlegen, Bearbeiten und Löschen von Records (A, AAAA, CNAME, TXT, MX, NS, SRV).
|
||||
- **Flexibles Auth-Handling:** API-Key aus Umgebungsvariablen (`export`), `.env`-Datei oder interaktiver Terminalabfrage.
|
||||
- **Tastaturgesteuert:** Schnelle Navigation und Aktionen über Hotkeys.
|
||||
|
||||
### Voraussetzungen
|
||||
|
||||
- Python 3.10+
|
||||
- Abhängigkeiten:
|
||||
```bash
|
||||
pip install textual httpx python-dotenv
|
||||
```
|
||||
|
||||
### Konfiguration
|
||||
Optional kann eine .env-Datei im Stammverzeichnis angelegt werden:
|
||||
|
||||
```bash
|
||||
IPV64_API_KEY=dein_api_token_hier
|
||||
```
|
||||
Alternativ kann der Schlüssel vor dem Start exportiert werden
|
||||
|
||||
```bash
|
||||
export IPV64_API_KEY="dein_api_token_hier"
|
||||
```
|
||||
Wird kein Key gefunden (oder steht noch der Standard-Platzhalter in der .env), fragt das Tool beim Start interaktiv nach dem Token.
|
||||
|
||||
|
||||
### Starten
|
||||
|
||||
```bash
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
### Tastenkürzel
|
||||
|
||||
|Taste|Aktion|
|
||||
|------|------|
|
||||
|r|Domains & Records neu von IPv64 laden|
|
||||
|e / Enter|Ausgewählten Record ins Formular laden (Bearbeiten)|
|
||||
|d|Ausgewählten Record sofort löschen|
|
||||
|Tab / Shift+Tab|"Zwischen Feldern, Tabelle und Buttons wechseln"|
|
||||
|q|Anwendung beenden|
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+106
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""IPv64 DNS TUI Client
|
||||
Author: Pascal Bouquet <pascal@ptbos.de>
|
||||
License: GNU General Public License v3.0 (GPL-3.0)
|
||||
Year: 2026
|
||||
"""
|
||||
|
||||
import httpx
|
||||
|
||||
BASE_URL = "https://ipv64.net/api.php"
|
||||
|
||||
|
||||
class IPv64Client:
|
||||
def __init__(self, api_key: str):
|
||||
self.headers = {"Authorization": f"Bearer {api_key.strip()}"}
|
||||
|
||||
async def get_account_info(self) -> dict:
|
||||
"""[GET] get_account_info: Ruft Kontoinformationen und Limits ab."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(
|
||||
f"{BASE_URL}?get_account_info",
|
||||
headers=self.headers,
|
||||
timeout=10.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
async def get_domains(self) -> dict:
|
||||
"""[GET] get_domains: Liefert alle Domains und Records."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(
|
||||
f"{BASE_URL}?get_domains",
|
||||
headers=self.headers,
|
||||
timeout=10.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
async def add_domain(self, domain_name: str) -> dict:
|
||||
"""[POST] add_domain: Legt eine neue Domain an."""
|
||||
data = {"add_domain": domain_name}
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
BASE_URL,
|
||||
headers=self.headers,
|
||||
data=data,
|
||||
timeout=10.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
async def delete_domain(self, domain_name: str) -> dict:
|
||||
"""[DELETE] del_domain: Löscht Domain mit allen Einträgen."""
|
||||
data = {"del_domain": domain_name}
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.request(
|
||||
"DELETE",
|
||||
BASE_URL,
|
||||
headers=self.headers,
|
||||
data=data,
|
||||
timeout=10.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
async def add_record(
|
||||
self,
|
||||
domain: str,
|
||||
subdomain: str,
|
||||
typ: str,
|
||||
content: str,
|
||||
prio: str = "",
|
||||
) -> dict:
|
||||
"""[POST] add_record: DNS-Record anlegen."""
|
||||
data = {
|
||||
"add_record": domain,
|
||||
"praefix": subdomain,
|
||||
"type": typ.upper(),
|
||||
"content": content,
|
||||
}
|
||||
if prio and prio.strip() and prio.strip().lower() != "none":
|
||||
data["prio"] = prio.strip()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.post(
|
||||
BASE_URL,
|
||||
headers=self.headers,
|
||||
data=data,
|
||||
timeout=10.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
async def delete_record_by_id(self, record_id: int | str) -> dict:
|
||||
"""[DELETE] del_record per ID."""
|
||||
data = {"del_record": str(record_id)}
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.request(
|
||||
"DELETE",
|
||||
BASE_URL,
|
||||
headers=self.headers,
|
||||
data=data,
|
||||
timeout=10.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python3
|
||||
"""IPv64 DNS TUI Client
|
||||
Author: Pascal Bouquet <pascal@ptbos.de>
|
||||
License: GNU General Public License v3.0 (GPL-3.0)
|
||||
Year: 2026
|
||||
"""
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.widgets import (
|
||||
Button,
|
||||
DataTable,
|
||||
Footer,
|
||||
Header,
|
||||
Label,
|
||||
Select,
|
||||
Static,
|
||||
)
|
||||
|
||||
from api_client import IPv64Client
|
||||
from widgets import RecordForm
|
||||
|
||||
|
||||
class IPv64TUI(App):
|
||||
CSS = """
|
||||
Screen {
|
||||
layout: vertical;
|
||||
}
|
||||
#top-bar {
|
||||
height: 3;
|
||||
margin: 1 1 0 1;
|
||||
}
|
||||
#main-container {
|
||||
height: 1fr;
|
||||
margin: 0 1;
|
||||
}
|
||||
#left-pane {
|
||||
width: 30%;
|
||||
border: round $primary;
|
||||
padding: 0 1;
|
||||
}
|
||||
#right-pane {
|
||||
width: 70%;
|
||||
border: round $primary;
|
||||
}
|
||||
#form-pane {
|
||||
height: auto;
|
||||
border: round $accent;
|
||||
margin: 0 1 1 1;
|
||||
padding: 1;
|
||||
}
|
||||
.form-row {
|
||||
height: 3;
|
||||
align-vertical: middle;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
.form-row Input, .form-row Select {
|
||||
width: 1fr;
|
||||
}
|
||||
.form-label {
|
||||
width: 12;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
#status {
|
||||
height: 1;
|
||||
margin: 0 1;
|
||||
color: $text-muted;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
("r", "refresh_data", "Aktualisieren"),
|
||||
("e", "edit_selected", "Bearbeiten"),
|
||||
("d", "delete_selected", "Löschen"),
|
||||
("q", "quit", "Beenden"),
|
||||
]
|
||||
|
||||
def __init__(self, api_key: str):
|
||||
super().__init__()
|
||||
self.client = IPv64Client(api_key)
|
||||
self.domains_cache = {}
|
||||
self.current_domain = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header(show_clock=True)
|
||||
yield Static("IPv64.net DNS Manager", id="top-bar")
|
||||
|
||||
with Horizontal(id="main-container"):
|
||||
with Vertical(id="left-pane"):
|
||||
yield Label("Domain auswählen:")
|
||||
yield Select([], id="domain-select", prompt="Wähle Domain...")
|
||||
yield Button("Domains neu laden", id="btn-refresh", variant="primary")
|
||||
with Vertical(id="right-pane"):
|
||||
yield DataTable(id="records-table")
|
||||
|
||||
yield RecordForm()
|
||||
yield Static("Bereit.", id="status")
|
||||
yield Footer()
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
table = self.query_one("#records-table", DataTable)
|
||||
table.cursor_type = "row"
|
||||
table.add_columns("ID", "Typ", "Subdomain", "Inhalt", "Prio")
|
||||
await self.load_domains()
|
||||
|
||||
async def load_domains(self) -> None:
|
||||
self.set_status("Lade Domains von IPv64...")
|
||||
try:
|
||||
res = await self.client.get_domains()
|
||||
domains_sub = res.get("subdomains", {})
|
||||
self.domains_cache = domains_sub
|
||||
|
||||
options = [(dom, dom) for dom in domains_sub.keys()]
|
||||
dom_select = self.query_one("#domain-select", Select)
|
||||
|
||||
# Verhindert, dass während des Updates unerwünschte Events feuern
|
||||
with self.prevent(Select.Changed):
|
||||
dom_select.set_options(options)
|
||||
|
||||
# Vorherige Auswahl beibehalten oder Fallback auf erste Domain
|
||||
if self.current_domain and self.current_domain in self.domains_cache:
|
||||
dom_select.value = self.current_domain
|
||||
elif options:
|
||||
self.current_domain = options[0][0]
|
||||
dom_select.value = self.current_domain
|
||||
else:
|
||||
self.current_domain = None
|
||||
dom_select.value = Select.BLANK
|
||||
|
||||
if self.current_domain:
|
||||
self.update_table(self.current_domain)
|
||||
else:
|
||||
self.query_one("#records-table", DataTable).clear()
|
||||
|
||||
self.set_status(f"{len(options)} Domains geladen.")
|
||||
except Exception as e:
|
||||
self.set_status(f"Fehler: {e}")
|
||||
|
||||
def update_table(self, domain_name: str) -> None:
|
||||
table = self.query_one("#records-table", DataTable)
|
||||
table.clear()
|
||||
domain_data = self.domains_cache.get(domain_name, {})
|
||||
records = domain_data.get("records", [])
|
||||
|
||||
for rec in records:
|
||||
rec_id = str(rec.get("record_id", ""))
|
||||
table.add_row(
|
||||
rec_id,
|
||||
str(rec.get("type", "-")),
|
||||
str(rec.get("praefix", "@")),
|
||||
str(rec.get("content", "-")),
|
||||
str(rec.get("prio", "-")),
|
||||
key=rec_id,
|
||||
)
|
||||
|
||||
def on_select_changed(self, event: Select.Changed) -> None:
|
||||
if event.select.id == "domain-select":
|
||||
if event.value != Select.BLANK:
|
||||
self.current_domain = str(event.value)
|
||||
self.update_table(self.current_domain)
|
||||
# Formular zurücksetzen beim Domain-Wechsel
|
||||
self.query_one(RecordForm).reset_mode()
|
||||
|
||||
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
||||
"""Öffnet den Bearbeitungsmodus auch bei Doppel-/Enter-Klick auf eine Zeile."""
|
||||
self.action_edit_selected()
|
||||
|
||||
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||
if event.button.id == "btn-refresh":
|
||||
await self.load_domains()
|
||||
elif event.button.id == "btn-submit":
|
||||
await self.submit_record()
|
||||
elif event.button.id == "btn-cancel":
|
||||
self.query_one(RecordForm).reset_mode()
|
||||
self.set_status("Bearbeitung abgebrochen.")
|
||||
|
||||
def action_edit_selected(self) -> None:
|
||||
table = self.query_one("#records-table", DataTable)
|
||||
if table.row_count == 0 or table.cursor_row is None:
|
||||
self.set_status("Kein Eintrag zum Bearbeiten ausgewählt.")
|
||||
return
|
||||
|
||||
row_data = table.get_row_at(table.cursor_row)
|
||||
rec_id, typ, subdomain, content, prio = (
|
||||
str(row_data[0]),
|
||||
str(row_data[1]),
|
||||
str(row_data[2]),
|
||||
str(row_data[3]),
|
||||
str(row_data[4]),
|
||||
)
|
||||
|
||||
form = self.query_one(RecordForm)
|
||||
form.set_edit_mode(rec_id, subdomain, typ, content, prio)
|
||||
self.set_status(f"Bearbeite Record ID {rec_id}. Daten im Formular anpassen.")
|
||||
|
||||
async def submit_record(self) -> None:
|
||||
if not self.current_domain:
|
||||
self.set_status("Bitte zuerst eine Domain auswählen.")
|
||||
return
|
||||
|
||||
form = self.query_one(RecordForm)
|
||||
data = form.get_data()
|
||||
|
||||
if not data["content"]:
|
||||
self.set_status("Content darf nicht leer sein.")
|
||||
return
|
||||
|
||||
old_record_id = data["record_id"]
|
||||
|
||||
try:
|
||||
# Wenn wir im Bearbeitungsmodus sind: Alten Eintrag zuerst löschen
|
||||
if old_record_id:
|
||||
self.set_status(f"Aktualisiere Record {old_record_id}...")
|
||||
del_res = await self.client.delete_record_by_id(old_record_id)
|
||||
if del_res.get("status") not in ("success", None) and del_res.get("info") != "success":
|
||||
self.set_status(f"Fehler beim Ersetzen des Records: {del_res}")
|
||||
return
|
||||
else:
|
||||
self.set_status("Erstelle neuen Record...")
|
||||
|
||||
res = await self.client.add_record(
|
||||
self.current_domain,
|
||||
data["subdomain"],
|
||||
data["type"],
|
||||
data["content"],
|
||||
data["prio"],
|
||||
)
|
||||
|
||||
if res.get("status") == "success" or res.get("info") == "success":
|
||||
msg = "Record aktualisiert." if old_record_id else "Record angelegt."
|
||||
self.set_status(msg)
|
||||
form.reset_mode()
|
||||
await self.load_domains()
|
||||
else:
|
||||
self.set_status(f"API-Rückmeldung: {res}")
|
||||
except Exception as e:
|
||||
self.set_status(f"Fehler: {e}")
|
||||
|
||||
async def action_delete_selected(self) -> None:
|
||||
table = self.query_one("#records-table", DataTable)
|
||||
if table.row_count == 0 or table.cursor_row is None:
|
||||
self.set_status("Keine Einträge vorhanden.")
|
||||
return
|
||||
|
||||
current_row_idx = table.cursor_row
|
||||
row_data = table.get_row_at(current_row_idx)
|
||||
record_id = str(row_data[0])
|
||||
|
||||
if not record_id or record_id == "-":
|
||||
self.set_status("Keine gültige Record-ID gefunden.")
|
||||
return
|
||||
|
||||
self.set_status(f"Lösche Record ID {record_id}...")
|
||||
try:
|
||||
res = await self.client.delete_record_by_id(record_id)
|
||||
if res.get("status") == "success" or res.get("info") == "success":
|
||||
self.set_status(f"Record {record_id} gelöscht.")
|
||||
# Falls wir diesen Record gerade im Formular bearbeitet hatten: Formular leeren
|
||||
form = self.query_one(RecordForm)
|
||||
if form.edit_record_id == record_id:
|
||||
form.reset_mode()
|
||||
await self.load_domains()
|
||||
else:
|
||||
self.set_status(f"API-Rückmeldung: {res}")
|
||||
except Exception as e:
|
||||
self.set_status(f"Fehler beim Löschen: {e}")
|
||||
|
||||
def action_refresh_data(self) -> None:
|
||||
self.run_worker(self.load_domains())
|
||||
|
||||
def set_status(self, text: str) -> None:
|
||||
self.query_one("#status", Static).update(text)
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
"""IPv64 DNS TUI Client
|
||||
Author: Pascal Bouquet <pascal@ptbos.de>
|
||||
License: GNU General Public License v3.0 (GPL-3.0)
|
||||
Year: 2026
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
ENV_KEY_NAME = "IPV64_API_KEY"
|
||||
|
||||
# Standard-Platzhalter, die als "nicht konfiguriert" gewertet werden
|
||||
PLACEHOLDERS = {
|
||||
"",
|
||||
"dein_api_token_hier",
|
||||
"dein_api_key",
|
||||
"dein_geheimer_api_schluessel",
|
||||
"your_api_key_here",
|
||||
}
|
||||
|
||||
|
||||
def get_api_key() -> str:
|
||||
token = os.getenv(ENV_KEY_NAME, "").strip()
|
||||
|
||||
# Falls der Token leer ist oder noch ein Platzhalter drinsteht
|
||||
if token.lower() in PLACEHOLDERS:
|
||||
print("Kein gültiger API-Token gefunden (Platzhalter oder leer).")
|
||||
try:
|
||||
token = input("Bitte IPv64 API-Token eingeben: ").strip()
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
print("\nAbgebrochen.")
|
||||
sys.exit(0)
|
||||
|
||||
if not token or token.lower() in PLACEHOLDERS:
|
||||
print("Fehler: Ein gültiger Token ist erforderlich, um fortzufahren.")
|
||||
sys.exit(1)
|
||||
|
||||
return token
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""IPv64 DNS TUI Client
|
||||
Author: Pascal Bouquet <pascal@ptbos.de>
|
||||
License: GNU General Public License v3.0 (GPL-3.0)
|
||||
Year: 2026
|
||||
"""
|
||||
|
||||
from app import IPv64TUI
|
||||
from config import get_api_key
|
||||
|
||||
|
||||
def main():
|
||||
api_key = get_api_key()
|
||||
app = IPv64TUI(api_key=api_key)
|
||||
app.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3
|
||||
"""IPv64 DNS TUI Client
|
||||
Author: Pascal Bouquet <pascal@ptbos.de>
|
||||
License: GNU General Public License v3.0 (GPL-3.0)
|
||||
Year: 2026
|
||||
"""
|
||||
|
||||
from rich.markup import escape
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Horizontal, Vertical
|
||||
from textual.widgets import Button, Input, Label, Select, Static
|
||||
|
||||
|
||||
class RecordForm(Static):
|
||||
"""Eingabe- und Bearbeitungsformular für DNS-Records."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.edit_record_id: str | None = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="form-pane"):
|
||||
yield Label("Record hinzufügen:", id="form-title")
|
||||
with Horizontal(classes="form-row"):
|
||||
yield Label("Subdomain:", classes="form-label")
|
||||
yield Input(placeholder="z. B. www oder mx01", id="in-subdomain")
|
||||
yield Label("Typ:", classes="form-label")
|
||||
yield Select(
|
||||
[
|
||||
("A", "A"),
|
||||
("AAAA", "AAAA"),
|
||||
("CNAME", "CNAME"),
|
||||
("TXT", "TXT"),
|
||||
("MX", "MX"),
|
||||
("NS", "NS"),
|
||||
("SRV", "SRV"),
|
||||
],
|
||||
value="A",
|
||||
id="sel-type",
|
||||
)
|
||||
with Horizontal(classes="form-row"):
|
||||
yield Label("Content/IP:", classes="form-label")
|
||||
yield Input(placeholder="192.0.2.1 oder Ziel", id="in-content")
|
||||
yield Label("Prio:", classes="form-label")
|
||||
yield Input(placeholder="10 (nur MX/SRV)", id="in-prio")
|
||||
yield Button("Erstellen", id="btn-submit", variant="success")
|
||||
yield Button(
|
||||
"Abbrechen",
|
||||
id="btn-cancel",
|
||||
variant="default",
|
||||
classes="hidden",
|
||||
)
|
||||
|
||||
def set_edit_mode(
|
||||
self, record_id: str, subdomain: str, typ: str, content: str, prio: str
|
||||
) -> None:
|
||||
"""Schaltet das Formular in den Bearbeitungsmodus."""
|
||||
self.edit_record_id = record_id
|
||||
|
||||
# Runde Klammern und escape() verhindern den Rich-Markup-Error
|
||||
clean_id = escape(str(record_id))
|
||||
self.query_one("#form-title", Label).update(
|
||||
f"Record bearbeiten (ID: {clean_id}):"
|
||||
)
|
||||
|
||||
# Subdomain bereinigen: @ oder leer wird zu leerem Input
|
||||
self.query_one("#in-subdomain", Input).value = "" if subdomain in ("@", "") else subdomain
|
||||
|
||||
# Select-Wert für Typ setzen, falls in Optionen vorhanden
|
||||
sel_type = self.query_one("#sel-type", Select)
|
||||
if typ in [opt[1] for opt in sel_type._options]:
|
||||
sel_type.value = typ
|
||||
|
||||
self.query_one("#in-content", Input).value = "" if content == "-" else content
|
||||
self.query_one("#in-prio", Input).value = "" if prio in ("-", "None") else prio
|
||||
|
||||
btn_submit = self.query_one("#btn-submit", Button)
|
||||
btn_submit.label = "Speichern"
|
||||
btn_submit.variant = "warning"
|
||||
|
||||
self.query_one("#btn-cancel", Button).remove_class("hidden")
|
||||
|
||||
def reset_mode(self) -> None:
|
||||
"""Setzt das Formular wieder auf 'Neuer Record' zurück."""
|
||||
self.edit_record_id = None
|
||||
self.query_one("#form-title", Label).update("Record hinzufügen:")
|
||||
self.clear()
|
||||
|
||||
btn_submit = self.query_one("#btn-submit", Button)
|
||||
btn_submit.label = "Erstellen"
|
||||
btn_submit.variant = "success"
|
||||
|
||||
self.query_one("#btn-cancel", Button).add_class("hidden")
|
||||
|
||||
def get_data(self) -> dict:
|
||||
return {
|
||||
"record_id": self.edit_record_id,
|
||||
"subdomain": self.query_one("#in-subdomain", Input).value.strip(),
|
||||
"type": str(self.query_one("#sel-type", Select).value),
|
||||
"content": self.query_one("#in-content", Input).value.strip(),
|
||||
"prio": self.query_one("#in-prio", Input).value.strip(),
|
||||
}
|
||||
|
||||
def clear(self) -> None:
|
||||
self.query_one("#in-subdomain", Input).value = ""
|
||||
self.query_one("#in-content", Input).value = ""
|
||||
self.query_one("#in-prio", Input).value = ""
|
||||
Reference in new issue
Block a user