From 1d1afafd1ed732abc090831f411d97c980014d00 Mon Sep 17 00:00:00 2001 From: MrSphay Date: Fri, 1 May 2026 16:09:27 +0200 Subject: [PATCH] Improve desktop UI settings defaults and env inference --- README.md | 104 ++++++++- src/App.tsx | 570 ++++++++++++++++++++++++++++++++++++++++-------- src/env.ts | 114 +++++++--- src/styles.css | 577 +++++++++++++++++++++++++++++++++++-------------- 4 files changed, 1073 insertions(+), 292 deletions(-) diff --git a/README.md b/README.md index e4c54ce..38fc75e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,88 @@ # EnvHelper -EnvHelper ist eine Windows-Desktop-App zum Ersetzen von `CHANGE_ME` Platzhaltern in `.env` Dateien. +**EnvHelper** ist eine lokale Windows-Desktop-App zum Ausfüllen von `.env` Vorlagen. Sie erkennt `CHANGE_ME` Platzhalter, erzeugt passende Werte im erwarteten Format und gibt eine fertige `.env` aus. -## Funktionen +```text +Status: Desktop App +Plattform: Windows +Stack: Electron, React, Vite, TypeScript +Build: Gitea Actions Runner +``` + +## Highlights - `.env` Datei laden oder reinen Text einfügen -- `CHANGE_ME...` Platzhalter erkennen -- passende Werte anhand von Variablenname und Platzhalter ableiten -- gleiche Platzhalter konsistent mit demselben Wert ersetzen -- neue `.env` als Text kopieren oder speichern +- `CHANGE_ME...` Platzhalter automatisch erkennen +- Passwörter, Secrets, Base64-Keys, UUIDs, Ports, URLs und E-Mails heuristisch erzeugen +- Gleiche Placeholder konsistent mit demselben Wert ersetzen +- Standardwerte pro Key definieren und im Output überschreiben lassen +- Hell/Dunkel/System Theme +- Einstellungen mit Sprache, Version und Projektinfos +- Ausgabe kopieren oder als neue `.env` speichern + +## Vorschau + +Screenshots können später im Repository ergänzt werden. + +```text +┌─────────────────────────────────────────────────────────────┐ +│ EnvHelper Datei laden .env speichern Theme Settings │ +├─────────────────────────────┬───────────────────────────────┤ +│ Input │ Output │ +│ DATABASE_URL=...CHANGE_ME │ DATABASE_URL=...generated... │ +├─────────────────────────────┴───────────────────────────────┤ +│ Standardwerte Erkannte Anforderungen │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Beispiel + +Eingabe: + +```env +DATABASE_URL=postgresql://hilden_app:CHANGE_ME_POSTGRES_PASSWORD@postgres:5432/hilden_directory +POSTGRES_PASSWORD=CHANGE_ME_POSTGRES_PASSWORD +SESSION_SECRET=CHANGE_ME_AT_LEAST_32_RANDOM_CHARACTERS +ENCRYPTION_KEY_BASE64=CHANGE_ME_32_RANDOM_BYTES_AS_BASE64 +BOOTSTRAP_ADMIN_EMAIL=admin@example.local +``` + +Ausgabe: + +```env +DATABASE_URL=postgresql://hilden_app:generated-postgres-password@postgres:5432/hilden_directory +POSTGRES_PASSWORD=generated-postgres-password +SESSION_SECRET=generated-url-safe-secret +ENCRYPTION_KEY_BASE64=generated-base64-key +BOOTSTRAP_ADMIN_EMAIL=admin@example.local +``` + +Die tatsächlichen Werte werden zufällig lokal erzeugt. + +## Standardwerte + +Standardwerte werden in einer eigenen Sektion gepflegt. Wenn ein Key dort eingetragen ist, überschreibt der Wert den entsprechenden Output-Key. + +Beispiel: + +```text +BOOTSTRAP_ADMIN_EMAIL = admin@example.local +``` + +Dann wird im Output immer dieser Wert gesetzt, auch wenn die Vorlage bereits eine andere E-Mail enthält. + +## Sicherheit + +EnvHelper arbeitet lokal. Die Werte werden im Renderer mit Web-Crypto erzeugt und nicht an externe Dienste gesendet. Die App ist ein Helfer für Vorlagen, ersetzt aber keine zentrale Secret-Verwaltung für produktive Infrastruktur. + +## Download und Artefakte + +Der Windows-Build erzeugt zwei Dateien: + +- `EnvHelper-0.1.0-setup-x64.exe` +- `EnvHelper-0.1.0-portable-x64.exe` + +Die Dateien werden vom Gitea Runner als Actions-Artefakt und als Generic Package veröffentlicht. ## Entwicklung @@ -17,7 +91,7 @@ npm install npm run dev ``` -In einem zweiten Terminal: +In einem zweiten Terminal kann der Electron-Build geprüft werden: ```bash npm run build @@ -25,10 +99,22 @@ npm run build ## Windows Build -Der Windows-Build läuft über Gitea Actions: +Der Windows-Build läuft über Gitea Actions und nutzt den Linux Runner mit Wine: ```bash npm run dist:win ``` -Die erzeugten Dateien liegen im Runner unter `release/` und werden als Workflow-Artefakt hochgeladen. +Workflow: + +```text +.gitea/workflows/build-windows.yml +``` + +Der Runner installiert Node.js, Wine, baut die App mit Electron Builder und lädt die fertigen `.exe` Artefakte hoch. + +## Projektinfos + +- Autor: `MrSphay` +- Repository: `MrSphay/envHelper` +- App-ID: `de.wilkensxl.envhelper` diff --git a/src/App.tsx b/src/App.tsx index eb74a73..9cb61a7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,25 @@ -import { Check, Clipboard, FileDown, FileInput, RefreshCcw, ShieldCheck, Sparkles } from "lucide-react"; -import { useMemo, useState } from "react"; +import { + Check, + Clipboard, + FileDown, + FileInput, + Languages, + Plus, + RefreshCcw, + Settings, + ShieldCheck, + SunMoon, + Trash2, + X +} from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import type { ReactNode } from "react"; +import packageInfo from "../package.json"; import { transformEnv } from "./env"; +import type { EnvDefault } from "./env"; + +type ThemeMode = "system" | "light" | "dark"; +type Language = "de" | "en" | "es" | "fr" | "nl"; const sampleEnv = `APP_IMAGE=git.wilkensxl.de/mrsphay/hilden-directory-gateway:latest APP_PORT=3000 @@ -18,12 +37,264 @@ ENCRYPTION_KEY_BASE64=CHANGE_ME_32_RANDOM_BYTES_AS_BASE64 BOOTSTRAP_ADMIN_EMAIL=admin@example.local BOOTSTRAP_ADMIN_PASSWORD=CHANGE_ME_LONG_INITIAL_ADMIN_PASSWORD`; +const initialDefaults: EnvDefault[] = [{ key: "BOOTSTRAP_ADMIN_EMAIL", value: "admin@example.local" }]; + +const languageNames: Record = { + de: "Deutsch", + en: "English", + es: "Español", + fr: "Français", + nl: "Nederlands" +}; + +const translations = { + de: { + appSubtitle: "Lokaler Helfer für .env Vorlagen", + loadFile: "Datei laden", + saveFile: ".env speichern", + settings: "Einstellungen", + input: "Input", + output: "Output", + inputHint: "Datei laden oder Text direkt bearbeiten", + outputHint: "Generierte .env, bereit zum Kopieren", + copy: "Kopieren", + copied: "Kopiert", + regenerate: "Neu erzeugen", + sample: "Beispiel laden", + hits: "Fundstellen", + values: "Werte", + defaultsApplied: "Defaults", + source: "Quelle", + text: "Text", + defaults: "Standardwerte", + defaultsHint: "Diese Werte überschreiben passende Keys im Output.", + addDefault: "Standardwert hinzufügen", + key: "Key", + value: "Wert", + detected: "Erkannte Anforderungen", + detectedHint: "Placeholder, betroffene Keys und erzeugtes Format.", + placeholder: "Placeholder", + keys: "Keys", + format: "Format", + noPlaceholders: "Keine CHANGE_ME Platzhalter gefunden.", + language: "Sprache", + theme: "Darstellung", + system: "System", + light: "Hell", + dark: "Dunkel", + version: "Version", + author: "Autor", + repository: "Repository" + }, + en: { + appSubtitle: "Local helper for .env templates", + loadFile: "Load file", + saveFile: "Save .env", + settings: "Settings", + input: "Input", + output: "Output", + inputHint: "Load a file or edit text directly", + outputHint: "Generated .env, ready to copy", + copy: "Copy", + copied: "Copied", + regenerate: "Regenerate", + sample: "Load sample", + hits: "Matches", + values: "Values", + defaultsApplied: "Defaults", + source: "Source", + text: "Text", + defaults: "Defaults", + defaultsHint: "These values override matching keys in the output.", + addDefault: "Add default", + key: "Key", + value: "Value", + detected: "Detected requirements", + detectedHint: "Placeholder, affected keys, and generated format.", + placeholder: "Placeholder", + keys: "Keys", + format: "Format", + noPlaceholders: "No CHANGE_ME placeholders found.", + language: "Language", + theme: "Appearance", + system: "System", + light: "Light", + dark: "Dark", + version: "Version", + author: "Author", + repository: "Repository" + }, + es: { + appSubtitle: "Ayudante local para plantillas .env", + loadFile: "Cargar archivo", + saveFile: "Guardar .env", + settings: "Ajustes", + input: "Entrada", + output: "Salida", + inputHint: "Carga un archivo o edita el texto", + outputHint: ".env generado, listo para copiar", + copy: "Copiar", + copied: "Copiado", + regenerate: "Regenerar", + sample: "Cargar ejemplo", + hits: "Coincidencias", + values: "Valores", + defaultsApplied: "Defaults", + source: "Origen", + text: "Texto", + defaults: "Valores predeterminados", + defaultsHint: "Estos valores sobrescriben claves iguales en la salida.", + addDefault: "Añadir valor", + key: "Clave", + value: "Valor", + detected: "Requisitos detectados", + detectedHint: "Placeholder, claves afectadas y formato generado.", + placeholder: "Placeholder", + keys: "Claves", + format: "Formato", + noPlaceholders: "No se encontraron placeholders CHANGE_ME.", + language: "Idioma", + theme: "Apariencia", + system: "Sistema", + light: "Claro", + dark: "Oscuro", + version: "Versión", + author: "Autor", + repository: "Repositorio" + }, + fr: { + appSubtitle: "Assistant local pour modèles .env", + loadFile: "Charger", + saveFile: "Enregistrer .env", + settings: "Réglages", + input: "Entrée", + output: "Sortie", + inputHint: "Chargez un fichier ou modifiez le texte", + outputHint: ".env généré, prêt à copier", + copy: "Copier", + copied: "Copié", + regenerate: "Régénérer", + sample: "Charger exemple", + hits: "Occurrences", + values: "Valeurs", + defaultsApplied: "Défauts", + source: "Source", + text: "Texte", + defaults: "Valeurs par défaut", + defaultsHint: "Ces valeurs remplacent les clés correspondantes dans la sortie.", + addDefault: "Ajouter", + key: "Clé", + value: "Valeur", + detected: "Exigences détectées", + detectedHint: "Placeholder, clés concernées et format généré.", + placeholder: "Placeholder", + keys: "Clés", + format: "Format", + noPlaceholders: "Aucun placeholder CHANGE_ME trouvé.", + language: "Langue", + theme: "Apparence", + system: "Système", + light: "Clair", + dark: "Sombre", + version: "Version", + author: "Auteur", + repository: "Dépôt" + }, + nl: { + appSubtitle: "Lokale helper voor .env sjablonen", + loadFile: "Bestand laden", + saveFile: ".env opslaan", + settings: "Instellingen", + input: "Input", + output: "Output", + inputHint: "Laad een bestand of bewerk tekst direct", + outputHint: "Gegenereerde .env, klaar om te kopiëren", + copy: "Kopiëren", + copied: "Gekopieerd", + regenerate: "Opnieuw maken", + sample: "Voorbeeld laden", + hits: "Treffers", + values: "Waarden", + defaultsApplied: "Defaults", + source: "Bron", + text: "Tekst", + defaults: "Standaardwaarden", + defaultsHint: "Deze waarden overschrijven gelijke keys in de output.", + addDefault: "Waarde toevoegen", + key: "Key", + value: "Waarde", + detected: "Gedetecteerde eisen", + detectedHint: "Placeholder, betrokken keys en gegenereerd formaat.", + placeholder: "Placeholder", + keys: "Keys", + format: "Formaat", + noPlaceholders: "Geen CHANGE_ME placeholders gevonden.", + language: "Taal", + theme: "Weergave", + system: "Systeem", + light: "Licht", + dark: "Donker", + version: "Versie", + author: "Auteur", + repository: "Repository" + } +} satisfies Record>; + +const themeLabels: ThemeMode[] = ["system", "light", "dark"]; + +function detectLanguage(): Language { + const stored = localStorage.getItem("envhelper-language") as Language | null; + if (stored && stored in languageNames) { + return stored; + } + + const browserLanguage = navigator.language.slice(0, 2) as Language; + return browserLanguage in languageNames ? browserLanguage : "de"; +} + +function readDefaults(): EnvDefault[] { + const stored = localStorage.getItem("envhelper-defaults"); + if (!stored) { + return initialDefaults; + } + + try { + const parsed = JSON.parse(stored) as EnvDefault[]; + return Array.isArray(parsed) ? parsed : initialDefaults; + } catch { + return initialDefaults; + } +} + +function readTheme(): ThemeMode { + const stored = localStorage.getItem("envhelper-theme") as ThemeMode | null; + return stored && themeLabels.includes(stored) ? stored : "system"; +} + export default function App() { const [input, setInput] = useState(sampleEnv); const [loadedPath, setLoadedPath] = useState(null); - const [copyLabel, setCopyLabel] = useState("Kopieren"); + const [copyLabel, setCopyLabel] = useState<"copy" | "copied">("copy"); const [generation, setGeneration] = useState(0); - const result = useMemo(() => transformEnv(input), [input, generation]); + const [settingsOpen, setSettingsOpen] = useState(false); + const [themeMode, setThemeMode] = useState(readTheme); + const [language, setLanguage] = useState(detectLanguage); + const [defaults, setDefaults] = useState(readDefaults); + const t = translations[language]; + const result = useMemo(() => transformEnv(input, defaults), [input, defaults, generation]); + + useEffect(() => { + document.documentElement.dataset.theme = themeMode; + localStorage.setItem("envhelper-theme", themeMode); + }, [themeMode]); + + useEffect(() => { + localStorage.setItem("envhelper-language", language); + }, [language]); + + useEffect(() => { + localStorage.setItem("envhelper-defaults", JSON.stringify(defaults)); + }, [defaults]); async function openFile() { const file = await window.envHelper?.openFile(); @@ -39,112 +310,219 @@ export default function App() { async function copyOutput() { await navigator.clipboard.writeText(result.output); - setCopyLabel("Kopiert"); - window.setTimeout(() => setCopyLabel("Kopieren"), 1400); + setCopyLabel("copied"); + window.setTimeout(() => setCopyLabel("copy"), 1400); } - function regenerate() { - setGeneration((current) => current + 1); + function updateDefault(index: number, field: keyof EnvDefault, value: string) { + setDefaults((current) => current.map((entry, entryIndex) => (entryIndex === index ? { ...entry, [field]: value } : entry))); + } + + function addDefault() { + setDefaults((current) => [...current, { key: "", value: "" }]); + } + + function removeDefault(index: number) { + setDefaults((current) => current.filter((_, entryIndex) => entryIndex !== index)); } return ( -
-
-
-

EnvHelper

-

.env Platzhalter sauber ersetzen

-

- Erkennt CHANGE_ME Werte, erzeugt passende Secrets im richtigen Format und hält gleiche Platzhalter synchron. -

+
+
+
+
EH
+
+

EnvHelper

+

{t.appSubtitle}

+
-
+ +
-
-
- -
-
- {result.changedCount} -

Fundstellen

-
-
- {result.replacements.length} -

eindeutige Werte

-
-
- {loadedPath ? "Datei" : "Text"} -

Quelle

-
-
- -
-
-
-
-

Input

-

{loadedPath ?? "Direkt einfügen oder Beispieldaten ersetzen"}

-
- -
-