code clean ups, minor improvements

This commit is contained in:
Egor 2026-02-12 23:13:16 -08:00
parent 07f8768b20
commit 360df3c0f1
20 changed files with 215 additions and 294 deletions

View file

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

View file

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

View file

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

View file

@ -2,14 +2,13 @@
BINARY_NAME=prism
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
export PATH := $(GOBIN):$(PATH)
all: fix 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
./$(BINARY_NAME)

View file

@ -1 +1 @@
0.2.1
0.2.2

View file

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

View file

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

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<title>Prism Admin</title>
<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="manifest" href="/manifest.json">
<link rel="stylesheet" href="/index.css?v={{.Version}}">

View file

@ -1,122 +1,87 @@
document.addEventListener('submit', async (e) => {
if (e.target.id === 'telegram-auth-form') {
e.preventDefault();
const form = e.target;
const status = document.getElementById('telegram-auth-status');
async function handleAuthForm(form, endpoint, statusId, getPayload) {
const status = document.getElementById(statusId);
const btn = form.querySelector('button[type="submit"]');
const showError = (msg) => {
status.textContent = `Error: ${msg}`;
status.className = 'auth-status error';
btn.disabled = false;
};
btn.disabled = true;
try {
const formData = new FormData(form);
const response = await fetch('/api/telegram/auth', {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
bot_token: formData.get('bot_token'),
chat_id: formData.get('chat_id'),
}),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(getPayload(formData, form)),
});
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;
showError(error.error || 'Failed to save');
}
} catch (err) {
status.textContent = `Error: ${err.message}`;
status.className = 'auth-status error';
btn.disabled = false;
}
showError(err.message);
}
}
if (e.target.id === 'telegram-chatid-form') {
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onsubmit
async function submitTelegramAuth(e) {
e.preventDefault();
const form = e.target;
const status = document.getElementById('telegram-chatid-status');
const btn = form.querySelector('button[type="submit"]');
btn.disabled = true;
try {
const formData = new FormData(form);
const botToken = form.dataset.botToken;
const response = await fetch('/api/telegram/auth', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
bot_token: botToken,
chat_id: formData.get('chat_id'),
await handleAuthForm(
e.target,
'/api/telegram/auth',
'telegram-auth-status',
(fd) => ({
bot_token: fd.get('bot_token'),
chat_id: fd.get('chat_id'),
}),
});
);
}
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') {
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onsubmit
async function submitTelegramChatId(e) {
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'),
await handleAuthForm(
e.target,
'/api/telegram/auth',
'telegram-chatid-status',
(fd, form) => ({
bot_token: form.dataset.botToken,
chat_id: fd.get('chat_id'),
}),
});
);
}
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;
}
}
});
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onsubmit
async function submitProtonAuth(e) {
e.preventDefault();
await handleAuthForm(
e.target,
'/api/proton/auth',
'proton-auth-status',
(fd) => ({
email: fd.get('email'),
password: fd.get('password'),
totp: fd.get('totp'),
}),
);
}
let signalLinkingPoll = null;
document.addEventListener('click', async (e) => {
if (e.target.id === 'signal-link-btn') {
const btn = e.target;
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onclick
async function linkSignal(btn) {
const qrContainer = document.getElementById('signal-qr-container');
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';
qrContainer.style.display = 'none';
@ -124,17 +89,13 @@ document.addEventListener('click', async (e) => {
try {
const response = await fetch('/api/signal/link', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_name: 'Prism' }),
});
if (!response.ok) {
const error = await response.json();
qrContainer.innerHTML = `<p class="channel-not-configured">Error: ${error.error || 'Failed to generate link'}</p>`;
qrContainer.style.display = 'block';
btn.style.display = 'inline-block';
showQrError(error.error || 'Failed to generate link');
return;
}
@ -159,30 +120,18 @@ document.addEventListener('click', async (e) => {
}
}, 2000);
} catch (err) {
qrContainer.innerHTML = `<p class="channel-not-configured">Error: ${err.message}</p>`;
qrContainer.style.display = 'block';
btn.style.display = 'inline-block';
}
showQrError(err.message);
}
}
if (e.target.classList.contains('reload-btn')) {
location.reload();
}
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onclick
async function deleteTelegram(btn) {
if (!confirm('Unlink Telegram integration?')) return;
if (e.target.classList.contains('delete-telegram-btn')) {
e.preventDefault();
if (!confirm('Unlink Telegram integration?')) {
return;
}
const btn = e.target;
btn.disabled = true;
try {
const response = await fetch('/api/telegram/auth', {
method: 'DELETE',
});
const response = await fetch('/api/telegram/auth', { method: 'DELETE' });
if (response.ok) {
location.reload();
@ -195,26 +144,26 @@ document.addEventListener('click', async (e) => {
alert(`Error: ${err.message}`);
btn.disabled = false;
}
}
}
if (e.target.classList.contains('delete-proton-btn')) {
if (!confirm('Unlink Proton Mail integration?')) {
return;
}
// biome-ignore lint/correctness/noUnusedVariables: called from HTML onclick
async function deleteProton(btn) {
if (!confirm('Unlink Proton Mail integration?')) return;
btn.disabled = true;
try {
const response = await fetch('/api/proton/auth', {
method: 'DELETE',
});
const response = await fetch('/api/proton/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;
}
} catch (err) {
alert(`Error: ${err.message}`);
btn.disabled = false;
}
}
});
}

View file

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

View file

@ -1,8 +1,7 @@
const themes = ['system', 'light', 'dark'];
let currentIndex = 0;
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;
function applyTheme(theme) {

View file

@ -1,8 +1,8 @@
{{if .Connected}}
<button class="btn-danger delete-proton-btn">Unlink</button>
<button class="btn-danger" onclick="deleteProton(this)">Unlink</button>
{{else}}
<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">
<label for="proton-email">Email:</label>
<input type="email" id="proton-email" name="email" placeholder="your-email@proton.me" required>

View file

@ -9,7 +9,7 @@
{{if .Error}}
<p class="channel-not-configured">{{.Error}}</p>
{{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;">
<p><strong>Scan this QR code with Signal:</strong></p>
<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>Enter both below:</li>
</ol>
<form id="telegram-auth-form" class="auth-form">
<form class="auth-form" onsubmit="submitTelegramAuth(event)">
<div class="form-group">
<label for="telegram-bot-token">Bot Token:</label>
<input type="text" id="telegram-bot-token" name="bot_token" placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz" required>
@ -21,11 +21,11 @@
</form>
{{else if .Error}}
<p>Error: {{.Error}}</p>
<button class="btn-secondary reload-btn">Retry</button>
<button class="btn-secondary" onclick="location.reload()">Retry</button>
{{else if .NeedsChatID}}
<p><strong>Complete Setup:</strong></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">
<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>
@ -34,6 +34,6 @@
<div id="telegram-chatid-status" class="auth-status"></div>
</form>
{{else}}
<button class="btn-danger delete-telegram-btn">Unlink</button>
<button class="btn-danger" onclick="deleteTelegram(this)">Unlink</button>
{{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"
)
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) {
mappings, err := s.store.GetAllMappings()
if err != nil {

View file

@ -101,7 +101,11 @@ func (s *Server) setupRoutes() {
r.Get("/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) {
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 {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
parts := strings.Split(xff, ",")
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 {
host, _, _ := net.SplitHostPort(r.RemoteAddr)
if host == "" {
return r.RemoteAddr
}
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) {
if err != nil {
logAttrs := append([]any{"error", err}, attrs...)
logger.Error(message, logAttrs...)
} else {
logger.Error(message, attrs...)
}
http.Error(w, message, code)
}