dev: automated commit - 2025-06-17 16:15:21

This commit is contained in:
Mariano Z. 2025-06-17 16:15:21 -03:00
parent 73e91db78b
commit 27775e6b29
16 changed files with 1860 additions and 1240 deletions

View file

@ -1,512 +0,0 @@
// Global variables
let isConfigured = false;
let currentIP = "";
let domainName = "mz.uy";
// Helper functions
function showToast(message, type = "success") {
const toastContainer = document.querySelector(".toast-container");
const toast = document.createElement("div");
toast.className = `toast align-items-center text-white bg-${type}`;
toast.setAttribute("role", "alert");
toast.setAttribute("aria-live", "assertive");
toast.setAttribute("aria-atomic", "true");
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
// Remove the toast after it's hidden
toast.addEventListener("hidden.bs.toast", () => {
toast.remove();
});
}
// Initialize the application
async function initApp() {
// Show loading indicator
document.getElementById("loading").style.display = "block";
document.getElementById("config-warning").style.display = "none";
document.getElementById("config-status").style.display = "none";
document.getElementById("dns-records-section").style.display = "none";
// Load configuration
await loadConfig();
// Load current IP
await loadCurrentIP();
// Load update frequencies for the dropdown
await loadUpdateFrequencies();
// If configured, load DNS records
if (isConfigured) {
await loadDNSRecords();
}
// Hide loading indicator
document.getElementById("loading").style.display = "none";
}
// Load configuration
async function loadConfig() {
try {
const response = await fetch("/api/config");
const data = await response.json();
isConfigured = data.is_configured;
domainName = data.domain;
if (isConfigured) {
document.getElementById("config-warning").style.display = "none";
document.getElementById("config-status").style.display = "block";
document.getElementById("dns-records-section").style.display = "block";
document.getElementById("zone-id").textContent = data.zone_id;
document.getElementById("domain-name").textContent = data.domain;
document.getElementById("domain-suffix").textContent = "." + data.domain;
document.getElementById("domain-input").value = data.domain;
document.getElementById("zone-id-input").value = data.zone_id;
// Set the update schedule display
let scheduleDisplay = "Manual updates only";
if (data.update_period) {
switch (data.update_period) {
case "*/5 * * * *":
scheduleDisplay = "Every 5 minutes";
break;
case "*/30 * * * *":
scheduleDisplay = "Every 30 minutes";
break;
case "0 * * * *":
scheduleDisplay = "Hourly";
break;
case "0 */6 * * *":
scheduleDisplay = "Every 6 hours";
break;
case "0 0 * * *":
scheduleDisplay = "Daily";
break;
default:
scheduleDisplay = data.update_period;
}
}
document.getElementById("update-schedule").textContent = scheduleDisplay;
} else {
document.getElementById("config-warning").style.display = "block";
document.getElementById("config-status").style.display = "none";
document.getElementById("dns-records-section").style.display = "none";
}
} catch (error) {
console.error("Failed to load configuration:", error);
showToast("Failed to load configuration: " + error.message, "danger");
}
}
// Load current IP
async function loadCurrentIP() {
try {
const response = await fetch("/api/current-ip");
const data = await response.json();
currentIP = data.ip;
document.getElementById("current-ip").textContent = currentIP;
} catch (error) {
console.error("Failed to load current IP:", error);
document.getElementById("current-ip").textContent = "Failed to load";
}
}
// Load update frequencies
async function loadUpdateFrequencies() {
try {
const response = await fetch("/api/update-frequencies");
const frequencies = await response.json();
const select = document.getElementById("update-period");
select.innerHTML = "";
frequencies.forEach((freq) => {
const option = document.createElement("option");
option.value = freq.value;
option.textContent = freq.label;
select.appendChild(option);
});
} catch (error) {
console.error("Failed to load update frequencies:", error);
}
}
// Load DNS records
async function loadDNSRecords() {
try {
const response = await fetch("/api/records");
if (!response.ok) {
throw new Error("Failed to fetch DNS records");
}
const records = await response.json();
const tbody = document.getElementById("dns-records");
tbody.innerHTML = "";
if (records.length === 0) {
const tr = document.createElement("tr");
tr.innerHTML =
'<td colspan="6" class="text-center">No DNS records found</td>';
tbody.appendChild(tr);
return;
}
records.forEach((record) => {
const tr = document.createElement("tr");
// Highlight records that match the current IP
const isCurrentIP = record.type === "A" && record.content === currentIP;
const ipBadge = isCurrentIP
? '<span class="badge bg-success update-badge">Current IP</span>'
: record.type === "A"
? '<span class="badge bg-warning update-badge">Outdated IP</span>'
: "";
tr.innerHTML = `
<td>${record.type}</td>
<td>${record.name}</td>
<td>${record.content} ${ipBadge}</td>
<td>${record.ttl === 1 ? "Auto" : record.ttl + "s"}</td>
<td>${record.proxied ? '<i class="bi bi-check-lg text-success"></i>' : ""}</td>
<td>
<button class="btn btn-sm btn-outline-primary me-1 edit-record" data-id="${record.id}">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger delete-record" data-id="${record.id}" data-name="${record.name}">
<i class="bi bi-trash"></i>
</button>
</td>
`;
tbody.appendChild(tr);
});
// Add event listeners for edit and delete buttons
document.querySelectorAll(".edit-record").forEach((button) => {
button.addEventListener("click", () => editRecord(button.dataset.id));
});
document.querySelectorAll(".delete-record").forEach((button) => {
button.addEventListener("click", () =>
deleteRecord(button.dataset.id, button.dataset.name),
);
});
} catch (error) {
console.error("Failed to load DNS records:", error);
showToast("Failed to load DNS records: " + error.message, "danger");
}
}
// Edit a DNS record
async function editRecord(id) {
try {
const response = await fetch("/api/records");
const records = await response.json();
const record = records.find((r) => r.id === id);
if (!record) {
showToast("Record not found", "danger");
return;
}
// Open the modal
const modal = new bootstrap.Modal(document.getElementById("recordModal"));
modal.show();
// Update modal title
document.getElementById("recordModalLabel").textContent = "Edit DNS Record";
// Fill the form
document.getElementById("record-id").value = record.id;
// Set the subdomain name without the domain suffix
let name = record.name;
if (name === domainName) {
name = "@";
} else if (name.endsWith("." + domainName)) {
name = name.substring(0, name.length - domainName.length - 1);
}
document.getElementById("record-name").value = name;
document.getElementById("record-type").value = record.type;
document.getElementById("record-content").value = record.content;
document.getElementById("record-ttl").value = record.ttl;
document.getElementById("record-proxied").checked = record.proxied;
document.getElementById("use-my-ip").checked = false;
// Show/hide the "Use my current IP" option based on record type
toggleMyIPOption();
} catch (error) {
console.error("Failed to load record:", error);
showToast("Failed to load record: " + error.message, "danger");
}
}
// Delete a DNS record
async function deleteRecord(id, name) {
if (!confirm(`Are you sure you want to delete the record for "${name}"?`)) {
return;
}
try {
const response = await fetch(`/api/records/${id}`, {
method: "DELETE",
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Failed to delete record");
}
showToast(`Record for "${name}" deleted successfully`);
await loadDNSRecords();
} catch (error) {
console.error("Failed to delete record:", error);
showToast("Failed to delete record: " + error.message, "danger");
}
}
// Toggle the "Use my current IP" option based on record type
function toggleMyIPOption() {
const recordType = document.getElementById("record-type").value;
const useMyIPGroup = document.getElementById("use-my-ip-group");
const contentGroup = document.getElementById("content-group");
if (recordType === "A") {
useMyIPGroup.style.display = "block";
// If the checkbox is checked, hide the content field
const useMyIP = document.getElementById("use-my-ip").checked;
contentGroup.style.display = useMyIP ? "none" : "block";
} else {
useMyIPGroup.style.display = "none";
contentGroup.style.display = "block";
}
}
// Event listeners
document.addEventListener("DOMContentLoaded", function () {
// Initialize the application
initApp();
// Refresh IP button
document
.getElementById("refresh-ip")
.addEventListener("click", async function () {
await loadCurrentIP();
showToast("Current IP refreshed");
});
// Save configuration button
document
.getElementById("save-config")
.addEventListener("click", async function () {
const apiToken = document.getElementById("api-token").value;
const zoneId = document.getElementById("zone-id-input").value;
const domain = document.getElementById("domain-input").value;
const updatePeriod = document.getElementById("update-period").value;
if (!apiToken || !zoneId || !domain) {
showToast("Please fill all required fields", "danger");
return;
}
try {
const response = await fetch("/api/config", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
api_token: apiToken,
zone_id: zoneId,
domain: domain,
update_period: updatePeriod,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Failed to save configuration");
}
// Hide the modal
const modal = bootstrap.Modal.getInstance(
document.getElementById("configModal"),
);
modal.hide();
showToast("Configuration saved successfully");
// Reload the app
initApp();
} catch (error) {
console.error("Failed to save configuration:", error);
showToast("Failed to save configuration: " + error.message, "danger");
}
});
// Record type change event
document
.getElementById("record-type")
.addEventListener("change", toggleMyIPOption);
// Use my IP checkbox change event
document.getElementById("use-my-ip").addEventListener("change", function () {
const contentGroup = document.getElementById("content-group");
contentGroup.style.display = this.checked ? "none" : "block";
if (this.checked) {
document.getElementById("record-content").value = currentIP;
}
});
// Save record button
document
.getElementById("save-record")
.addEventListener("click", async function () {
const id = document.getElementById("record-id").value;
let name = document.getElementById("record-name").value;
const type = document.getElementById("record-type").value;
const content = document.getElementById("record-content").value;
const ttl = parseInt(document.getElementById("record-ttl").value);
const proxied = document.getElementById("record-proxied").checked;
const useMyIP = document.getElementById("use-my-ip").checked;
// Validate the form
if (!name) {
showToast("Name is required", "danger");
return;
}
if (!useMyIP && !content) {
showToast("Content is required", "danger");
return;
}
// Prepare the record data
const recordData = {
name: name,
type: type,
content: useMyIP ? "" : content,
ttl: ttl,
proxied: proxied,
use_my_ip: useMyIP,
};
try {
let response;
if (id) {
// Update existing record
response = await fetch(`/api/records/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(recordData),
});
} else {
// Create new record
response = await fetch("/api/records", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(recordData),
});
}
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Failed to save record");
}
// Hide the modal
const modal = bootstrap.Modal.getInstance(
document.getElementById("recordModal"),
);
modal.hide();
showToast(
id ? "Record updated successfully" : "Record created successfully",
);
// Reset the form
document.getElementById("record-form").reset();
document.getElementById("record-id").value = "";
// Reload DNS records
await loadDNSRecords();
} catch (error) {
console.error("Failed to save record:", error);
showToast("Failed to save record: " + error.message, "danger");
}
});
// Update all records button
document
.getElementById("update-all-records")
.addEventListener("click", async function () {
if (
!confirm(
"Are you sure you want to update all A records to your current IP?",
)
) {
return;
}
try {
const response = await fetch("/api/update-all", {
method: "POST",
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Failed to update records");
}
showToast("All A records updated to current IP");
// Reload DNS records
await loadDNSRecords();
} catch (error) {
console.error("Failed to update records:", error);
showToast("Failed to update records: " + error.message, "danger");
}
});
// Reset record form when opening the modal
document
.getElementById("recordModal")
.addEventListener("show.bs.modal", function (event) {
const button = event.relatedTarget;
// If opening from the "Add Record" button, reset the form
if (button && button.textContent.trim().includes("Add Record")) {
document.getElementById("recordModalLabel").textContent =
"Add DNS Record";
document.getElementById("record-form").reset();
document.getElementById("record-id").value = "";
document.getElementById("record-type").value = "A";
// Show/hide the "Use my current IP" option based on record type
toggleMyIPOption();
}
});
});

View file

@ -10,23 +10,22 @@ ON CONFLICT DO UPDATE SET
domain = excluded.domain,
update_period = excluded.update_period;
-- name: DeleteAllConfig :exec
DELETE FROM config;
-- name: InsertConfig :exec
INSERT INTO config (api_token, zone_id, domain, update_period)
VALUES (?, ?, ?, ?);
-- name: InitSchema :exec
-- This query is used to ensure the schema is set up properly
CREATE TABLE IF NOT EXISTS config (
api_token TEXT,
zone_id TEXT,
api_token TEXT NOT NULL DEFAULT '',
zone_id TEXT NOT NULL DEFAULT '',
domain TEXT NOT NULL DEFAULT 'mz.uy',
update_period TEXT NOT NULL DEFAULT '0 */6 * * *'
);
CREATE TRIGGER IF NOT EXISTS enforce_single_config
BEFORE INSERT ON config
WHEN (SELECT COUNT(*) FROM config) > 0
BEGIN
SELECT RAISE(FAIL, 'Only one config record allowed');
END;
-- Insert default config if none exists
INSERT OR IGNORE INTO config (domain, update_period)
SELECT 'mz.uy', '0 */6 * * *'
INSERT OR IGNORE INTO config (api_token, zone_id, domain, update_period)
SELECT '', '', 'mz.uy', '0 */6 * * *'
WHERE NOT EXISTS (SELECT 1 FROM config);

View file

@ -9,6 +9,15 @@ import (
"context"
)
const deleteAllConfig = `-- name: DeleteAllConfig :exec
DELETE FROM config
`
func (q *Queries) DeleteAllConfig(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteAllConfig)
return err
}
const getConfig = `-- name: GetConfig :one
SELECT api_token, zone_id, domain, update_period FROM config LIMIT 1
`
@ -27,19 +36,40 @@ func (q *Queries) GetConfig(ctx context.Context) (Config, error) {
const initSchema = `-- name: InitSchema :exec
CREATE TABLE IF NOT EXISTS config (
api_token TEXT,
zone_id TEXT,
api_token TEXT NOT NULL DEFAULT '',
zone_id TEXT NOT NULL DEFAULT '',
domain TEXT NOT NULL DEFAULT 'mz.uy',
update_period TEXT NOT NULL DEFAULT '0 */6 * * *'
)
`
// This query is used to ensure the schema is set up properly
func (q *Queries) InitSchema(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, initSchema)
return err
}
const insertConfig = `-- name: InsertConfig :exec
INSERT INTO config (api_token, zone_id, domain, update_period)
VALUES (?, ?, ?, ?)
`
type InsertConfigParams struct {
ApiToken string `json:"api_token"`
ZoneID string `json:"zone_id"`
Domain string `json:"domain"`
UpdatePeriod string `json:"update_period"`
}
func (q *Queries) InsertConfig(ctx context.Context, arg InsertConfigParams) error {
_, err := q.db.ExecContext(ctx, insertConfig,
arg.ApiToken,
arg.ZoneID,
arg.Domain,
arg.UpdatePeriod,
)
return err
}
const upsertConfig = `-- name: UpsertConfig :exec
INSERT INTO config (api_token, zone_id, domain, update_period)
VALUES (?, ?, ?, ?)

50
go.mod
View file

@ -3,24 +3,72 @@ module ddns-manager
go 1.24.1
require (
github.com/a-h/templ v0.3.865
github.com/a-h/templ v0.3.898
github.com/cloudflare/cloudflare-go v0.115.0
github.com/davecgh/go-spew v1.1.1
github.com/labstack/echo/v4 v4.13.3
github.com/mattn/go-sqlite3 v1.14.28
github.com/robfig/cron/v3 v3.0.1
)
require (
cel.dev/expr v0.19.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/cubicdaiya/gonp v1.0.4 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/go-sql-driver/mysql v1.9.2 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/cel-go v0.24.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.4 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pganalyze/pg_query_go/v6 v6.1.0 // indirect
github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect
github.com/pingcap/log v1.1.0 // indirect
github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/riza-io/grpc-go v0.2.0 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/sqlc-dev/sqlc v1.29.0 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/grpc v1.71.1 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.62.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.9.1 // indirect
modernc.org/sqlite v1.37.0 // indirect
)
tool github.com/sqlc-dev/sqlc/cmd/sqlc

137
go.sum
View file

@ -1,16 +1,56 @@
github.com/a-h/templ v0.3.865 h1:nYn5EWm9EiXaDgWcMQaKiKvrydqgxDUtT1+4zU2C43A=
github.com/a-h/templ v0.3.865/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4=
cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s=
github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM=
github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws=
github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI=
github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
@ -22,28 +62,121 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pganalyze/pg_query_go/v6 v6.1.0 h1:jG5ZLhcVgL1FAw4C/0VNQaVmX1SUJx71wBGdtTtBvls=
github.com/pganalyze/pg_query_go/v6 v6.1.0/go.mod h1:nvTHIuoud6e1SfrUaFwHqT0i4b5Nr+1rPWVds3B5+50=
github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb h1:3pSi4EDG6hg0orE1ndHkXvX6Qdq2cZn8gAPir8ymKZk=
github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg=
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 h1:tdMsjOqUR7YXHoBitzdebTvOjs/swniBTOLy5XiMtuE=
github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86/go.mod h1:exzhVYca3WRtd6gclGNErRWb1qEgff3LYta0LvRmON4=
github.com/pingcap/log v1.1.0 h1:ELiPxACz7vdo1qAvvaWJg1NrYFoY6gqAh/+Uo6aXdD8=
github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4=
github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 h1:W3rpAI3bubR6VWOcwxDIG0Gz9G5rl5b3SL116T0vBt0=
github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0/go.mod h1:+8feuexTKcXHZF/dkDfvCwEyBAmgb4paFc3/WeYV2eE=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ=
github.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/sqlc-dev/sqlc v1.29.0 h1:HQctoD7y/i29Bao53qXO7CZ/BV9NcvpGpsJWvz9nKWs=
github.com/sqlc-dev/sqlc v1.29.0/go.mod h1:BavmYw11px5AdPOjAVHmb9fctP5A8GTziC38wBF9tp0=
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 h1:mJdDDPblDfPe7z7go8Dvv1AJQDI3eQ/5xith3q2mFlo=
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17IJ037caFp4jpCw/iQQ7/W74Sqpb1YuKJU6HTKfM=
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4=
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s=
modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g=
modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM=

514
main.go
View file

@ -6,8 +6,10 @@ import (
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
@ -18,7 +20,7 @@ import (
"github.com/cloudflare/cloudflare-go"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
_ "github.com/mattn/go-sqlite3" // SQLite driver
_ "github.com/mattn/go-sqlite3"
"github.com/robfig/cron/v3"
)
@ -30,54 +32,189 @@ var (
queries *db.Queries
)
// Simple validation
func validateDNSRecord(name, recordType, content string) error {
if strings.TrimSpace(name) == "" {
return fmt.Errorf("name is required")
}
if strings.TrimSpace(content) == "" {
return fmt.Errorf("content is required")
}
// Validate by type
switch recordType {
case "A":
if net.ParseIP(content) == nil {
return fmt.Errorf("invalid IP address")
}
case "CNAME":
if !regexp.MustCompile(`^[a-zA-Z0-9\-\.]+$`).MatchString(content) {
return fmt.Errorf("invalid domain name")
}
}
return nil
}
// Clean input sanitization
func sanitizeInput(input string) string {
return strings.TrimSpace(input)
}
// Enhanced error responses
func errorResponse(c echo.Context, message string) error {
c.Response().WriteHeader(http.StatusBadRequest)
return templates.Render(c.Response(), templates.ErrorNotification(message))
}
func successResponse(c echo.Context, message string) error {
return templates.Render(c.Response(), templates.SuccessNotification(message))
}
// Improved createDNSRecord
func createDNSRecord(zoneID, domain, name, recordType, content string, ttl int, proxied bool) error {
if api == nil {
return fmt.Errorf("cloudflare API not initialized")
}
// Validate input
if err := validateDNSRecord(name, recordType, content); err != nil {
return err
}
// Prepare full name
fullName := name
if name != "@" && !strings.HasSuffix(name, domain) {
fullName = name + "." + domain
}
if name == "@" {
fullName = domain
}
// Create record
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
rc := cloudflare.ZoneIdentifier(zoneID)
_, err := api.CreateDNSRecord(ctx, rc, cloudflare.CreateDNSRecordParams{
Type: recordType,
Name: fullName,
Content: content,
TTL: ttl,
Proxied: &proxied,
})
if err != nil {
// Simple error handling
if cfErr, ok := err.(*cloudflare.Error); ok {
switch cfErr.ErrorCodes[0] {
case 10000:
return fmt.Errorf("invalid API credentials")
case 81044:
return fmt.Errorf("record already exists")
default:
return fmt.Errorf("cloudflare error: %s", cfErr.ErrorMessages[0])
}
}
return fmt.Errorf("failed to create record: %w", err)
}
return nil
}
func updateDNSRecord(zoneID, id, name, recordType, content string, ttl int, proxied bool) error {
if api == nil {
return fmt.Errorf("cloudflare API not initialized")
}
if err := validateDNSRecord(name, recordType, content); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
rc := cloudflare.ZoneIdentifier(zoneID)
_, err := api.UpdateDNSRecord(ctx, rc, cloudflare.UpdateDNSRecordParams{
ID: id,
Type: recordType,
Name: name,
Content: content,
TTL: ttl,
Proxied: &proxied,
})
if err != nil {
if cfErr, ok := err.(*cloudflare.Error); ok {
return fmt.Errorf("cloudflare error: %s", cfErr.ErrorMessages[0])
}
return fmt.Errorf("failed to update record: %w", err)
}
return nil
}
func deleteDNSRecord(zoneID, id string) error {
if api == nil {
return fmt.Errorf("cloudflare API not initialized")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
rc := cloudflare.ZoneIdentifier(zoneID)
err := api.DeleteDNSRecord(ctx, rc, id)
if err != nil {
if cfErr, ok := err.(*cloudflare.Error); ok {
return fmt.Errorf("cloudflare error: %s", cfErr.ErrorMessages[0])
}
return fmt.Errorf("failed to delete record: %w", err)
}
return nil
}
func initDatabase() (*sql.DB, error) {
dbPath := os.Getenv("DB_PATH")
if dbPath == "" {
dbPath = "./ddns.db"
}
log.Printf("Using database path: %s", dbPath)
sqlDB, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
return nil, err
}
if err := db.InitSchema(sqlDB); err != nil {
return nil, fmt.Errorf("failed to initialize schema: %w", err)
return nil, err
}
queries = db.New(sqlDB)
return sqlDB, nil
}
func initCloudflare(apiToken, zoneID string) error {
if apiToken == "" || zoneID == "" {
func initCloudflare(apiToken string) error {
if apiToken == "" {
return nil
}
var err error
api, err = cloudflare.NewWithAPIToken(apiToken)
if err != nil {
return fmt.Errorf("failed to initialize Cloudflare API: %w", err)
}
return nil
return err
}
func getCurrentIP() (string, error) {
resp, err := http.Get("https://api.ipify.org")
if err != nil {
return "", fmt.Errorf("failed to get current IP: %w", err)
return "", err
}
defer resp.Body.Close()
ip, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read IP response: %w", err)
return "", err
}
return string(ip), nil
return strings.TrimSpace(string(ip)), nil
}
func getDNSRecords(zoneID string) ([]templates.DNSRecord, error) {
@ -85,12 +222,13 @@ func getDNSRecords(zoneID string) ([]templates.DNSRecord, error) {
return nil, fmt.Errorf("cloudflare API not initialized")
}
ctx := context.Background()
rc := cloudflare.ZoneIdentifier(zoneID)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
rc := cloudflare.ZoneIdentifier(zoneID)
recs, _, err := api.ListDNSRecords(ctx, rc, cloudflare.ListDNSRecordsParams{})
if err != nil {
return nil, fmt.Errorf("failed to get DNS records: %w", err)
return nil, err
}
var records []templates.DNSRecord
@ -109,117 +247,39 @@ func getDNSRecords(zoneID string) ([]templates.DNSRecord, error) {
return records, nil
}
func createDNSRecord(zoneID, domain, name, recordType, content string, ttl int, proxied bool) error {
if api == nil {
return fmt.Errorf("cloudflare API not initialized")
}
if !strings.HasSuffix(name, domain) && name != "@" {
name = name + "." + domain
}
if name == "@" {
name = domain
}
ctx := context.Background()
rc := cloudflare.ZoneIdentifier(zoneID)
_, err := api.CreateDNSRecord(ctx, rc, cloudflare.CreateDNSRecordParams{
Type: recordType,
Name: name,
Content: content,
TTL: ttl,
Proxied: &proxied,
})
if err != nil {
return fmt.Errorf("failed to create DNS record: %w", err)
}
return nil
}
func updateDNSRecord(zoneID, id, name, recordType, content string, ttl int, proxied bool) error {
if api == nil {
return fmt.Errorf("cloudflare API not initialized")
}
ctx := context.Background()
rc := cloudflare.ZoneIdentifier(zoneID)
_, err := api.UpdateDNSRecord(ctx, rc, cloudflare.UpdateDNSRecordParams{
ID: id,
Type: recordType,
Name: name,
Content: content,
TTL: ttl,
Proxied: &proxied,
})
if err != nil {
return fmt.Errorf("failed to update DNS record: %w", err)
}
return nil
}
func deleteDNSRecord(zoneID, id string) error {
if api == nil {
return fmt.Errorf("cloudflare API not initialized")
}
ctx := context.Background()
rc := cloudflare.ZoneIdentifier(zoneID)
err := api.DeleteDNSRecord(ctx, rc, id)
if err != nil {
return fmt.Errorf("failed to delete DNS record: %w", err)
}
return nil
}
func updateAllRecordsWithCurrentIP(zoneID string) error {
if api == nil {
return fmt.Errorf("cloudflare API not initialized")
}
currentIP, err := getCurrentIP()
if err != nil {
return err
}
if currentIP == lastIP {
log.Println("IP hasn't changed, no updates needed")
return nil
}
lastIP = currentIP
ctx := context.Background()
rc := cloudflare.ZoneIdentifier(zoneID)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
records, _, err := api.ListDNSRecords(ctx, rc, cloudflare.ListDNSRecordsParams{
Type: "A",
})
rc := cloudflare.ZoneIdentifier(zoneID)
records, _, err := api.ListDNSRecords(ctx, rc, cloudflare.ListDNSRecordsParams{Type: "A"})
if err != nil {
return fmt.Errorf("failed to get DNS records: %w", err)
return err
}
for _, rec := range records {
if rec.Content != currentIP {
proxied := rec.Proxied
_, err := api.UpdateDNSRecord(ctx, rc, cloudflare.UpdateDNSRecordParams{
ID: rec.ID,
Type: rec.Type,
Name: rec.Name,
Content: currentIP,
TTL: rec.TTL,
Proxied: proxied,
Proxied: rec.Proxied,
})
if err != nil {
log.Printf("Failed to update record %s: %v", rec.Name, err)
} else {
log.Printf("Updated record %s to %s", rec.Name, currentIP)
}
}
}
@ -230,26 +290,22 @@ func updateAllRecordsWithCurrentIP(zoneID string) error {
func scheduleUpdates(zoneID, updatePeriod string) error {
if jobID != 0 {
scheduler.Remove(jobID)
log.Println("Scheduled update removed")
}
if updatePeriod == "" {
log.Println("Automatic updates disabled")
return nil
}
var err error
jobID, err = scheduler.AddFunc(updatePeriod, func() {
log.Println("Running scheduled IP update")
if err := updateAllRecordsWithCurrentIP(zoneID); err != nil {
log.Printf("Scheduled update failed: %v", err)
}
log.Println("Scheduled update completed")
})
if err != nil {
return fmt.Errorf("failed to schedule updates: %w", err)
}
log.Printf("Scheduled IP updates with cron: %s", updatePeriod)
return nil
return err
}
func getUpdateFrequencies() []templates.UpdateFrequency {
@ -265,44 +321,42 @@ func getUpdateFrequencies() []templates.UpdateFrequency {
}
func main() {
// Initialize database
sqlDB, err := initDatabase()
if err != nil {
log.Fatalf("Failed to initialize database: %v", err)
log.Fatalf("Database init failed: %v", err)
}
defer sqlDB.Close()
// Load config
config, err := queries.GetConfig(context.Background())
if err != nil {
log.Printf("Warning: Failed to load configuration: %v", err)
config = db.Config{
Domain: "mz.uy",
UpdatePeriod: "0 */6 * * *",
}
config = db.Config{Domain: "example.com", UpdatePeriod: "0 */6 * * *"}
}
if err := initCloudflare(config.ApiToken, config.ZoneID); err != nil {
log.Printf("Warning: Cloudflare initialization failed: %v", err)
// Initialize Cloudflare
if err := initCloudflare(config.ApiToken); err != nil {
log.Printf("Cloudflare init failed: %v", err)
}
// Initialize scheduler
scheduler = cron.New()
scheduler.Start()
defer scheduler.Stop()
if config.ApiToken != "" && config.ZoneID != "" && config.UpdatePeriod != "" {
if err := scheduleUpdates(config.ZoneID, config.UpdatePeriod); err != nil {
log.Printf("Warning: Failed to schedule updates: %v", err)
}
scheduleUpdates(config.ZoneID, config.UpdatePeriod)
}
// Setup Echo
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Static("/assets", "assets")
e.Use(middleware.CORS())
// Main page
// Routes
e.GET("/", func(c echo.Context) error {
currentIP, _ := getCurrentIP()
var records []templates.DNSRecord
isConfigured := config.ApiToken != "" && config.ZoneID != ""
@ -310,8 +364,8 @@ func main() {
records, _ = getDNSRecords(config.ZoneID)
}
component := templates.Index(templates.IndexProps{
Title: "mz.uy DNS Manager",
return templates.Render(c.Response(), templates.Index(templates.IndexProps{
Title: "DNS Manager",
IsConfigured: isConfigured,
CurrentIP: currentIP,
Config: templates.ConfigData{
@ -322,145 +376,126 @@ func main() {
},
Records: records,
UpdateFreqs: getUpdateFrequencies(),
})
return templates.Render(c.Response(), component)
}))
})
// Refresh current IP
e.GET("/refresh-ip", func(c echo.Context) error {
ip, err := getCurrentIP()
if err != nil {
c.Response().Header().Set("HX-Error-Message", "Failed to get current IP")
return c.String(http.StatusInternalServerError, "Error")
return errorResponse(c, "Failed to get current IP")
}
return c.String(http.StatusOK, ip)
return c.HTML(http.StatusOK, fmt.Sprintf(`<span id="current-ip" class="fw-bold">%s</span>`, ip))
})
// Configuration
e.POST("/config", func(c echo.Context) error {
apiToken := c.FormValue("api_token")
zoneID := c.FormValue("zone_id")
domain := c.FormValue("domain")
updatePeriod := c.FormValue("update_period")
apiToken := sanitizeInput(c.FormValue("api_token"))
zoneID := sanitizeInput(c.FormValue("zone_id"))
domain := sanitizeInput(c.FormValue("domain"))
updatePeriod := sanitizeInput(c.FormValue("update_period"))
if apiToken == "" || zoneID == "" || domain == "" {
c.Response().Header().Set("HX-Error-Message", "Please fill all required fields")
return c.String(http.StatusBadRequest, "Invalid input")
return errorResponse(c, "Please fill all required fields")
}
err := queries.UpsertConfig(context.Background(), db.UpsertConfigParams{
// Save config
queries.DeleteAllConfig(context.Background())
err := queries.InsertConfig(context.Background(), db.InsertConfigParams{
ApiToken: apiToken,
ZoneID: zoneID,
Domain: domain,
UpdatePeriod: updatePeriod,
})
if err != nil {
c.Response().Header().Set("HX-Error-Message", "Failed to save configuration")
return c.String(http.StatusInternalServerError, "Database error")
return errorResponse(c, "Failed to save configuration")
}
// Update global config
config.ApiToken = apiToken
config.ZoneID = zoneID
config.Domain = domain
config.UpdatePeriod = updatePeriod
if err := initCloudflare(config.ApiToken, config.ZoneID); err != nil {
c.Response().Header().Set("HX-Error-Message", "Failed to initialize Cloudflare client")
return c.String(http.StatusInternalServerError, "API error")
}
// Reinitialize Cloudflare
initCloudflare(apiToken)
scheduleUpdates(zoneID, updatePeriod)
if err := scheduleUpdates(config.ZoneID, config.UpdatePeriod); err != nil {
c.Response().Header().Set("HX-Error-Message", "Failed to schedule updates")
return c.String(http.StatusInternalServerError, "Scheduler error")
}
c.Response().Header().Set("HX-Success-Message", "Configuration saved successfully")
return c.Redirect(http.StatusSeeOther, "/")
return templates.Render(c.Response(), templates.ConfigStatus(templates.ConfigData{
ZoneID: zoneID,
Domain: domain,
UpdatePeriod: updatePeriod,
ApiToken: apiToken,
}))
})
// Create DNS record
e.POST("/records", func(c echo.Context) error {
if config.ApiToken == "" || config.ZoneID == "" {
c.Response().Header().Set("HX-Error-Message", "API not configured")
return c.String(http.StatusBadRequest, "Not configured")
}
e.GET("/config", func(c echo.Context) error {
return templates.Render(c.Response(), templates.ConfigModal(templates.ConfigData{
ZoneID: config.ZoneID,
Domain: config.Domain,
UpdatePeriod: config.UpdatePeriod,
ApiToken: config.ApiToken,
}, getUpdateFrequencies()))
})
name := c.FormValue("name")
recordType := c.FormValue("type")
content := c.FormValue("content")
ttlStr := c.FormValue("ttl")
e.GET("/records/new", func(c echo.Context) error {
return templates.Render(c.Response(), templates.RecordForm("Add DNS Record", "", config.Domain, templates.DNSRecord{Type: "A", TTL: 1}))
})
e.POST("/records", func(c echo.Context) error {
name := sanitizeInput(c.FormValue("name"))
recordType := sanitizeInput(c.FormValue("type"))
content := sanitizeInput(c.FormValue("content"))
ttlStr := sanitizeInput(c.FormValue("ttl"))
proxied := c.FormValue("proxied") == "on"
useMyIP := c.FormValue("use_my_ip") == "on"
if name == "" {
c.Response().Header().Set("HX-Error-Message", "Name is required")
return c.String(http.StatusBadRequest, "Invalid input")
}
ttl, err := strconv.Atoi(ttlStr)
if err != nil {
ttl = 1
}
if useMyIP {
currentIP, err := getCurrentIP()
if err != nil {
c.Response().Header().Set("HX-Error-Message", "Failed to get current IP")
return c.String(http.StatusInternalServerError, "IP error")
return errorResponse(c, "Failed to get current IP")
}
content = currentIP
}
if content == "" {
c.Response().Header().Set("HX-Error-Message", "Content is required")
return c.String(http.StatusBadRequest, "Invalid input")
ttl, _ := strconv.Atoi(ttlStr)
if ttl == 0 {
ttl = 1
}
err = createDNSRecord(config.ZoneID, config.Domain, name, recordType, content, ttl, proxied)
if err != nil {
c.Response().Header().Set("HX-Error-Message", "Failed to create DNS record")
return c.String(http.StatusInternalServerError, "DNS error")
if err := createDNSRecord(config.ZoneID, config.Domain, name, recordType, content, ttl, proxied); err != nil {
return errorResponse(c, err.Error())
}
c.Response().Header().Set("HX-Success-Message", "DNS record created successfully")
// Return updated records table
// Return updated table
records, _ := getDNSRecords(config.ZoneID)
currentIP, _ := getCurrentIP()
component := templates.DNSRecordsTable(records, currentIP)
return templates.Render(c.Response(), component)
notification := templates.SuccessNotification("DNS record created")
table := templates.DNSRecordsTable(records, currentIP)
return templates.RenderMultiple(c.Response().Writer, notification, table)
})
// Update DNS record
e.PUT("/records/:id", func(c echo.Context) error {
if config.ApiToken == "" || config.ZoneID == "" {
c.Response().Header().Set("HX-Error-Message", "API not configured")
return c.String(http.StatusBadRequest, "Not configured")
}
id := c.Param("id")
name := c.FormValue("name")
recordType := c.FormValue("type")
content := c.FormValue("content")
ttlStr := c.FormValue("ttl")
name := sanitizeInput(c.FormValue("name"))
recordType := sanitizeInput(c.FormValue("type"))
content := sanitizeInput(c.FormValue("content"))
ttlStr := sanitizeInput(c.FormValue("ttl"))
proxied := c.FormValue("proxied") == "on"
useMyIP := c.FormValue("use_my_ip") == "on"
ttl, err := strconv.Atoi(ttlStr)
if err != nil {
ttl = 1
}
if useMyIP {
currentIP, err := getCurrentIP()
if err != nil {
c.Response().Header().Set("HX-Error-Message", "Failed to get current IP")
return c.String(http.StatusInternalServerError, "IP error")
return errorResponse(c, "Failed to get current IP")
}
content = currentIP
}
// Convert name to full domain name if needed
ttl, _ := strconv.Atoi(ttlStr)
if ttl == 0 {
ttl = 1
}
// Convert name to full domain
fullName := name
if name != "@" && !strings.HasSuffix(name, config.Domain) {
fullName = name + "." + config.Domain
@ -469,51 +504,36 @@ func main() {
fullName = config.Domain
}
err = updateDNSRecord(config.ZoneID, id, fullName, recordType, content, ttl, proxied)
if err != nil {
c.Response().Header().Set("HX-Error-Message", "Failed to update DNS record")
return c.String(http.StatusInternalServerError, "DNS error")
if err := updateDNSRecord(config.ZoneID, id, fullName, recordType, content, ttl, proxied); err != nil {
return errorResponse(c, err.Error())
}
c.Response().Header().Set("HX-Success-Message", "DNS record updated successfully")
// Return updated records table
records, _ := getDNSRecords(config.ZoneID)
currentIP, _ := getCurrentIP()
component := templates.DNSRecordsTable(records, currentIP)
return templates.Render(c.Response(), component)
notification := templates.SuccessNotification("DNS record updated")
table := templates.DNSRecordsTable(records, currentIP)
return templates.RenderMultiple(c.Response().Writer, notification, table)
})
// Delete DNS record
e.DELETE("/records/:id", func(c echo.Context) error {
if config.ApiToken == "" || config.ZoneID == "" {
c.Response().Header().Set("HX-Error-Message", "API not configured")
return c.String(http.StatusBadRequest, "Not configured")
}
id := c.Param("id")
err := deleteDNSRecord(config.ZoneID, id)
if err != nil {
c.Response().Header().Set("HX-Error-Message", "Failed to delete DNS record")
return c.String(http.StatusInternalServerError, "DNS error")
if err := deleteDNSRecord(config.ZoneID, id); err != nil {
return errorResponse(c, "Failed to delete record")
}
c.Response().Header().Set("HX-Success-Message", "DNS record deleted successfully")
return c.String(http.StatusOK, "")
records, _ := getDNSRecords(config.ZoneID)
currentIP, _ := getCurrentIP()
notification := templates.SuccessNotification("DNS record deleted")
table := templates.DNSRecordsTable(records, currentIP)
return templates.RenderMultiple(c.Response().Writer, notification, table)
})
// Edit record form
e.GET("/edit-record/:id", func(c echo.Context) error {
if config.ApiToken == "" || config.ZoneID == "" {
c.Response().Header().Set("HX-Error-Message", "API not configured")
return c.String(http.StatusBadRequest, "Not configured")
}
id := c.Param("id")
records, err := getDNSRecords(config.ZoneID)
if err != nil {
c.Response().Header().Set("HX-Error-Message", "Failed to load DNS records")
return c.String(http.StatusInternalServerError, "DNS error")
return errorResponse(c, "Failed to load records")
}
var record templates.DNSRecord
@ -525,36 +545,24 @@ func main() {
}
if record.ID == "" {
c.Response().Header().Set("HX-Error-Message", "Record not found")
return c.String(http.StatusNotFound, "Not found")
return errorResponse(c, "Record not found")
}
component := templates.RecordForm("Edit DNS Record", id, config.Domain, record)
return templates.Render(c.Response(), component)
return templates.Render(c.Response(), templates.RecordForm("Edit DNS Record", id, config.Domain, record))
})
// Update all records with current IP
e.POST("/update-all-records", func(c echo.Context) error {
if config.ApiToken == "" || config.ZoneID == "" {
c.Response().Header().Set("HX-Error-Message", "API not configured")
return c.String(http.StatusBadRequest, "Not configured")
if err := updateAllRecordsWithCurrentIP(config.ZoneID); err != nil {
return errorResponse(c, "Failed to update records")
}
err := updateAllRecordsWithCurrentIP(config.ZoneID)
if err != nil {
c.Response().Header().Set("HX-Error-Message", "Failed to update records")
return c.String(http.StatusInternalServerError, "Update error")
}
c.Response().Header().Set("HX-Success-Message", "All A records updated with current IP")
// Return updated records table
records, _ := getDNSRecords(config.ZoneID)
currentIP, _ := getCurrentIP()
component := templates.DNSRecordsTable(records, currentIP)
return templates.Render(c.Response(), component)
notification := templates.SuccessNotification("All A records updated")
table := templates.DNSRecordsTable(records, currentIP)
return templates.RenderMultiple(c.Response().Writer, notification, table)
})
log.Println("Starting server on http://localhost:3000")
log.Println("Starting server on :3000")
log.Fatal(e.Start(":3000"))
}

34
templates/alert.templ Normal file
View file

@ -0,0 +1,34 @@
package templates
templ Alert(id, alertType, message string, dismissible bool) {
<div
id={ id }
class={ "alert", "alert-" + alertType, templ.KV("alert-dismissible", dismissible), templ.KV("d-none", message == "") }
role="alert"
>
if message != "" {
{ message }
}
if dismissible {
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
}
</div>
}
// Helper components for common alert types
templ ErrorAlert(id, message string) {
@Alert(id, "danger", message, false)
}
templ SuccessAlert(id, message string) {
@Alert(id, "success", message, true)
}
templ WarningAlert(id, message string) {
@Alert(id, "warning", message, true)
}
templ InfoAlert(id, message string) {
@Alert(id, "info", message, true)
}

214
templates/alert_templ.go Normal file
View file

@ -0,0 +1,214 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
func Alert(id, alertType, message string, dismissible bool) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
var templ_7745c5c3_Var2 = []any{"alert", "alert-" + alertType, templ.KV("alert-dismissible", dismissible), templ.KV("d-none", message == "")}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(id)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/alert.templ`, Line: 6, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/alert.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" role=\"alert\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if message != "" {
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(message)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/alert.templ`, Line: 11, Col: 12}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if dismissible {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"alert\" aria-label=\"Close\"></button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Helper components for common alert types
func ErrorAlert(id, message string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Alert(id, "danger", message, false).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func SuccessAlert(id, message string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Alert(id, "success", message, true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func WarningAlert(id, message string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Alert(id, "warning", message, true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func InfoAlert(id, message string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var9 := templ.GetChildren(ctx)
if templ_7745c5c3_Var9 == nil {
templ_7745c5c3_Var9 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Alert(id, "info", message, true).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate

View file

@ -43,17 +43,16 @@ templ Index(props IndexProps) {
<h1>{ props.Title }</h1>
<div class="current-ip d-flex align-items-center">
<span class="me-2">Current IP:</span>
<span id="current-ip" class="fw-bold">{ props.CurrentIP }</span>
<button
class="btn btn-sm btn-outline-secondary ms-2"
title="Refresh current IP"
hx-get="/refresh-ip"
hx-target="#current-ip"
hx-indicator="#refresh-spinner"
<span id="current-ip" x-init class="fw-bold">{ props.CurrentIP }</span>
<a
href="/refresh-ip"
x-target="current-ip"
class="btn btn-sm btn-outline-secondary ms-2 d-inline-flex align-items-center"
@ajax:before="$el.querySelector('.bi-arrow-clockwise').classList.add('spin')"
@ajax:after="$el.querySelector('.bi-arrow-clockwise').classList.remove('spin')"
>
<i class="bi bi-arrow-clockwise"></i>
<span id="refresh-spinner" class="htmx-indicator spinner-border spinner-border-sm ms-1"></span>
</button>
</a>
</div>
</div>
if !props.IsConfigured {
@ -62,12 +61,9 @@ templ Index(props IndexProps) {
@ConfigStatus(props.Config)
@DNSRecordsSection(props.Records, props.CurrentIP)
}
@ConfigModal(props.Config, props.UpdateFreqs)
@RecordModal(props.Config.Domain)
</div>
</div>
</div>
<div class="toast-container"></div>
}
}
@ -86,16 +82,34 @@ templ ConfigWarning() {
}
templ ConfigStatus(config ConfigData) {
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Configuration</h5>
<button
class="btn btn-sm btn-outline-primary"
data-bs-toggle="modal"
data-bs-target="#configModal"
<div class="card mb-4" id="config-status">
<div
class="card-header d-flex justify-content-between align-items-center"
x-init
x-data="{ editLoading: false, deleteLoading: false }"
@ajax:success="$dispatch('dialog:open')"
>
<h5 class="mb-0">Configuration</h5>
<a
href="/config"
@ajax:before="editLoading = true"
@ajax:success="$dispatch('dialog:open')"
@ajax:after="editLoading = false"
@ajax:error="editLoading = false"
x-target="contact"
class="btn btn-sm btn-outline-primary me-2"
:disabled="editLoading || deleteLoading"
>
<template x-if="!editLoading">
<span class="ms-1">
Edit
</button>
<i class="bi bi-pencil"></i>
</span>
</template>
<template x-if="editLoading">
<span class="spinner-border spinner-border-sm"></span>
</template>
</a>
</div>
<div class="card-body">
<div class="row">
@ -118,25 +132,57 @@ templ DNSRecordsSection(records []DNSRecord, currentIP string) {
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">DNS Records</h5>
<div>
<div x-data="{ updating: false, addingRecord: false }">
<form
method="post"
action="/update-all-records"
x-target="dns-records-table"
@ajax:before="confirm('Are you sure you want to update all A records to your current IP?') || $event.preventDefault(); updating = true"
@ajax:after="updating = false"
@ajax:error="updating = false"
style="display: inline;"
>
<button
class="btn btn-sm btn-success me-2"
hx-post="/update-all-records"
hx-target="#dns-records-table"
hx-confirm="Are you sure you want to update all A records to your current IP?"
hx-indicator="#update-all-spinner"
type="submit"
:disabled="updating || addingRecord"
>
<i class="bi bi-arrow-repeat"></i> Update All to Current IP
<span id="update-all-spinner" class="htmx-indicator spinner-border spinner-border-sm ms-1"></span>
<template x-if="!updating">
<span class="d-flex align-items-center">
<i class="bi bi-arrow-repeat me-1"></i>
Update All to Current IP
</span>
</template>
<template x-if="updating">
<span class="d-flex align-items-center">
<span class="spinner-border spinner-border-sm me-2"></span>
Updating All Records...
</span>
</template>
</button>
<button
class="btn btn-sm btn-primary"
data-bs-toggle="modal"
data-bs-target="#recordModal"
onclick="resetRecordForm()"
</form>
<a
href="/records/new"
x-target="contact"
@ajax:before="addingRecord = true; $dispatch('dialog:open')"
@ajax:after="addingRecord = false"
@ajax:error="addingRecord = false"
class="btn btn-primary"
:disabled="updating || addingRecord"
>
<i class="bi bi-plus-lg"></i> Add Record
</button>
<template x-if="!addingRecord">
<span class="d-flex align-items-center">
<i class="bi bi-plus-lg me-1"></i>
Add Record
</span>
</template>
<template x-if="addingRecord">
<span class="d-flex align-items-center">
<span class="spinner-border spinner-border-sm me-2"></span>
Loading...
</span>
</template>
</a>
</div>
</div>
<div class="card-body p-0">
@ -174,7 +220,7 @@ templ DNSRecordsTable(records []DNSRecord, currentIP string) {
}
templ DNSRecordRow(record DNSRecord, currentIP string) {
<tr>
<tr x-data="{ editLoading: false, deleteLoading: false }">
<td>{ record.Type }</td>
<td>{ record.Name }</td>
<td>
@ -200,23 +246,46 @@ templ DNSRecordRow(record DNSRecord, currentIP string) {
}
</td>
<td>
<button
class="btn btn-sm btn-outline-primary me-1"
hx-get={ fmt.Sprintf("/edit-record/%s", record.ID) }
hx-target="#record-modal-content"
hx-on::after-request="if(event.detail.successful) bootstrap.Modal.getOrCreateInstance(document.getElementById('recordModal')).show()"
<a
href={ templ.URL(fmt.Sprintf("/edit-record/%s", record.ID)) }
@ajax:before="editLoading = true"
@ajax:success="$dispatch('dialog:open')"
@ajax:after="editLoading = false"
@ajax:error="editLoading = false"
x-target="contact"
class="btn btn-sm btn-outline-primary me-2"
:disabled="editLoading || deleteLoading"
>
<template x-if="!editLoading">
<i class="bi bi-pencil"></i>
</button>
</template>
<template x-if="editLoading">
<span class="spinner-border spinner-border-sm"></span>
</template>
</a>
<form
method="delete"
action={ templ.URL(fmt.Sprintf("/records/%s", record.ID)) }
x-target="closest tr"
@ajax:before={ fmt.Sprintf(`confirm('Are you sure you want to delete the record for "%s"?') || $event.preventDefault(); deleteLoading = true`, record.Name) }
@ajax:after="deleteLoading = false"
@ajax:error="deleteLoading = false"
@ajax:success="$el.closest('tr').remove()"
style="display: inline;"
>
<button
class="btn btn-sm btn-outline-danger"
hx-delete={ fmt.Sprintf("/records/%s", record.ID) }
hx-target="closest tr"
hx-swap="outerHTML"
hx-confirm={ fmt.Sprintf("Are you sure you want to delete the record for \"%s\"?", record.Name) }
type="submit"
:disabled="editLoading || deleteLoading"
>
<template x-if="!deleteLoading">
<i class="bi bi-trash"></i>
</template>
<template x-if="deleteLoading">
<span class="spinner-border spinner-border-sm"></span>
</template>
</button>
</form>
</td>
</tr>
}

View file

@ -88,20 +88,20 @@ func Index(props IndexProps) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h1><div class=\"current-ip d-flex align-items-center\"><span class=\"me-2\">Current IP:</span> <span id=\"current-ip\" class=\"fw-bold\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h1><div class=\"current-ip d-flex align-items-center\"><span class=\"me-2\">Current IP:</span> <span id=\"current-ip\" x-init class=\"fw-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.CurrentIP)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 46, Col: 62}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 46, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span> <button class=\"btn btn-sm btn-outline-secondary ms-2\" title=\"Refresh current IP\" hx-get=\"/refresh-ip\" hx-target=\"#current-ip\" hx-indicator=\"#refresh-spinner\"><i class=\"bi bi-arrow-clockwise\"></i> <span id=\"refresh-spinner\" class=\"htmx-indicator spinner-border spinner-border-sm ms-1\"></span></button></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span> <a href=\"/refresh-ip\" x-target=\"current-ip\" class=\"btn btn-sm btn-outline-secondary ms-2 d-inline-flex align-items-center\" @ajax:before=\"$el.querySelector('.bi-arrow-clockwise').classList.add('spin')\" @ajax:after=\"$el.querySelector('.bi-arrow-clockwise').classList.remove('spin')\"><i class=\"bi bi-arrow-clockwise\"></i></a></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -124,15 +124,7 @@ func Index(props IndexProps) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = ConfigModal(props.Config, props.UpdateFreqs).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = RecordModal(props.Config.Domain).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div></div><div class=\"toast-container\"></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -196,14 +188,14 @@ func ConfigStatus(config ConfigData) templ.Component {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"card mb-4\"><div class=\"card-header d-flex justify-content-between align-items-center\"><h5 class=\"mb-0\">Configuration</h5><button class=\"btn btn-sm btn-outline-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#configModal\">Edit</button></div><div class=\"card-body\"><div class=\"row\"><div class=\"col-md-4\"><strong>Domain:</strong> <span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"card mb-4\" id=\"config-status\"><div class=\"card-header d-flex justify-content-between align-items-center\" x-init x-data=\"{ editLoading: false, deleteLoading: false }\" @ajax:success=\"$dispatch('dialog:open')\"><h5 class=\"mb-0\">Configuration</h5><a href=\"/config\" @ajax:before=\"editLoading = true\" @ajax:success=\"$dispatch('dialog:open')\" @ajax:after=\"editLoading = false\" @ajax:error=\"editLoading = false\" x-target=\"contact\" class=\"btn btn-sm btn-outline-primary me-2\" :disabled=\"editLoading || deleteLoading\"><template x-if=\"!editLoading\"><span class=\"ms-1\">Edit <i class=\"bi bi-pencil\"></i></span></template><template x-if=\"editLoading\"><span class=\"spinner-border spinner-border-sm\"></span></template></a></div><div class=\"card-body\"><div class=\"row\"><div class=\"col-md-4\"><strong>Domain:</strong> <span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(config.Domain)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 103, Col: 51}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 117, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@ -216,7 +208,7 @@ func ConfigStatus(config ConfigData) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(config.ZoneID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 106, Col: 52}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 120, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@ -229,7 +221,7 @@ func ConfigStatus(config ConfigData) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(formatUpdateSchedule(config.UpdatePeriod))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 110, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 124, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@ -264,7 +256,7 @@ func DNSRecordsSection(records []DNSRecord, currentIP string) templ.Component {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"card\"><div class=\"card-header d-flex justify-content-between align-items-center\"><h5 class=\"mb-0\">DNS Records</h5><div><button class=\"btn btn-sm btn-success me-2\" hx-post=\"/update-all-records\" hx-target=\"#dns-records-table\" hx-confirm=\"Are you sure you want to update all A records to your current IP?\" hx-indicator=\"#update-all-spinner\"><i class=\"bi bi-arrow-repeat\"></i> Update All to Current IP <span id=\"update-all-spinner\" class=\"htmx-indicator spinner-border spinner-border-sm ms-1\"></span></button> <button class=\"btn btn-sm btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#recordModal\" onclick=\"resetRecordForm()\"><i class=\"bi bi-plus-lg\"></i> Add Record</button></div></div><div class=\"card-body p-0\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"card\"><div class=\"card-header d-flex justify-content-between align-items-center\"><h5 class=\"mb-0\">DNS Records</h5><div x-data=\"{ updating: false, addingRecord: false }\"><form method=\"post\" action=\"/update-all-records\" x-target=\"dns-records-table\" @ajax:before=\"confirm('Are you sure you want to update all A records to your current IP?') || $event.preventDefault(); updating = true\" @ajax:after=\"updating = false\" @ajax:error=\"updating = false\" style=\"display: inline;\"><button class=\"btn btn-sm btn-success me-2\" type=\"submit\" :disabled=\"updating || addingRecord\"><template x-if=\"!updating\"><span class=\"d-flex align-items-center\"><i class=\"bi bi-arrow-repeat me-1\"></i> Update All to Current IP</span></template><template x-if=\"updating\"><span class=\"d-flex align-items-center\"><span class=\"spinner-border spinner-border-sm me-2\"></span> Updating All Records...</span></template></button></form><a href=\"/records/new\" x-target=\"contact\" @ajax:before=\"addingRecord = true; $dispatch('dialog:open')\" @ajax:after=\"addingRecord = false\" @ajax:error=\"addingRecord = false\" class=\"btn btn-primary\" :disabled=\"updating || addingRecord\"><template x-if=\"!addingRecord\"><span class=\"d-flex align-items-center\"><i class=\"bi bi-plus-lg me-1\"></i> Add Record</span></template><template x-if=\"addingRecord\"><span class=\"d-flex align-items-center\"><span class=\"spinner-border spinner-border-sm me-2\"></span> Loading...</span></template></a></div></div><div class=\"card-body p-0\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -347,14 +339,14 @@ func DNSRecordRow(record DNSRecord, currentIP string) templ.Component {
templ_7745c5c3_Var12 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<tr><td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<tr x-data=\"{ editLoading: false, deleteLoading: false }\"><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(record.Type)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 178, Col: 19}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 224, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
@ -367,7 +359,7 @@ func DNSRecordRow(record DNSRecord, currentIP string) templ.Component {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(record.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 179, Col: 19}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 225, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@ -380,7 +372,7 @@ func DNSRecordRow(record DNSRecord, currentIP string) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(record.Content)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 181, Col: 19}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 227, Col: 19}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@ -416,7 +408,7 @@ func DNSRecordRow(record DNSRecord, currentIP string) templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%ds", record.TTL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 194, Col: 36}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 240, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@ -433,46 +425,46 @@ func DNSRecordRow(record DNSRecord, currentIP string) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td><td><button class=\"btn btn-sm btn-outline-primary me-1\" hx-get=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</td><td><a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/edit-record/%s", record.ID))
var templ_7745c5c3_Var17 templ.SafeURL
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/edit-record/%s", record.ID)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 205, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 250, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" hx-target=\"#record-modal-content\" hx-on::after-request=\"if(event.detail.successful) bootstrap.Modal.getOrCreateInstance(document.getElementById('recordModal')).show()\"><i class=\"bi bi-pencil\"></i></button> <button class=\"btn btn-sm btn-outline-danger\" hx-delete=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" @ajax:before=\"editLoading = true\" @ajax:success=\"$dispatch('dialog:open')\" @ajax:after=\"editLoading = false\" @ajax:error=\"editLoading = false\" x-target=\"contact\" class=\"btn btn-sm btn-outline-primary me-2\" :disabled=\"editLoading || deleteLoading\"><template x-if=\"!editLoading\"><i class=\"bi bi-pencil\"></i></template><template x-if=\"editLoading\"><span class=\"spinner-border spinner-border-sm\"></span></template></a><form method=\"delete\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/records/%s", record.ID))
var templ_7745c5c3_Var18 templ.SafeURL
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/records/%s", record.ID)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 213, Col: 53}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 268, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" hx-target=\"closest tr\" hx-swap=\"outerHTML\" hx-confirm=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" x-target=\"closest tr\" @ajax:before=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Are you sure you want to delete the record for \"%s\"?", record.Name))
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(`confirm('Are you sure you want to delete the record for "%s"?') || $event.preventDefault(); deleteLoading = true`, record.Name))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 216, Col: 99}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 270, Col: 159}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\"><i class=\"bi bi-trash\"></i></button></td></tr>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" @ajax:after=\"deleteLoading = false\" @ajax:error=\"deleteLoading = false\" @ajax:success=\"$el.closest('tr').remove()\" style=\"display: inline;\"><button class=\"btn btn-sm btn-outline-danger\" type=\"submit\" :disabled=\"editLoading || deleteLoading\"><template x-if=\"!deleteLoading\"><i class=\"bi bi-trash\"></i></template><template x-if=\"deleteLoading\"><span class=\"spinner-border spinner-border-sm\"></span></template></button></form></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

View file

@ -8,6 +8,16 @@ func Render(w io.Writer, component templ.Component) error {
return component.Render(context.Background(), w)
}
// RenderMultiple renders multiple components to an io.Writer in sequence
func RenderMultiple(w io.Writer, components ...templ.Component) error {
for _, component := range components {
if err := component.Render(context.Background(), w); err != nil {
return err
}
}
return nil
}
// Layout is the base layout for all pages
templ Layout(title string) {
<!DOCTYPE html>
@ -16,6 +26,8 @@ templ Layout(title string) {
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>{ title }</title>
<script defer src="https://cdn.jsdelivr.net/npm/@imacrayon/alpine-ajax@0.12.2/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.1/dist/cdn.min.js"></script>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
@ -24,8 +36,9 @@ templ Layout(title string) {
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"
/>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<style>
/* Base Styles */
body {
padding-top: 20px;
background-color: #f8f9fa;
@ -47,62 +60,185 @@ templ Layout(title string) {
font-size: 0.8em;
margin-left: 10px;
}
.htmx-indicator {
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Modal Styles */
dialog {
border: none !important;
outline: none !important;
border-radius: 0;
padding: 0;
margin: 0;
background: transparent;
box-shadow: none;
max-width: none;
max-height: none;
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
}
dialog[open] {
border: none !important;
outline: none !important;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease-out;
}
.modal-container {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
animation: modalShow 0.3s ease-out;
}
.modal-content {
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
width: 100%;
max-width: 600px;
max-height: 90vh;
overflow: hidden;
transform: scale(1);
animation: modalSlideIn 0.3s ease-out;
}
.modal-header {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-bottom: 1px solid #dee2e6;
padding: 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-title {
margin: 0;
font-weight: 600;
color: #495057;
font-size: 1.25rem;
}
.modal-body {
padding: 2rem;
max-height: 60vh;
overflow-y: auto;
}
.modal-footer {
background: #f8f9fa;
border-top: 1px solid #dee2e6;
padding: 1.5rem;
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
/* Enhanced form styling within modals */
.modal-body .form-label {
color: #495057;
margin-bottom: 0.75rem;
}
.modal-body .form-control,
.modal-body .form-select {
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 0.75rem;
transition: all 0.2s ease;
}
.modal-body .form-control:focus,
.modal-body .form-select:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.1);
}
.modal-body .input-group-text {
border: 2px solid #e9ecef;
border-left: none;
background: #f8f9fa;
font-weight: 500;
}
.modal-body .form-check {
border: 1px solid #e9ecef;
}
.modal-body .form-text {
font-size: 0.875rem;
color: #6c757d;
margin-top: 0.5rem;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes modalShow {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes modalSlideIn {
from {
transform: scale(0.95) translateY(-20px);
opacity: 0;
transition: opacity 300ms ease-in;
}
.htmx-request .htmx-indicator {
to {
transform: scale(1) translateY(0);
opacity: 1;
}
.htmx-request.htmx-indicator {
opacity: 1;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.modal-container {
padding: 0.5rem;
}
.modal-content {
max-height: 95vh;
border-radius: 8px;
}
.modal-header,
.modal-body,
.modal-footer {
padding: 1rem;
}
.modal-body {
max-height: 70vh;
}
}
</style>
</head>
<body>
{ children... }
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Simple toast function for HTMX responses
document.body.addEventListener('htmx:afterRequest', function(evt) {
const xhr = evt.detail.xhr;
if (xhr.status >= 200 && xhr.status < 300) {
const successMessage = xhr.getResponseHeader('HX-Success-Message');
if (successMessage) {
showToast(successMessage, 'success');
}
} else {
const errorMessage = xhr.getResponseHeader('HX-Error-Message') || 'An error occurred';
showToast(errorMessage, 'danger');
}
});
function showToast(message, type = "success") {
const toastContainer = document.querySelector(".toast-container");
const toast = document.createElement("div");
toast.className = `toast align-items-center text-white bg-${type}`;
toast.setAttribute("role", "alert");
toast.setAttribute("aria-live", "assertive");
toast.setAttribute("aria-atomic", "true");
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
<body id="body">
<div class="toast-container position-fixed top-0 end-0 p-3">
<ul x-sync id="notification_list" x-merge="prepend" role="status" class="list-unstyled"></ul>
</div>
`;
toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
toast.addEventListener("hidden.bs.toast", () => {
toast.remove();
});
}
</script>
{ children... }
<dialog id="global-dialog" x-init @dialog:open.window="$el.showModal()">
<div id="contact"></div>
</dialog>
</body>
</html>
}

File diff suppressed because one or more lines are too long

View file

@ -3,68 +3,87 @@ package templates
import "fmt"
templ ConfigModal(config ConfigData, frequencies []UpdateFrequency) {
<div
class="modal fade"
id="configModal"
tabindex="-1"
aria-labelledby="configModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div id="contact" class="modal-container">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="configModalLabel">Configuration</h5>
<h5 class="modal-title">Configuration Settings</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
@click="$el.closest('dialog').close()"
aria-label="Close"
></button>
</div>
<form hx-post="/config" hx-target="body" hx-swap="outerHTML">
<form
x-target="config-status"
method="post"
action="/config"
@ajax:success="$el.closest('dialog').close()"
x-data="{ saving: false }"
@ajax:before="saving = true"
@ajax:after="saving = false"
@ajax:error="saving = false"
>
<div class="modal-body">
<div class="mb-3">
<label for="api-token" class="form-label">Cloudflare API Token</label>
<label for="api-token" class="form-label fw-semibold">
<i class="bi bi-key-fill text-primary me-2"></i>
Cloudflare API Token
</label>
<input
type="password"
class="form-control"
id="api-token"
name="api_token"
value={ config.ApiToken }
placeholder="Enter your Cloudflare API token"
required
/>
<div class="form-text">
<i class="bi bi-info-circle me-1"></i>
Create a token with <code>Zone.DNS:Edit</code> permissions in
the Cloudflare dashboard.
</div>
</div>
<div class="mb-3">
<label for="zone-id-input" class="form-label">Zone ID</label>
<label for="zone-id-input" class="form-label fw-semibold">
<i class="bi bi-globe text-info me-2"></i>
Zone ID
</label>
<input
type="text"
class="form-control"
id="zone-id-input"
name="zone_id"
value={ config.ZoneID }
placeholder="e.g., 1234567890abcdef1234567890abcdef"
required
/>
<div class="form-text">
<i class="bi bi-info-circle me-1"></i>
Found in the Cloudflare dashboard under your domain's overview page.
</div>
</div>
<div class="mb-3">
<label for="domain-input" class="form-label">Domain</label>
<label for="domain-input" class="form-label fw-semibold">
<i class="bi bi-link-45deg text-success me-2"></i>
Domain
</label>
<input
type="text"
class="form-control"
id="domain-input"
name="domain"
value={ config.Domain }
placeholder="e.g., example.com"
required
/>
</div>
<div class="mb-3">
<label for="update-period" class="form-label">Update Frequency</label>
<label for="update-period" class="form-label fw-semibold">
<i class="bi bi-clock text-warning me-2"></i>
Update Frequency
</label>
<select class="form-select" id="update-period" name="update_period">
for _, freq := range frequencies {
<option value={ freq.Value } selected?={ freq.Value == config.UpdatePeriod }>
@ -78,59 +97,77 @@ templ ConfigModal(config ConfigData, frequencies []UpdateFrequency) {
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
@click="$el.closest('dialog').close()"
>
<i class="bi bi-x-lg me-1"></i>
Cancel
</button>
<button type="submit" class="btn btn-primary">
Save
<span class="htmx-indicator spinner-border spinner-border-sm ms-1"></span>
<button
type="submit"
class="btn btn-primary"
:disabled="saving"
>
<template x-if="!saving">
<span class="d-flex align-items-center">
<i class="bi bi-check-lg me-2"></i>
Save Configuration
</span>
</template>
<template x-if="saving">
<span class="d-flex align-items-center">
<span class="spinner-border spinner-border-sm me-2"></span>
Saving...
</span>
</template>
</button>
</div>
</form>
</div>
</div>
</div>
}
templ RecordModal(domain string) {
<div
class="modal fade"
id="recordModal"
tabindex="-1"
aria-labelledby="recordModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div id="record-modal-content" class="modal-content">
@RecordForm("Add DNS Record", "", domain, DNSRecord{Type: "A", TTL: 1})
</div>
</div>
</div>
}
templ RecordForm(title, recordID, domain string, record DNSRecord) {
<div id="contact" class="modal-container">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{ title }</h5>
<h5 class="modal-title">
<i class="bi bi-dns text-primary me-2"></i>
{ title }
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
@click="$el.closest('dialog').close()"
aria-label="Close"
></button>
</div>
<div id="error-message" class="alert alert-danger d-none mx-3 mt-3" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<span class="error-text"></span>
</div>
<form
if recordID != "" {
hx-put={ fmt.Sprintf("/records/%s", recordID) }
method="put"
action={ templ.URL(fmt.Sprintf("/records/%s", recordID)) }
} else {
hx-post="/records"
method="post"
action="/records"
}
hx-target="#dns-records-table"
hx-on::after-request="if(event.detail.successful) bootstrap.Modal.getInstance(document.getElementById('recordModal')).hide()"
x-target="dns-records-table"
x-target.error="none"
@ajax:success="$el.closest('dialog').close()"
x-data="{ saving: false }"
@ajax:before="saving = true"
@ajax:after="saving = false"
@ajax:error="saving = false"
>
<div class="modal-body">
<div class="mb-3">
<label for="record-name" class="form-label">Name</label>
<div class="row">
<div class="col-md-8 mb-3">
<label for="record-name" class="form-label fw-semibold">
<i class="bi bi-tag text-info me-2"></i>
Record Name
</label>
<div class="input-group">
<input
type="text"
@ -141,37 +178,48 @@ templ RecordForm(title, recordID, domain string, record DNSRecord) {
value={ getRecordName(record.Name, domain) }
required
/>
<span class="input-group-text">.{ domain }</span>
<span class="input-group-text bg-light text-muted">.{ domain }</span>
</div>
<div class="form-text">Use @ for the root domain</div>
<div class="form-text">
<i class="bi bi-info-circle me-1"></i>
Use <code>{ "@" }</code> { "for" } the root domain
</div>
<div class="mb-3">
<label for="record-type" class="form-label">Type</label>
</div>
<div class="col-md-4 mb-3">
<label for="record-type" class="form-label fw-semibold">
<i class="bi bi-diagram-3 text-success me-2"></i>
Type
</label>
<select
class="form-select"
id="record-type"
name="type"
onchange="toggleMyIPOption()"
>
<option value="A" selected?={ record.Type == "A" }>A</option>
<option value="AAAA" selected?={ record.Type == "AAAA" }>AAAA</option>
<option value="A" selected?={ record.Type == "A" }>A (IPv4)</option>
<option value="AAAA" selected?={ record.Type == "AAAA" }>AAAA (IPv6)</option>
<option value="CNAME" selected?={ record.Type == "CNAME" }>CNAME</option>
<option value="TXT" selected?={ record.Type == "TXT" }>TXT</option>
<option value="MX" selected?={ record.Type == "MX" }>MX</option>
</select>
</div>
</div>
<div class="mb-3" id="content-group">
<label for="record-content" class="form-label">Content</label>
<label for="record-content" class="form-label fw-semibold">
<i class="bi bi-file-text text-warning me-2"></i>
Content
</label>
<input
type="text"
class="form-control"
id="record-content"
name="content"
value={ record.Content }
placeholder="Enter the record value"
required
/>
</div>
<div class="mb-3 form-check" id="use-my-ip-group" style="display: none;">
<div class="mb-3 form-check bg-light p-3 rounded" id="use-my-ip-group" style="display: none;">
<input
type="checkbox"
class="form-check-input"
@ -179,10 +227,20 @@ templ RecordForm(title, recordID, domain string, record DNSRecord) {
name="use_my_ip"
onchange="toggleContentField()"
/>
<label class="form-check-label" for="use-my-ip">Use my current IP address</label>
<label class="form-check-label fw-semibold" for="use-my-ip">
<i class="bi bi-geo-alt text-primary me-2"></i>
Use my current IP address
</label>
<div class="form-text">
Automatically use your current public IP address
</div>
<div class="mb-3">
<label for="record-ttl" class="form-label">TTL</label>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="record-ttl" class="form-label fw-semibold">
<i class="bi bi-clock-history text-secondary me-2"></i>
TTL (Time to Live)
</label>
<select class="form-select" id="record-ttl" name="ttl">
<option value="1" selected?={ record.TTL == 1 }>Auto</option>
<option value="120" selected?={ record.TTL == 120 }>2 minutes</option>
@ -196,7 +254,8 @@ templ RecordForm(title, recordID, domain string, record DNSRecord) {
<option value="86400" selected?={ record.TTL == 86400 }>1 day</option>
</select>
</div>
<div class="mb-3 form-check">
<div class="col-md-6 mb-3 d-flex align-items-end">
<div class="form-check bg-light p-3 rounded w-100">
<input
type="checkbox"
class="form-check-input"
@ -204,52 +263,54 @@ templ RecordForm(title, recordID, domain string, record DNSRecord) {
name="proxied"
checked?={ record.Proxied }
/>
<label class="form-check-label" for="record-proxied">Proxied through Cloudflare</label>
<label class="form-check-label fw-semibold" for="record-proxied">
<i class="bi bi-shield-check text-success me-2"></i>
Proxied through Cloudflare
</label>
<div class="form-text">
Enable Cloudflare's proxy and security features
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
@click="$el.closest('dialog').close()"
>
<i class="bi bi-x-lg me-1"></i>
Cancel
</button>
<button type="submit" class="btn btn-primary">
Save
<span class="htmx-indicator spinner-border spinner-border-sm ms-1"></span>
<button
type="submit"
class="btn btn-primary"
:disabled="saving"
>
<template x-if="!saving">
<span class="d-flex align-items-center">
<i class="bi bi-check-lg me-2"></i>
if recordID != "" {
Update Record
} else {
Create Record
}
</span>
</template>
<template x-if="saving">
<span class="d-flex align-items-center">
<span class="spinner-border spinner-border-sm me-2"></span>
if recordID != "" {
Updating...
} else {
Creating...
}
</span>
</template>
</button>
</div>
</form>
<script>
function toggleMyIPOption() {
const recordType = document.getElementById('record-type').value;
const useMyIPGroup = document.getElementById('use-my-ip-group');
const contentGroup = document.getElementById('content-group');
if (recordType === 'A') {
useMyIPGroup.style.display = 'block';
} else {
useMyIPGroup.style.display = 'none';
contentGroup.style.display = 'block';
document.getElementById('use-my-ip').checked = false;
}
}
function toggleContentField() {
const useMyIP = document.getElementById('use-my-ip').checked;
const contentGroup = document.getElementById('content-group');
contentGroup.style.display = useMyIP ? 'none' : 'block';
}
function resetRecordForm() {
setTimeout(() => {
document.getElementById('record-type').value = 'A';
toggleMyIPOption();
}, 100);
}
// Initialize the form state
toggleMyIPOption();
</script>
</div>
</div>
}

View file

@ -31,46 +31,46 @@ func ConfigModal(config ConfigData, frequencies []UpdateFrequency) templ.Compone
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"modal fade\" id=\"configModal\" tabindex=\"-1\" aria-labelledby=\"configModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"configModalLabel\">Configuration</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><form hx-post=\"/config\" hx-target=\"body\" hx-swap=\"outerHTML\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"api-token\" class=\"form-label\">Cloudflare API Token</label> <input type=\"password\" class=\"form-control\" id=\"api-token\" name=\"api_token\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div id=\"contact\" class=\"modal-container\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\">Configuration Settings</h5><button type=\"button\" class=\"btn-close\" @click=\"$el.closest('dialog').close()\" aria-label=\"Close\"></button></div><form x-target=\"config-status\" method=\"post\" action=\"/config\" @ajax:success=\"$el.closest('dialog').close()\" x-data=\"{ saving: false }\" @ajax:before=\"saving = true\" @ajax:after=\"saving = false\" @ajax:error=\"saving = false\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"api-token\" class=\"form-label fw-semibold\"><i class=\"bi bi-key-fill text-primary me-2\"></i> Cloudflare API Token</label> <input type=\"password\" class=\"form-control\" id=\"api-token\" name=\"api_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(config.ApiToken)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 33, Col: 31}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 38, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" required><div class=\"form-text\">Create a token with <code>Zone.DNS:Edit</code> permissions in the Cloudflare dashboard.</div></div><div class=\"mb-3\"><label for=\"zone-id-input\" class=\"form-label\">Zone ID</label> <input type=\"text\" class=\"form-control\" id=\"zone-id-input\" name=\"zone_id\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" placeholder=\"Enter your Cloudflare API token\" required><div class=\"form-text\"><i class=\"bi bi-info-circle me-1\"></i> Create a token with <code>Zone.DNS:Edit</code> permissions in the Cloudflare dashboard.</div></div><div class=\"mb-3\"><label for=\"zone-id-input\" class=\"form-label fw-semibold\"><i class=\"bi bi-globe text-info me-2\"></i> Zone ID</label> <input type=\"text\" class=\"form-control\" id=\"zone-id-input\" name=\"zone_id\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(config.ZoneID)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 48, Col: 29}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 58, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" required><div class=\"form-text\">Found in the Cloudflare dashboard under your domain's overview page.</div></div><div class=\"mb-3\"><label for=\"domain-input\" class=\"form-label\">Domain</label> <input type=\"text\" class=\"form-control\" id=\"domain-input\" name=\"domain\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\" placeholder=\"e.g., 1234567890abcdef1234567890abcdef\" required><div class=\"form-text\"><i class=\"bi bi-info-circle me-1\"></i> Found in the Cloudflare dashboard under your domain's overview page.</div></div><div class=\"mb-3\"><label for=\"domain-input\" class=\"form-label fw-semibold\"><i class=\"bi bi-link-45deg text-success me-2\"></i> Domain</label> <input type=\"text\" class=\"form-control\" id=\"domain-input\" name=\"domain\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(config.Domain)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 62, Col: 29}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 77, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" required></div><div class=\"mb-3\"><label for=\"update-period\" class=\"form-label\">Update Frequency</label> <select class=\"form-select\" id=\"update-period\" name=\"update_period\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" placeholder=\"e.g., example.com\" required></div><div class=\"mb-3\"><label for=\"update-period\" class=\"form-label fw-semibold\"><i class=\"bi bi-clock text-warning me-2\"></i> Update Frequency</label> <select class=\"form-select\" id=\"update-period\" name=\"update_period\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -82,7 +82,7 @@ func ConfigModal(config ConfigData, frequencies []UpdateFrequency) templ.Compone
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(freq.Value)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 70, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 89, Col: 34}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@ -105,7 +105,7 @@ func ConfigModal(config ConfigData, frequencies []UpdateFrequency) templ.Compone
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(freq.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 71, Col: 22}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 90, Col: 21}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@ -116,44 +116,7 @@ func ConfigModal(config ConfigData, frequencies []UpdateFrequency) templ.Compone
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</select></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">Save <span class=\"htmx-indicator spinner-border spinner-border-sm ms-1\"></span></button></div></form></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func RecordModal(domain string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"modal fade\" id=\"recordModal\" tabindex=\"-1\" aria-labelledby=\"recordModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div id=\"record-modal-content\" class=\"modal-content\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = RecordForm("Add DNS Record", "", domain, DNSRecord{Type: "A", TTL: 1}).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</select></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" @click=\"$el.closest('dialog').close()\"><i class=\"bi bi-x-lg me-1\"></i> Cancel</button> <button type=\"submit\" class=\"btn btn-primary\" :disabled=\"saving\"><template x-if=\"!saving\"><span class=\"d-flex align-items-center\"><i class=\"bi bi-check-lg me-2\"></i> Save Configuration</span></template><template x-if=\"saving\"><span class=\"d-flex align-items-center\"><span class=\"spinner-border spinner-border-sm me-2\"></span> Saving...</span></template></button></div></form></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -177,79 +140,105 @@ func RecordForm(title, recordID, domain string, record DNSRecord) templ.Componen
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
templ_7745c5c3_Var7 := templ.GetChildren(ctx)
if templ_7745c5c3_Var7 == nil {
templ_7745c5c3_Var7 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<div class=\"modal-header\"><h5 class=\"modal-title\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div id=\"contact\" class=\"modal-container\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\"><i class=\"bi bi-dns text-primary me-2\"></i> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(title)
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 114, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 135, Col: 12}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</h5><button type=\"button\" class=\"btn-close\" @click=\"$el.closest('dialog').close()\" aria-label=\"Close\"></button></div><div id=\"error-message\" class=\"alert alert-danger d-none mx-3 mt-3\" role=\"alert\"><i class=\"bi bi-exclamation-triangle-fill me-2\"></i> <span class=\"error-text\"></span></div><form")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if recordID != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " method=\"put\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 templ.SafeURL
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/records/%s", recordID)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 151, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><form")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if recordID != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " hx-put=\"")
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " method=\"post\" action=\"/records\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " x-target=\"dns-records-table\" x-target.error=\"none\" @ajax:success=\"$el.closest('dialog').close()\" x-data=\"{ saving: false }\" @ajax:before=\"saving = true\" @ajax:after=\"saving = false\" @ajax:error=\"saving = false\"><div class=\"modal-body\"><div class=\"row\"><div class=\"col-md-8 mb-3\"><label for=\"record-name\" class=\"form-label fw-semibold\"><i class=\"bi bi-tag text-info me-2\"></i> Record Name</label><div class=\"input-group\"><input type=\"text\" class=\"form-control\" id=\"record-name\" name=\"name\" placeholder=\"subdomain\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("/records/%s", recordID))
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(getRecordName(record.Name, domain))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 124, Col: 48}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 178, Col: 51}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " hx-post=\"/records\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " hx-target=\"#dns-records-table\" hx-on::after-request=\"if(event.detail.successful) bootstrap.Modal.getInstance(document.getElementById('recordModal')).hide()\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"record-name\" class=\"form-label\">Name</label><div class=\"input-group\"><input type=\"text\" class=\"form-control\" id=\"record-name\" name=\"name\" placeholder=\"subdomain\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" required> <span class=\"input-group-text bg-light text-muted\">.")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(getRecordName(record.Name, domain))
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(domain)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 141, Col: 48}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 181, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" required> <span class=\"input-group-text\">.")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</span></div><div class=\"form-text\"><i class=\"bi bi-info-circle me-1\"></i> Use <code>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(domain)
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs("@")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 144, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 185, Col: 23}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span></div><div class=\"form-text\">Use @ for the root domain</div></div><div class=\"mb-3\"><label for=\"record-type\" class=\"form-label\">Type</label> <select class=\"form-select\" id=\"record-type\" name=\"type\" onchange=\"toggleMyIPOption()\"><option value=\"A\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</code> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs("for")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 185, Col: 40}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " the root domain</div></div><div class=\"col-md-4 mb-3\"><label for=\"record-type\" class=\"form-label fw-semibold\"><i class=\"bi bi-diagram-3 text-success me-2\"></i> Type</label> <select class=\"form-select\" id=\"record-type\" name=\"type\" onchange=\"toggleMyIPOption()\"><option value=\"A\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -259,7 +248,7 @@ func RecordForm(title, recordID, domain string, record DNSRecord) templ.Componen
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, ">A</option> <option value=\"AAAA\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, ">A (IPv4)</option> <option value=\"AAAA\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -269,7 +258,7 @@ func RecordForm(title, recordID, domain string, record DNSRecord) templ.Componen
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, ">AAAA</option> <option value=\"CNAME\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, ">AAAA (IPv6)</option> <option value=\"CNAME\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -299,20 +288,20 @@ func RecordForm(title, recordID, domain string, record DNSRecord) templ.Componen
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, ">MX</option></select></div><div class=\"mb-3\" id=\"content-group\"><label for=\"record-content\" class=\"form-label\">Content</label> <input type=\"text\" class=\"form-control\" id=\"record-content\" name=\"content\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, ">MX</option></select></div></div><div class=\"mb-3\" id=\"content-group\"><label for=\"record-content\" class=\"form-label fw-semibold\"><i class=\"bi bi-file-text text-warning me-2\"></i> Content</label> <input type=\"text\" class=\"form-control\" id=\"record-content\" name=\"content\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(record.Content)
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(record.Content)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 170, Col: 27}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/modal.templ`, Line: 217, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" required></div><div class=\"mb-3 form-check\" id=\"use-my-ip-group\" style=\"display: none;\"><input type=\"checkbox\" class=\"form-check-input\" id=\"use-my-ip\" name=\"use_my_ip\" onchange=\"toggleContentField()\"> <label class=\"form-check-label\" for=\"use-my-ip\">Use my current IP address</label></div><div class=\"mb-3\"><label for=\"record-ttl\" class=\"form-label\">TTL</label> <select class=\"form-select\" id=\"record-ttl\" name=\"ttl\"><option value=\"1\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" placeholder=\"Enter the record value\" required></div><div class=\"mb-3 form-check bg-light p-3 rounded\" id=\"use-my-ip-group\" style=\"display: none;\"><input type=\"checkbox\" class=\"form-check-input\" id=\"use-my-ip\" name=\"use_my_ip\" onchange=\"toggleContentField()\"> <label class=\"form-check-label fw-semibold\" for=\"use-my-ip\"><i class=\"bi bi-geo-alt text-primary me-2\"></i> Use my current IP address</label><div class=\"form-text\">Automatically use your current public IP address</div></div><div class=\"row\"><div class=\"col-md-6 mb-3\"><label for=\"record-ttl\" class=\"form-label fw-semibold\"><i class=\"bi bi-clock-history text-secondary me-2\"></i> TTL (Time to Live)</label> <select class=\"form-select\" id=\"record-ttl\" name=\"ttl\"><option value=\"1\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -412,7 +401,7 @@ func RecordForm(title, recordID, domain string, record DNSRecord) templ.Componen
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">1 day</option></select></div><div class=\"mb-3 form-check\"><input type=\"checkbox\" class=\"form-check-input\" id=\"record-proxied\" name=\"proxied\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">1 day</option></select></div><div class=\"col-md-6 mb-3 d-flex align-items-end\"><div class=\"form-check bg-light p-3 rounded w-100\"><input type=\"checkbox\" class=\"form-check-input\" id=\"record-proxied\" name=\"proxied\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -422,7 +411,37 @@ func RecordForm(title, recordID, domain string, record DNSRecord) templ.Componen
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "> <label class=\"form-check-label\" for=\"record-proxied\">Proxied through Cloudflare</label></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"submit\" class=\"btn btn-primary\">Save <span class=\"htmx-indicator spinner-border spinner-border-sm ms-1\"></span></button></div></form><script>\n\t\tfunction toggleMyIPOption() {\n\t\t\tconst recordType = document.getElementById('record-type').value;\n\t\t\tconst useMyIPGroup = document.getElementById('use-my-ip-group');\n\t\t\tconst contentGroup = document.getElementById('content-group');\n\n\t\t\tif (recordType === 'A') {\n\t\t\t\tuseMyIPGroup.style.display = 'block';\n\t\t\t} else {\n\t\t\t\tuseMyIPGroup.style.display = 'none';\n\t\t\t\tcontentGroup.style.display = 'block';\n\t\t\t\tdocument.getElementById('use-my-ip').checked = false;\n\t\t\t}\n\t\t}\n\n\t\tfunction toggleContentField() {\n\t\t\tconst useMyIP = document.getElementById('use-my-ip').checked;\n\t\t\tconst contentGroup = document.getElementById('content-group');\n\t\t\tcontentGroup.style.display = useMyIP ? 'none' : 'block';\n\t\t}\n\n\t\tfunction resetRecordForm() {\n\t\t\tsetTimeout(() => {\n\t\t\t\tdocument.getElementById('record-type').value = 'A';\n\t\t\t\ttoggleMyIPOption();\n\t\t\t}, 100);\n\t\t}\n\n\t\t// Initialize the form state\n\t\ttoggleMyIPOption();\n\t</script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "> <label class=\"form-check-label fw-semibold\" for=\"record-proxied\"><i class=\"bi bi-shield-check text-success me-2\"></i> Proxied through Cloudflare</label><div class=\"form-text\">Enable Cloudflare's proxy and security features</div></div></div></div></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" @click=\"$el.closest('dialog').close()\"><i class=\"bi bi-x-lg me-1\"></i> Cancel</button> <button type=\"submit\" class=\"btn btn-primary\" :disabled=\"saving\"><template x-if=\"!saving\"><span class=\"d-flex align-items-center\"><i class=\"bi bi-check-lg me-2\"></i> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if recordID != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "Update Record")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "Create Record")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</span></template><template x-if=\"saving\"><span class=\"d-flex align-items-center\"><span class=\"spinner-border spinner-border-sm me-2\"></span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if recordID != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "Updating...")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "Creating...")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</span></template></button></div></form></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

73
templates/toast.templ Normal file
View file

@ -0,0 +1,73 @@
package templates
import "fmt"
// NotificationList renders the container for notifications
templ NotificationList() {
<ul x-sync id="notification_list" x-merge="prepend" role="status" class="list-unstyled">
{ children... }
</ul>
}
// NotificationToast renders a single notification toast
templ NotificationToast(message, notificationType string) {
<li>
<div
class={ fmt.Sprintf("toast align-items-center text-white bg-%s show", notificationType) }
role="alert"
aria-live="assertive"
aria-atomic="true"
x-data="{
show: false,
init() {
this.$nextTick(() => this.show = true);
setTimeout(() => this.dismiss(), 6000);
},
dismiss() {
this.show = false;
setTimeout(() => this.$root.remove(), 500);
}
}"
x-show="show"
x-transition.duration.500ms
>
<div class="d-flex">
<div class="toast-body">{ message }</div>
<button
type="button"
class="btn-close btn-close-white me-2 m-auto"
@click="dismiss()"
aria-label="Close"
>
&times;
</button>
</div>
</div>
</li>
}
// Helper functions for common notification types
templ SuccessNotification(message string) {
@NotificationList() {
@NotificationToast(message, "success")
}
}
templ ErrorNotification(message string) {
@NotificationList() {
@NotificationToast(message, "danger")
}
}
templ WarningNotification(message string) {
@NotificationList() {
@NotificationToast(message, "warning")
}
}
templ InfoNotification(message string) {
@NotificationList() {
@NotificationToast(message, "info")
}
}

306
templates/toast_templ.go Normal file
View file

@ -0,0 +1,306 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.898
package templates
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "fmt"
// NotificationList renders the container for notifications
func NotificationList() templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<ul x-sync id=\"notification_list\" x-merge=\"prepend\" role=\"status\" class=\"list-unstyled\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</ul>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// NotificationToast renders a single notification toast
func NotificationToast(message, notificationType string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil {
templ_7745c5c3_Var2 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 = []any{fmt.Sprintf("toast align-items-center text-white bg-%s show", notificationType)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var3...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var3).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/toast.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\" x-data=\"{\n\t\t\t\tshow: false,\n\t\t\t\tinit() {\n\t\t\t\t\tthis.$nextTick(() => this.show = true);\n\t\t\t\t\tsetTimeout(() => this.dismiss(), 6000);\n\t\t\t\t},\n\t\t\t\tdismiss() {\n\t\t\t\t\tthis.show = false;\n\t\t\t\t\tsetTimeout(() => this.$root.remove(), 500);\n\t\t\t\t}\n\t\t\t}\" x-show=\"show\" x-transition.duration.500ms><div class=\"d-flex\"><div class=\"toast-body\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(message)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/toast.templ`, Line: 36, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><button type=\"button\" class=\"btn-close btn-close-white me-2 m-auto\" @click=\"dismiss()\" aria-label=\"Close\">&times;</button></div></div></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
// Helper functions for common notification types
func SuccessNotification(message string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var7 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = NotificationToast(message, "success").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = NotificationList().Render(templ.WithChildren(ctx, templ_7745c5c3_Var7), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func ErrorNotification(message string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var8 := templ.GetChildren(ctx)
if templ_7745c5c3_Var8 == nil {
templ_7745c5c3_Var8 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var9 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = NotificationToast(message, "danger").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = NotificationList().Render(templ.WithChildren(ctx, templ_7745c5c3_Var9), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func WarningNotification(message string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var10 := templ.GetChildren(ctx)
if templ_7745c5c3_Var10 == nil {
templ_7745c5c3_Var10 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var11 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = NotificationToast(message, "warning").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = NotificationList().Render(templ.WithChildren(ctx, templ_7745c5c3_Var11), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
func InfoNotification(message string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
if templ_7745c5c3_Var12 == nil {
templ_7745c5c3_Var12 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var13 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = NotificationToast(message, "info").Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = NotificationList().Render(templ.WithChildren(ctx, templ_7745c5c3_Var13), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate