code clean ups, minor improvements

This commit is contained in:
lone-cloud 2026-02-12 23:13:16 -08:00
parent 2a7b1e91c6
commit ea3345825a
20 changed files with 215 additions and 294 deletions

View file

@ -30,5 +30,4 @@ jobs:
push: true push: true
build-args: | build-args: |
VERSION=dev VERSION=dev
COMMIT=${{ github.sha }}
tags: ghcr.io/lone-cloud/prism:dev tags: ghcr.io/lone-cloud/prism:dev

View file

@ -26,18 +26,17 @@ jobs:
- name: Build binaries - name: Build binaries
run: | run: |
VERSION=$(cat VERSION) VERSION=$(cat VERSION)
COMMIT=$(git rev-parse --short HEAD)
# Linux AMD64 # Linux AMD64
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-trimpath \ -trimpath \
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" \ -ldflags="-s -w -X main.version=$VERSION" \
-o prism-linux-amd64 . -o prism-linux-amd64 .
# Linux ARM64 # Linux ARM64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \
-trimpath \ -trimpath \
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" \ -ldflags="-s -w -X main.version=$VERSION" \
-o prism-linux-arm64 . -o prism-linux-arm64 .
- name: Create GitHub Release - name: Create GitHub Release
@ -72,7 +71,6 @@ jobs:
push: true push: true
build-args: | build-args: |
VERSION=${{ steps.version.outputs.tag }} VERSION=${{ steps.version.outputs.tag }}
COMMIT=${{ github.sha }}
tags: | tags: |
ghcr.io/lone-cloud/prism:${{ steps.version.outputs.tag }} ghcr.io/lone-cloud/prism:${{ steps.version.outputs.tag }}
ghcr.io/lone-cloud/prism:latest ghcr.io/lone-cloud/prism:latest

View file

@ -1,7 +1,6 @@
FROM golang:1.25-alpine3.23 AS builder FROM golang:1.25-alpine3.23 AS builder
ARG VERSION=dev ARG VERSION=dev
ARG COMMIT=unknown
WORKDIR /build WORKDIR /build
@ -14,7 +13,7 @@ COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \ RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \ -trimpath \
-ldflags="-w -s -X main.version=${VERSION} -X main.commit=${COMMIT}" \ -ldflags="-w -s -X main.version=${VERSION}" \
-o prism . -o prism .
FROM debian:trixie-slim FROM debian:trixie-slim

View file

@ -2,14 +2,13 @@
BINARY_NAME=prism BINARY_NAME=prism
VERSION?=$(shell cat VERSION 2>/dev/null || echo "dev") VERSION?=$(shell cat VERSION 2>/dev/null || echo "dev")
COMMIT?=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
GOBIN?=$(shell command -v go >/dev/null 2>&1 && go env GOPATH || echo "${HOME}/go")/bin GOBIN?=$(shell command -v go >/dev/null 2>&1 && go env GOPATH || echo "${HOME}/go")/bin
export PATH := $(GOBIN):$(PATH) export PATH := $(GOBIN):$(PATH)
all: fix build all: fix build
build: build:
go build -ldflags="-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT)" -o $(BINARY_NAME) . go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY_NAME) .
start: build start: build
./$(BINARY_NAME) ./$(BINARY_NAME)

View file

@ -1 +1 @@
0.2.1 0.2.2

View file

@ -21,7 +21,6 @@ var publicAssets embed.FS
var ( var (
version = "dev" version = "dev"
commit = "unknown"
) )
func init() { func init() {
@ -30,7 +29,7 @@ func init() {
func main() { func main() {
if len(os.Args) > 1 && os.Args[1] == "version" { if len(os.Args) > 1 && os.Args[1] == "version" {
fmt.Printf("Prism %s (%s)\n", version, commit) fmt.Printf("Prism %s\n", version)
return return
} }

View file

@ -1,3 +1,4 @@
/* Variables */
:root { :root {
--bg-primary: #f5f5f5; --bg-primary: #f5f5f5;
--bg-secondary: #fdfdfd; --bg-secondary: #fdfdfd;
@ -35,7 +36,7 @@
--spinner-track: #4a4a4a; --spinner-track: #4a4a4a;
} }
/* CSS Reset */ /* Reset */
*, *,
*::before, *::before,
*::after { *::after {
@ -78,9 +79,11 @@ h6 {
overflow-wrap: break-word; overflow-wrap: break-word;
} }
/* Styles */ /* Base */
body { body {
font-family: system-ui; font-family: system-ui;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
max-width: 50rem; max-width: 50rem;
margin: 1rem auto; margin: 1rem auto;
padding: 0 1.25rem 1.25rem; padding: 0 1.25rem 1.25rem;
@ -88,6 +91,22 @@ body {
color: var(--text-primary); color: var(--text-primary);
} }
h2 {
margin-bottom: 1.25rem;
}
a {
color: var(--accent);
text-decoration: underline;
}
a:hover {
opacity: 0.8;
}
/* Components */
/* Cards */
.card { .card {
background: var(--bg-secondary); background: var(--bg-secondary);
border-radius: 0.5rem; border-radius: 0.5rem;
@ -128,19 +147,7 @@ details[open] > .card-header::before {
padding: 0 1.25rem 1.25rem 1.25rem; padding: 0 1.25rem 1.25rem 1.25rem;
} }
h2 { /* Tooltips */
margin-bottom: 1.25rem;
}
a {
color: var(--accent);
text-decoration: underline;
}
a:hover {
opacity: 0.8;
}
.channel-badge:has(.tooltip), .channel-badge:has(.tooltip),
.integration-status:has(.tooltip) { .integration-status:has(.tooltip) {
cursor: help; cursor: help;
@ -183,6 +190,7 @@ a:hover {
opacity: 1; opacity: 1;
} }
/* Theme Toggle */
.theme-toggle { .theme-toggle {
position: fixed; position: fixed;
bottom: 1.5rem; bottom: 1.5rem;
@ -201,6 +209,7 @@ a:hover {
opacity: 1; opacity: 1;
} }
/* App List */
.app-list { .app-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
@ -300,6 +309,7 @@ a:hover {
filter: brightness(0.85); filter: brightness(0.85);
} }
/* Loading Spinner */
.loading { .loading {
color: var(--text-secondary); color: var(--text-secondary);
display: flex; display: flex;
@ -322,6 +332,7 @@ a:hover {
} }
} }
/* Integrations */
.integration-card { .integration-card {
background: var(--bg-secondary); background: var(--bg-secondary);
border-radius: 0.5rem; border-radius: 0.5rem;
@ -405,6 +416,7 @@ details[open] > .integration-header::before {
color: var(--text-on-color); color: var(--text-on-color);
} }
/* Forms */
.auth-form { .auth-form {
margin-top: 1rem; margin-top: 1rem;
max-width: 400px; max-width: 400px;
@ -433,6 +445,7 @@ details[open] > .integration-header::before {
font-size: 1rem; font-size: 1rem;
} }
/* Buttons */
.btn-primary, .btn-primary,
.btn-secondary, .btn-secondary,
.btn-danger { .btn-danger {
@ -500,6 +513,7 @@ details[open] > .integration-header::before {
color: var(--text-on-color); color: var(--text-on-color);
} }
/* QR Code */
.qr-container { .qr-container {
margin-top: 1rem; margin-top: 1rem;
} }

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Prism Admin</title> <title>Prism Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#8159b8"> <meta name="theme-color" content="#60c5ff">
<link rel="icon" type="image/webp" href="/favicon.webp"> <link rel="icon" type="image/webp" href="/favicon.webp">
<link rel="manifest" href="/manifest.json"> <link rel="manifest" href="/manifest.json">
<link rel="stylesheet" href="/index.css?v={{.Version}}"> <link rel="stylesheet" href="/index.css?v={{.Version}}">

View file

@ -1,220 +1,169 @@
document.addEventListener('submit', async (e) => { async function handleAuthForm(form, endpoint, statusId, getPayload) {
if (e.target.id === 'telegram-auth-form') { const status = document.getElementById(statusId);
e.preventDefault(); const btn = form.querySelector('button[type="submit"]');
const form = e.target; const showError = (msg) => {
const status = document.getElementById('telegram-auth-status'); status.textContent = `Error: ${msg}`;
const btn = form.querySelector('button[type="submit"]'); status.className = 'auth-status error';
btn.disabled = false;
};
btn.disabled = true; btn.disabled = true;
try { try {
const formData = new FormData(form); const formData = new FormData(form);
const response = await fetch('/api/telegram/auth', { const response = await fetch(endpoint, {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json', body: JSON.stringify(getPayload(formData, form)),
}, });
body: JSON.stringify({
bot_token: formData.get('bot_token'),
chat_id: formData.get('chat_id'),
}),
});
if (response.ok) { if (response.ok) {
location.reload(); location.reload();
} else { } else {
const error = await response.json(); const error = await response.json();
status.textContent = `Error: ${error.error || 'Failed to save'}`; showError(error.error || 'Failed to save');
status.className = 'auth-status error';
btn.disabled = false;
}
} catch (err) {
status.textContent = `Error: ${err.message}`;
status.className = 'auth-status error';
btn.disabled = false;
} }
} catch (err) {
showError(err.message);
} }
}
if (e.target.id === 'telegram-chatid-form') { // biome-ignore lint/correctness/noUnusedVariables: called from HTML onsubmit
e.preventDefault(); async function submitTelegramAuth(e) {
const form = e.target; e.preventDefault();
const status = document.getElementById('telegram-chatid-status'); await handleAuthForm(
const btn = form.querySelector('button[type="submit"]'); e.target,
'/api/telegram/auth',
'telegram-auth-status',
(fd) => ({
bot_token: fd.get('bot_token'),
chat_id: fd.get('chat_id'),
}),
);
}
btn.disabled = true; // biome-ignore lint/correctness/noUnusedVariables: called from HTML onsubmit
async function submitTelegramChatId(e) {
e.preventDefault();
await handleAuthForm(
e.target,
'/api/telegram/auth',
'telegram-chatid-status',
(fd, form) => ({
bot_token: form.dataset.botToken,
chat_id: fd.get('chat_id'),
}),
);
}
try { // biome-ignore lint/correctness/noUnusedVariables: called from HTML onsubmit
const formData = new FormData(form); async function submitProtonAuth(e) {
const botToken = form.dataset.botToken; e.preventDefault();
const response = await fetch('/api/telegram/auth', { await handleAuthForm(
method: 'POST', e.target,
headers: { '/api/proton/auth',
'Content-Type': 'application/json', 'proton-auth-status',
}, (fd) => ({
body: JSON.stringify({ email: fd.get('email'),
bot_token: botToken, password: fd.get('password'),
chat_id: formData.get('chat_id'), totp: fd.get('totp'),
}), }),
}); );
}
if (response.ok) {
location.reload();
} else {
const error = await response.json();
status.textContent = `Error: ${error.error || 'Failed to save'}`;
status.className = 'auth-status error';
btn.disabled = false;
}
} catch (err) {
status.textContent = `Error: ${err.message}`;
status.className = 'auth-status error';
btn.disabled = false;
}
}
if (e.target.id === 'proton-auth-form') {
e.preventDefault();
const form = e.target;
const status = document.getElementById('proton-auth-status');
const btn = form.querySelector('button[type="submit"]');
btn.disabled = true;
try {
const formData = new FormData(form);
const response = await fetch('/api/proton/auth', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: formData.get('email'),
password: formData.get('password'),
totp: formData.get('totp'),
}),
});
if (response.ok) {
location.reload();
} else {
const error = await response.json();
status.textContent = `Error: ${error.error || 'Authentication failed'}`;
status.className = 'auth-status error';
btn.disabled = false;
}
} catch (err) {
status.textContent = `Error: ${err.message}`;
status.className = 'auth-status error';
btn.disabled = false;
}
}
});
let signalLinkingPoll = null; let signalLinkingPoll = null;
document.addEventListener('click', async (e) => { // biome-ignore lint/correctness/noUnusedVariables: called from HTML onclick
if (e.target.id === 'signal-link-btn') { async function linkSignal(btn) {
const btn = e.target; const qrContainer = document.getElementById('signal-qr-container');
const qrContainer = document.getElementById('signal-qr-container'); const qrCode = document.getElementById('signal-qr-code');
const qrCode = document.getElementById('signal-qr-code'); const showQrError = (msg) => {
qrContainer.innerHTML = `<p class="channel-not-configured">Error: ${msg}</p>`;
qrContainer.style.display = 'block';
btn.style.display = 'inline-block';
};
btn.style.display = 'none'; btn.style.display = 'none';
qrContainer.style.display = 'none'; qrContainer.style.display = 'none';
try { try {
const response = await fetch('/api/signal/link', { const response = await fetch('/api/signal/link', {
method: 'POST', method: 'POST',
headers: { headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json', body: JSON.stringify({ device_name: 'Prism' }),
}, });
body: JSON.stringify({ device_name: 'Prism' }),
});
if (!response.ok) { if (!response.ok) {
const error = await response.json(); const error = await response.json();
qrContainer.innerHTML = `<p class="channel-not-configured">Error: ${error.error || 'Failed to generate link'}</p>`; showQrError(error.error || 'Failed to generate link');
qrContainer.style.display = 'block';
btn.style.display = 'inline-block';
return;
}
const data = await response.json();
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(data.qr_code)}`;
qrCode.src = qrUrl;
qrContainer.style.display = 'block';
signalLinkingPoll = setInterval(async () => {
try {
const statusResp = await fetch('/api/signal/status');
const statusData = await statusResp.json();
if (statusData.linked) {
clearInterval(signalLinkingPoll);
qrContainer.innerHTML =
'<p class="auth-status success">Linked! Refreshing...</p>';
setTimeout(() => location.reload(), 1000);
}
} catch (err) {
console.error('Status check failed:', err);
}
}, 2000);
} catch (err) {
qrContainer.innerHTML = `<p class="channel-not-configured">Error: ${err.message}</p>`;
qrContainer.style.display = 'block';
btn.style.display = 'inline-block';
}
}
if (e.target.classList.contains('reload-btn')) {
location.reload();
}
if (e.target.classList.contains('delete-telegram-btn')) {
e.preventDefault();
if (!confirm('Unlink Telegram integration?')) {
return; return;
} }
const btn = e.target; const data = await response.json();
btn.disabled = true; const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(data.qr_code)}`;
qrCode.src = qrUrl;
qrContainer.style.display = 'block';
try { signalLinkingPoll = setInterval(async () => {
const response = await fetch('/api/telegram/auth', { try {
method: 'DELETE', const statusResp = await fetch('/api/signal/status');
}); const statusData = await statusResp.json();
if (response.ok) { if (statusData.linked) {
location.reload(); clearInterval(signalLinkingPoll);
} else { qrContainer.innerHTML =
const error = await response.json(); '<p class="auth-status success">Linked! Refreshing...</p>';
alert(`Error: ${error.error || 'Failed to unlink integration'}`); setTimeout(() => location.reload(), 1000);
btn.disabled = false; }
} catch (err) {
console.error('Status check failed:', err);
} }
} catch (err) { }, 2000);
alert(`Error: ${err.message}`); } catch (err) {
showQrError(err.message);
}
}
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onclick
async function deleteTelegram(btn) {
if (!confirm('Unlink Telegram integration?')) return;
btn.disabled = true;
try {
const response = await fetch('/api/telegram/auth', { method: 'DELETE' });
if (response.ok) {
location.reload();
} else {
const error = await response.json();
alert(`Error: ${error.error || 'Failed to unlink integration'}`);
btn.disabled = false; btn.disabled = false;
} }
} catch (err) {
alert(`Error: ${err.message}`);
btn.disabled = false;
} }
}
if (e.target.classList.contains('delete-proton-btn')) { // biome-ignore lint/correctness/noUnusedVariables: called from HTML onclick
if (!confirm('Unlink Proton Mail integration?')) { async function deleteProton(btn) {
return; if (!confirm('Unlink Proton Mail integration?')) return;
}
btn.disabled = true;
try {
const response = await fetch('/api/proton/auth', { try {
method: 'DELETE', const response = await fetch('/api/proton/auth', { method: 'DELETE' });
});
if (response.ok) {
if (response.ok) { location.reload();
location.reload(); } else {
} else { const error = await response.json();
const error = await response.json(); alert(`Error: ${error.error || 'Failed to unlink integration'}`);
alert(`Error: ${error.error || 'Failed to unlink integration'}`); btn.disabled = false;
}
} catch (err) {
alert(`Error: ${err.message}`);
} }
} catch (err) {
alert(`Error: ${err.message}`);
btn.disabled = false;
} }
}); }

View file

@ -5,7 +5,7 @@
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"background_color": "#1a1a1a", "background_color": "#1a1a1a",
"theme_color": "#8159b8", "theme_color": "#60c5ff",
"icons": [ "icons": [
{ {
"src": "/favicon.webp", "src": "/favicon.webp",

View file

@ -1,8 +1,7 @@
const themes = ['system', 'light', 'dark'];
let currentIndex = 0;
const savedTheme = localStorage.getItem('theme') || 'system'; const savedTheme = localStorage.getItem('theme') || 'system';
currentIndex = themes.indexOf(savedTheme); const themes = ['system', 'light', 'dark'];
let currentIndex = themes.indexOf(savedTheme);
if (currentIndex === -1) currentIndex = 0; if (currentIndex === -1) currentIndex = 0;
function applyTheme(theme) { function applyTheme(theme) {

View file

@ -1,8 +1,8 @@
{{if .Connected}} {{if .Connected}}
<button class="btn-danger delete-proton-btn">Unlink</button> <button class="btn-danger" onclick="deleteProton(this)">Unlink</button>
{{else}} {{else}}
<p><strong>Link Proton Account:</strong></p> <p><strong>Link Proton Account:</strong></p>
<form id="proton-auth-form" class="auth-form"> <form class="auth-form" onsubmit="submitProtonAuth(event)">
<div class="form-group"> <div class="form-group">
<label for="proton-email">Email:</label> <label for="proton-email">Email:</label>
<input type="email" id="proton-email" name="email" placeholder="your-email@proton.me" required> <input type="email" id="proton-email" name="email" placeholder="your-email@proton.me" required>

View file

@ -9,7 +9,7 @@
{{if .Error}} {{if .Error}}
<p class="channel-not-configured">{{.Error}}</p> <p class="channel-not-configured">{{.Error}}</p>
{{else}} {{else}}
<button id="signal-link-btn" class="btn-primary">Link</button> <button class="btn-primary" onclick="linkSignal(this)">Link</button>
<div id="signal-qr-container" class="qr-container" style="display:none;"> <div id="signal-qr-container" class="qr-container" style="display:none;">
<p><strong>Scan this QR code with Signal:</strong></p> <p><strong>Scan this QR code with Signal:</strong></p>
<ol class="link-instructions"> <ol class="link-instructions">

View file

@ -7,7 +7,7 @@
<li>Message <a href="https://t.me/userinfobot" target="_blank">@userinfobot</a> to get your Chat ID</li> <li>Message <a href="https://t.me/userinfobot" target="_blank">@userinfobot</a> to get your Chat ID</li>
<li>Enter both below:</li> <li>Enter both below:</li>
</ol> </ol>
<form id="telegram-auth-form" class="auth-form"> <form class="auth-form" onsubmit="submitTelegramAuth(event)">
<div class="form-group"> <div class="form-group">
<label for="telegram-bot-token">Bot Token:</label> <label for="telegram-bot-token">Bot Token:</label>
<input type="text" id="telegram-bot-token" name="bot_token" placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz" required> <input type="text" id="telegram-bot-token" name="bot_token" placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz" required>
@ -21,11 +21,11 @@
</form> </form>
{{else if .Error}} {{else if .Error}}
<p>Error: {{.Error}}</p> <p>Error: {{.Error}}</p>
<button class="btn-secondary reload-btn">Retry</button> <button class="btn-secondary" onclick="location.reload()">Retry</button>
{{else if .NeedsChatID}} {{else if .NeedsChatID}}
<p><strong>Complete Setup:</strong></p> <p><strong>Complete Setup:</strong></p>
<p>Bot is configured, but Chat ID is missing.</p> <p>Bot is configured, but Chat ID is missing.</p>
<form id="telegram-chatid-form" class="auth-form" data-bot-token="{{.BotToken}}"> <form class="auth-form" data-bot-token="{{.BotToken}}" onsubmit="submitTelegramChatId(event)">
<div class="form-group"> <div class="form-group">
<label for="telegram-chat-id-only">Chat ID:</label> <label for="telegram-chat-id-only">Chat ID:</label>
<input type="text" id="telegram-chat-id-only" name="chat_id" placeholder="Get from @userinfobot" required> <input type="text" id="telegram-chat-id-only" name="chat_id" placeholder="Get from @userinfobot" required>
@ -34,6 +34,6 @@
<div id="telegram-chatid-status" class="auth-status"></div> <div id="telegram-chatid-status" class="auth-status"></div>
</form> </form>
{{else}} {{else}}
<button class="btn-danger delete-telegram-btn">Unlink</button> <button class="btn-danger" onclick="deleteTelegram(this)">Unlink</button>
{{end}} {{end}}

View file

@ -1,13 +0,0 @@
package server
import (
"net/http"
"prism/service/util"
)
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := s.indexTmpl.Execute(w, map[string]string{"Version": s.version}); err != nil {
util.LogAndError(w, s.logger, "Internal Server Error", http.StatusInternalServerError, err)
}
}

View file

@ -10,6 +10,22 @@ import (
"prism/service/util" "prism/service/util"
) )
type AppListItem struct {
AppName string
Channel string
ChannelBadge string
ChannelConfigured bool
Tooltip string
Hostname string
ChannelOptions []SelectOption
}
type SelectOption struct {
Value string
Label string
Selected bool
}
func (s *Server) handleFragmentApps(w http.ResponseWriter, r *http.Request) { func (s *Server) handleFragmentApps(w http.ResponseWriter, r *http.Request) {
mappings, err := s.store.GetAllMappings() mappings, err := s.store.GetAllMappings()
if err != nil { if err != nil {

View file

@ -101,7 +101,11 @@ func (s *Server) setupRoutes() {
r.Get("/health", s.handleHealthCheck) r.Get("/health", s.handleHealthCheck)
r.Head("/health", s.handleHealthCheck) r.Head("/health", s.handleHealthCheck)
r.Get("/", s.handleIndex) r.Get("/", func(w http.ResponseWriter, r *http.Request) {
if err := s.indexTmpl.Execute(w, map[string]string{"Version": s.version}); err != nil {
util.LogAndError(w, s.logger, "Internal Server Error", http.StatusInternalServerError, err)
}
})
r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/favicon.webp", http.StatusMovedPermanently) http.Redirect(w, r, "/favicon.webp", http.StatusMovedPermanently)
}) })

View file

@ -1,29 +0,0 @@
package server
import "html/template"
type AppListItem struct {
AppName string
Channel string
ChannelBadge string
ChannelConfigured bool
Tooltip string
Hostname string
ChannelOptions []SelectOption
}
type SelectOption struct {
Value string
Label string
Selected bool
}
type IntegrationData struct {
Name string
StatusClass string
StatusText string
StatusTooltip string
Content template.HTML
Open bool
PollAttrs string
}

View file

@ -77,17 +77,8 @@ func isLocalIP(addr string) bool {
} }
func GetClientIP(r *http.Request) string { func GetClientIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" { host, _, _ := net.SplitHostPort(r.RemoteAddr)
parts := strings.Split(xff, ",") if host == "" {
return strings.TrimSpace(parts[0])
}
if xri := r.Header.Get("X-Real-IP"); xri != "" {
return xri
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr return r.RemoteAddr
} }
return host return host

View file

@ -7,12 +7,8 @@ import (
) )
func LogAndError(w http.ResponseWriter, logger *slog.Logger, message string, code int, err error, attrs ...any) { func LogAndError(w http.ResponseWriter, logger *slog.Logger, message string, code int, err error, attrs ...any) {
if err != nil { logAttrs := append([]any{"error", err}, attrs...)
logAttrs := append([]any{"error", err}, attrs...) logger.Error(message, logAttrs...)
logger.Error(message, logAttrs...)
} else {
logger.Error(message, attrs...)
}
http.Error(w, message, code) http.Error(w, message, code)
} }