9883abda79
Replace Amber brand color with Violet (#6c3aed) across the entire app. - tokens.css: accent palette → Violet (light #6c3aed, dark #a78bfa) - Logo, favicon, PWA icons, Apple touch icon regenerated - GitHub Pages (index.html, install.html): accent + inline SVG updated - generate-icons.js: gradient updated for future icon generation - Semantic colors (warning, notes, meals) intentionally unchanged
1237 lines
74 KiB
HTML
1237 lines
74 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Install Oikos — Self-Hosted Family Planner</title>
|
|
<meta name="description" content="Step-by-step installation guide for Oikos. Get your self-hosted family planner running in minutes with Docker.">
|
|
|
|
<link rel="canonical" href="https://ulsklyc.github.io/oikos/install.html">
|
|
<link rel="icon" type="image/svg+xml" href="logo.svg">
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300..800;1,9..40,300..800&family=DM+Serif+Display&display=swap" rel="stylesheet">
|
|
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--ff-display: 'DM Serif Display', Georgia, serif;
|
|
--ff-body: 'DM Sans', system-ui, -apple-system, sans-serif;
|
|
--font-mono: 'SF Mono', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace;
|
|
|
|
--bg: #FAFAFB;
|
|
--bg-alt: #F2F1F6;
|
|
--surface: #FFFFFF;
|
|
--border: #E5E4EA;
|
|
--text-1: #181620;
|
|
--text-2: #5E5C6B;
|
|
--text-3: #8F8D9A;
|
|
--accent: #6c3aed;
|
|
--accent-hover: #5b2fd4;
|
|
--accent-soft: #ede9fe;
|
|
--accent-glow: rgba(108, 58, 237, 0.12);
|
|
--success: #15803D;
|
|
--success-bg: #F0FDF4;
|
|
--warning: #B45309;
|
|
--warning-bg: #FFFBEB;
|
|
--code-bg: #1C1B22;
|
|
--code-text: #E2E1EC;
|
|
--shadow-card: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.03);
|
|
--shadow-lg: 0 8px 32px rgba(0,0,0,0.08);
|
|
--radius: 16px;
|
|
}
|
|
|
|
[data-theme="dark"] {
|
|
--bg: #1A1A18;
|
|
--bg-alt: #141413;
|
|
--surface: #222220;
|
|
--border: #2A2A28;
|
|
--text-1: #F5F4F1;
|
|
--text-2: #AEADB0;
|
|
--text-3: #8E8D89;
|
|
--accent: #a78bfa;
|
|
--accent-hover: #9066f5;
|
|
--accent-soft: #1e1040;
|
|
--accent-glow: rgba(167, 139, 250, 0.15);
|
|
--code-bg: #141413;
|
|
--code-text: #E2E1DC;
|
|
--shadow-card: 0 1px 3px rgba(0,0,0,0.25);
|
|
--shadow-lg: 0 8px 24px rgba(0,0,0,0.45);
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--bg: #1A1A18; --bg-alt: #141413; --surface: #222220; --border: #2A2A28;
|
|
--text-1: #F5F4F1; --text-2: #AEADB0; --text-3: #8E8D89;
|
|
--accent: #a78bfa; --accent-hover: #9066f5; --accent-soft: #1e1040;
|
|
--accent-glow: rgba(167, 139, 250, 0.15);
|
|
--code-bg: #141413; --code-text: #E2E1DC;
|
|
--shadow-card: 0 1px 3px rgba(0,0,0,0.25); --shadow-lg: 0 8px 24px rgba(0,0,0,0.45);
|
|
}
|
|
}
|
|
|
|
html { scroll-behavior: smooth; }
|
|
body {
|
|
font-family: var(--ff-body);
|
|
background: var(--bg);
|
|
color: var(--text-1);
|
|
line-height: 1.6;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
overflow-x: hidden;
|
|
}
|
|
a { color: var(--accent); text-decoration: none; }
|
|
a:hover { text-decoration: underline; }
|
|
a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
|
|
|
|
.container { max-width: 800px; margin: 0 auto; padding: 0 20px; }
|
|
.container-wide { max-width: 1120px; margin: 0 auto; padding: 0 20px; }
|
|
|
|
/* Nav */
|
|
.nav {
|
|
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
|
background: color-mix(in srgb, var(--bg) 85%, transparent);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
border-bottom: 1px solid var(--border);
|
|
height: 56px;
|
|
}
|
|
.nav .container-wide { display: flex; align-items: center; justify-content: space-between; height: 100%; }
|
|
.nav-logo {
|
|
display: flex; align-items: center; gap: 10px;
|
|
font-family: var(--ff-display); font-weight: 700; font-size: 1.25rem;
|
|
color: var(--text-1); text-decoration: none;
|
|
}
|
|
.nav-logo:hover { text-decoration: none; }
|
|
.nav-logo svg { width: 28px; height: 28px; }
|
|
.nav-back {
|
|
display: flex; align-items: center; gap: 6px;
|
|
font-size: 0.8125rem; color: var(--text-2);
|
|
text-decoration: none;
|
|
padding: 4px 8px; border-radius: 6px;
|
|
transition: color 0.15s ease, background 0.15s ease;
|
|
}
|
|
.nav-back:hover { color: var(--text-1); background: var(--surface); text-decoration: none; }
|
|
.nav-back svg { width: 14px; height: 14px; }
|
|
.nav-controls { display: flex; align-items: center; gap: 6px; }
|
|
.nav-btn {
|
|
background: none; border: 1px solid var(--border);
|
|
color: var(--text-2); cursor: pointer;
|
|
padding: 6px 10px; border-radius: 8px;
|
|
font-size: 0.8125rem; font-family: var(--ff-body);
|
|
display: flex; align-items: center; gap: 4px;
|
|
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
|
}
|
|
.nav-btn:hover { background: var(--surface); color: var(--text-1); border-color: var(--text-3); }
|
|
.nav-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
.nav-btn svg { width: 16px; height: 16px; }
|
|
|
|
/* Hero */
|
|
.hero {
|
|
padding: 108px 0 56px;
|
|
position: relative; overflow: hidden;
|
|
}
|
|
.hero::before {
|
|
content: ''; position: absolute; top: -80px; left: 50%;
|
|
transform: translateX(-50%); width: 700px; height: 500px;
|
|
background: radial-gradient(ellipse at center, color-mix(in srgb, var(--accent) 7%, transparent) 0%, transparent 70%);
|
|
pointer-events: none; z-index: 0;
|
|
}
|
|
.hero > .container > * { position: relative; z-index: 1; }
|
|
.hero-eyebrow {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
font-size: 0.75rem; font-weight: 600; letter-spacing: 0.08em;
|
|
text-transform: uppercase; color: var(--accent);
|
|
margin-bottom: 14px;
|
|
}
|
|
.hero-eyebrow svg { width: 13px; height: 13px; }
|
|
.hero h1 {
|
|
font-family: var(--ff-display);
|
|
font-size: clamp(2rem, 5vw, 3rem);
|
|
font-weight: 800; line-height: 1.1;
|
|
letter-spacing: -0.02em; margin-bottom: 14px;
|
|
}
|
|
.hero-desc {
|
|
font-size: 1.0625rem; color: var(--text-2);
|
|
max-width: 540px; line-height: 1.7; margin-bottom: 28px;
|
|
}
|
|
.time-badge {
|
|
display: inline-flex; align-items: center; gap: 8px;
|
|
background: var(--success-bg);
|
|
border: 1px solid color-mix(in srgb, var(--success) 25%, transparent);
|
|
color: var(--success); border-radius: 999px;
|
|
padding: 6px 14px; font-size: 0.8125rem; font-weight: 500;
|
|
}
|
|
.time-badge svg { width: 14px; height: 14px; }
|
|
|
|
/* Section */
|
|
section { padding: 56px 0; }
|
|
.section-label {
|
|
font-size: 0.75rem; font-weight: 600; letter-spacing: 0.08em;
|
|
text-transform: uppercase; color: var(--accent); margin-bottom: 8px;
|
|
}
|
|
.section-title {
|
|
font-family: var(--ff-display);
|
|
font-size: clamp(1.5rem, 3vw, 2rem);
|
|
font-weight: 700; line-height: 1.2;
|
|
letter-spacing: -0.015em; margin-bottom: 10px;
|
|
}
|
|
.section-desc { font-size: 1rem; color: var(--text-2); line-height: 1.7; margin-bottom: 32px; }
|
|
|
|
/* Prerequisites */
|
|
.prereq-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
|
|
.prereq-card {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius); padding: 20px;
|
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
.prereq-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-card); }
|
|
.prereq-icon {
|
|
width: 40px; height: 40px; border-radius: 10px;
|
|
background: var(--accent-soft); color: var(--accent);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 1.25rem; margin-bottom: 12px;
|
|
}
|
|
.prereq-card h3 { font-family: var(--ff-display); font-size: 1rem; font-weight: 600; margin-bottom: 4px; }
|
|
.prereq-card p { font-size: 0.8125rem; color: var(--text-2); line-height: 1.5; margin-bottom: 8px; }
|
|
.prereq-links { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
.prereq-link {
|
|
font-size: 0.75rem; font-weight: 500; color: var(--accent);
|
|
background: var(--accent-soft); border-radius: 6px;
|
|
padding: 3px 8px; text-decoration: none;
|
|
}
|
|
.prereq-link:hover { text-decoration: underline; }
|
|
|
|
/* Steps */
|
|
.steps-section { background: var(--bg-alt); }
|
|
.tab-bar {
|
|
display: flex; gap: 4px; margin-bottom: 32px;
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius); padding: 4px; width: fit-content;
|
|
}
|
|
.tab-btn {
|
|
background: none; border: none; cursor: pointer;
|
|
font-family: var(--ff-body); font-size: 0.875rem; font-weight: 500;
|
|
color: var(--text-2); padding: 8px 18px; border-radius: 8px;
|
|
transition: background 0.15s ease, color 0.15s ease;
|
|
}
|
|
.tab-btn.active { background: var(--accent); color: #fff; }
|
|
.tab-btn:hover:not(.active) { background: var(--bg-alt); color: var(--text-1); }
|
|
.tab-panel { display: none; }
|
|
.tab-panel.active { display: block; }
|
|
|
|
.recommended-badge {
|
|
display: inline-flex; align-items: center; gap: 4px;
|
|
font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.05em;
|
|
text-transform: uppercase; color: var(--success);
|
|
background: var(--success-bg);
|
|
border: 1px solid color-mix(in srgb, var(--success) 20%, transparent);
|
|
border-radius: 999px; padding: 2px 8px; margin-left: 8px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.step {
|
|
display: grid; grid-template-columns: 48px 1fr;
|
|
gap: 0 20px; margin-bottom: 32px; position: relative;
|
|
}
|
|
.step:not(:last-child)::before {
|
|
content: ''; position: absolute;
|
|
left: 23px; top: 48px; bottom: -4px; width: 2px;
|
|
background: var(--border);
|
|
}
|
|
.step-num {
|
|
width: 48px; height: 48px; border-radius: 50%;
|
|
background: var(--accent); color: #fff;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-family: var(--ff-display); font-size: 1.125rem; font-weight: 700;
|
|
flex-shrink: 0; position: relative; z-index: 1;
|
|
box-shadow: var(--shadow-card);
|
|
}
|
|
.step-content { padding-top: 10px; }
|
|
.step-title {
|
|
font-family: var(--ff-display); font-size: 1.125rem; font-weight: 600;
|
|
margin-bottom: 8px;
|
|
}
|
|
.step-desc { font-size: 0.9375rem; color: var(--text-2); line-height: 1.65; margin-bottom: 12px; }
|
|
.step-desc code {
|
|
font-family: var(--font-mono); font-size: 0.8125rem;
|
|
background: var(--accent-soft); color: var(--accent);
|
|
border-radius: 4px; padding: 1px 5px;
|
|
}
|
|
|
|
/* Code blocks */
|
|
.code-wrap { position: relative; margin: 12px 0 4px; }
|
|
.code-block {
|
|
background: var(--code-bg); color: var(--code-text);
|
|
border-radius: var(--radius); padding: 18px 20px;
|
|
font-family: var(--font-mono); font-size: 0.8125rem;
|
|
line-height: 1.8; overflow-x: auto;
|
|
}
|
|
.code-block .comment { color: #6C6B67; }
|
|
.code-block .cmd { color: #60A5FA; }
|
|
.code-block .val { color: #86EFAC; }
|
|
.copy-btn {
|
|
position: absolute; top: 10px; right: 10px;
|
|
background: color-mix(in srgb, #fff 10%, transparent);
|
|
border: 1px solid color-mix(in srgb, #fff 15%, transparent);
|
|
color: #A8A8A6; cursor: pointer; border-radius: 6px;
|
|
padding: 4px 10px; font-size: 0.75rem; font-family: var(--ff-body);
|
|
transition: background 0.15s ease, color 0.15s ease;
|
|
display: flex; align-items: center; gap: 5px;
|
|
}
|
|
.copy-btn:hover { background: color-mix(in srgb, #fff 18%, transparent); color: #fff; }
|
|
.copy-btn svg { width: 13px; height: 13px; }
|
|
.copy-btn.copied { color: #86EFAC; }
|
|
|
|
/* Callouts */
|
|
.callout {
|
|
border-radius: var(--radius); padding: 14px 18px;
|
|
font-size: 0.875rem; line-height: 1.6; margin: 12px 0;
|
|
display: flex; gap: 10px; align-items: flex-start;
|
|
}
|
|
.callout svg { width: 16px; height: 16px; flex-shrink: 0; margin-top: 2px; }
|
|
.callout-warning {
|
|
background: var(--warning-bg);
|
|
border: 1px solid color-mix(in srgb, var(--warning) 25%, transparent);
|
|
color: var(--warning);
|
|
}
|
|
.callout-info {
|
|
background: var(--accent-soft);
|
|
border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
|
|
color: var(--accent);
|
|
}
|
|
.callout-success {
|
|
background: var(--success-bg);
|
|
border: 1px solid color-mix(in srgb, var(--success) 25%, transparent);
|
|
color: var(--success);
|
|
}
|
|
.callout a { color: inherit; font-weight: 500; text-decoration: underline; }
|
|
|
|
/* Env vars */
|
|
.env-section { }
|
|
.env-grid { display: grid; gap: 12px; }
|
|
.env-card {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius); padding: 18px 20px;
|
|
display: grid; grid-template-columns: auto 1fr; gap: 0 16px;
|
|
}
|
|
.env-required {
|
|
border-left: 3px solid var(--accent);
|
|
}
|
|
.env-name {
|
|
font-family: var(--font-mono); font-size: 0.875rem; font-weight: 600;
|
|
color: var(--accent); white-space: nowrap;
|
|
grid-row: span 2; display: flex; align-items: flex-start;
|
|
padding-top: 2px;
|
|
}
|
|
.env-label { font-size: 0.875rem; font-weight: 600; margin-bottom: 2px; }
|
|
.env-desc { font-size: 0.8125rem; color: var(--text-2); line-height: 1.5; }
|
|
.env-req-badge {
|
|
display: inline-flex; font-size: 0.6875rem; font-weight: 700;
|
|
letter-spacing: 0.04em; text-transform: uppercase;
|
|
color: var(--accent); margin-left: 6px; vertical-align: middle;
|
|
}
|
|
|
|
/* Optional sections */
|
|
.optional-section { background: var(--bg-alt); }
|
|
.optional-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
|
|
.optional-card {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius); padding: 20px;
|
|
}
|
|
.optional-card h3 { font-family: var(--ff-display); font-size: 1rem; font-weight: 600; margin-bottom: 6px; }
|
|
.optional-card p { font-size: 0.8125rem; color: var(--text-2); line-height: 1.55; }
|
|
.optional-card a { font-weight: 500; }
|
|
|
|
/* Troubleshooting */
|
|
.trouble-list { display: grid; gap: 8px; }
|
|
details {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius); overflow: hidden;
|
|
}
|
|
summary {
|
|
padding: 16px 20px; cursor: pointer;
|
|
font-weight: 600; font-size: 0.9375rem;
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
list-style: none; user-select: none;
|
|
transition: background 0.15s ease;
|
|
}
|
|
summary::-webkit-details-marker { display: none; }
|
|
summary:hover { background: var(--bg-alt); }
|
|
summary::after {
|
|
content: ''; width: 16px; height: 16px; flex-shrink: 0;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238E8D89' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
|
|
background-repeat: no-repeat; background-size: 16px;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
details[open] summary::after { transform: rotate(180deg); }
|
|
.details-body {
|
|
padding: 0 20px 18px;
|
|
font-size: 0.875rem; color: var(--text-2); line-height: 1.65;
|
|
border-top: 1px solid var(--border);
|
|
padding-top: 14px;
|
|
}
|
|
.details-body code {
|
|
font-family: var(--font-mono); font-size: 0.8125rem;
|
|
background: var(--accent-soft); color: var(--accent);
|
|
border-radius: 4px; padding: 1px 5px;
|
|
}
|
|
.details-body .code-block { margin: 10px 0; }
|
|
|
|
/* Success section */
|
|
.success-section {
|
|
background: var(--bg-alt);
|
|
}
|
|
.success-box {
|
|
background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: var(--radius); padding: 32px;
|
|
text-align: center;
|
|
}
|
|
.success-icon {
|
|
width: 56px; height: 56px; border-radius: 16px;
|
|
background: var(--success-bg); color: var(--success);
|
|
display: flex; align-items: center; justify-content: center;
|
|
margin: 0 auto 16px; font-size: 1.75rem;
|
|
}
|
|
.success-box h2 {
|
|
font-family: var(--ff-display); font-size: 1.5rem; font-weight: 700;
|
|
margin-bottom: 8px;
|
|
}
|
|
.success-box p { color: var(--text-2); font-size: 0.9375rem; margin-bottom: 20px; }
|
|
.success-url {
|
|
display: inline-flex; align-items: center; gap: 8px;
|
|
background: var(--code-bg); color: var(--code-text);
|
|
font-family: var(--font-mono); font-size: 0.9375rem;
|
|
padding: 12px 20px; border-radius: var(--radius);
|
|
margin-bottom: 20px;
|
|
}
|
|
.success-links { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; }
|
|
.btn-primary {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
background: var(--accent); color: #fff;
|
|
padding: 10px 20px; border-radius: 10px;
|
|
font-weight: 600; font-size: 0.9375rem; text-decoration: none;
|
|
transition: background 0.2s ease;
|
|
}
|
|
.btn-primary:hover { background: var(--accent-hover); text-decoration: none; }
|
|
.btn-secondary {
|
|
display: inline-flex; align-items: center; gap: 6px;
|
|
background: var(--surface); color: var(--text-1);
|
|
border: 1px solid var(--border);
|
|
padding: 10px 20px; border-radius: 10px;
|
|
font-weight: 500; font-size: 0.9375rem; text-decoration: none;
|
|
transition: background 0.2s ease, border-color 0.2s ease;
|
|
}
|
|
.btn-secondary:hover { background: var(--bg-alt); border-color: var(--text-3); text-decoration: none; }
|
|
|
|
/* Footer */
|
|
.footer {
|
|
border-top: 1px solid var(--border);
|
|
padding: 40px 0; text-align: center;
|
|
}
|
|
.footer-heart { font-size: 0.9375rem; color: var(--text-2); margin-bottom: 12px; }
|
|
.footer-links { display: flex; justify-content: center; gap: 24px; font-size: 0.8125rem; }
|
|
.footer-links a { color: var(--text-3); }
|
|
.footer-links a:hover { color: var(--accent); }
|
|
|
|
/* Divider */
|
|
.divider { border: none; border-top: 1px solid var(--border); margin: 40px 0; }
|
|
|
|
/* Reveal */
|
|
.reveal { opacity: 1; transform: none; }
|
|
.js .reveal { opacity: 0; transform: translateY(16px); transition: opacity 0.5s ease, transform 0.5s ease; }
|
|
.js .reveal.visible { opacity: 1; transform: none; }
|
|
.reveal-delay-1 { transition-delay: 0.08s; }
|
|
.reveal-delay-2 { transition-delay: 0.16s; }
|
|
|
|
@media (max-width: 640px) {
|
|
.hero { padding: 90px 0 40px; }
|
|
section { padding: 44px 0; }
|
|
.prereq-grid { grid-template-columns: 1fr; }
|
|
.step { grid-template-columns: 40px 1fr; gap: 0 14px; }
|
|
.step-num { width: 40px; height: 40px; font-size: 1rem; }
|
|
.step:not(:last-child)::before { left: 19px; }
|
|
.optional-grid { grid-template-columns: 1fr; }
|
|
.success-links { flex-direction: column; align-items: center; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<nav class="nav" role="navigation" aria-label="Main">
|
|
<div class="container-wide">
|
|
<a href="index.html" class="nav-logo" aria-label="Oikos Home">
|
|
<svg viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
<defs><linearGradient id="navbg" x1="0" y1="0" x2="160" y2="160" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#8b5cf6"/><stop offset="100%" stop-color="#6c3aed"/></linearGradient></defs>
|
|
<rect width="160" height="160" rx="36" fill="url(#navbg)"/>
|
|
<path d="M80 36L36 72V120C36 122.2 37.8 124 40 124H68V96H92V124H120C122.2 124 124 122.2 124 120V72L80 36Z" fill="white"/>
|
|
<rect x="100" y="46" width="12" height="22" rx="2" fill="white"/>
|
|
</svg>
|
|
Oikos
|
|
</a>
|
|
<div class="nav-controls">
|
|
<a href="index.html" class="nav-back" aria-label="Back to overview">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 18 9 12 15 6"/></svg>
|
|
<span id="backLabel">Overview</span>
|
|
</a>
|
|
<button class="nav-btn" id="langToggle" type="button" aria-label="Switch language">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
|
<span id="langLabel">DE</span>
|
|
</button>
|
|
<button class="nav-btn" id="themeToggle" type="button" aria-label="Toggle theme">
|
|
<svg id="themeIconSun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
|
<svg id="themeIconMoon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:none"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Hero -->
|
|
<header class="hero">
|
|
<div class="container">
|
|
<p class="hero-eyebrow reveal">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
|
|
<span data-t="hero_eyebrow">Installation</span>
|
|
</p>
|
|
<h1 class="reveal" data-t="hero_title">Install Oikos</h1>
|
|
<p class="hero-desc reveal" data-t="hero_desc">Get your self-hosted family planner running in a few minutes. No programming experience required — just Docker.</p>
|
|
<div class="reveal">
|
|
<span class="time-badge">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
<span data-t="hero_time">~10 minutes</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Prerequisites -->
|
|
<section id="prerequisites">
|
|
<div class="container">
|
|
<p class="section-label reveal" data-t="prereq_label">Before you start</p>
|
|
<h2 class="section-title reveal" data-t="prereq_title">What you need</h2>
|
|
<p class="section-desc reveal" data-t="prereq_desc">Oikos runs as a Docker container — you don't need to install Node.js or any other runtime. Just Docker, and you're good to go.</p>
|
|
<div class="prereq-grid">
|
|
<div class="prereq-card reveal">
|
|
<div class="prereq-icon" aria-hidden="true">🐳</div>
|
|
<h3>Docker</h3>
|
|
<p data-t="prereq_docker_desc">Packages the app so you don't need to install anything else. Free for personal use.</p>
|
|
<div class="prereq-links">
|
|
<a href="https://docs.docker.com/desktop/install/mac-install/" target="_blank" rel="noopener" class="prereq-link">macOS</a>
|
|
<a href="https://docs.docker.com/desktop/install/windows-install/" target="_blank" rel="noopener" class="prereq-link">Windows</a>
|
|
<a href="https://docs.docker.com/engine/install/" target="_blank" rel="noopener" class="prereq-link">Linux</a>
|
|
</div>
|
|
</div>
|
|
<div class="prereq-card reveal reveal-delay-1">
|
|
<div class="prereq-icon" aria-hidden="true">💻</div>
|
|
<h3 data-t="prereq_terminal_title">Terminal</h3>
|
|
<p data-t="prereq_terminal_desc">A command-line interface to type a few commands. Built into every OS — no extra install needed.</p>
|
|
<div class="prereq-links">
|
|
<span style="font-size:0.75rem;color:var(--text-3)" data-t="prereq_terminal_hint">macOS: Terminal · Windows: PowerShell · Linux: bash</span>
|
|
</div>
|
|
</div>
|
|
<div class="prereq-card reveal reveal-delay-2">
|
|
<div class="prereq-icon" aria-hidden="true">⚙️</div>
|
|
<h3 data-t="prereq_sys_title">System</h3>
|
|
<p data-t="prereq_sys_desc">256 MB RAM minimum. Runs on a Raspberry Pi, NAS, home server, or any desktop machine.</p>
|
|
<div class="prereq-links">
|
|
<span style="font-size:0.75rem;color:var(--text-3)" data-t="prereq_sys_hint">~500 MB disk for Docker image</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Steps -->
|
|
<section class="steps-section" id="install">
|
|
<div class="container">
|
|
<p class="section-label reveal" data-t="steps_label">Step by step</p>
|
|
<h2 class="section-title reveal" data-t="steps_title">Installation</h2>
|
|
|
|
<div class="tab-bar reveal" role="tablist" aria-label="Installation options">
|
|
<button class="tab-btn active" role="tab" aria-selected="true" aria-controls="panel-installer" id="tab-installer" data-t="tab_installer">
|
|
Option A — Web Installer
|
|
</button>
|
|
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-a" id="tab-a" data-t="tab_a">
|
|
Option B — Pre-built Image
|
|
</button>
|
|
<button class="tab-btn" role="tab" aria-selected="false" aria-controls="panel-b" id="tab-b" data-t="tab_b">
|
|
Option C — Build from Source
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Option A — Web Installer -->
|
|
<div class="tab-panel active" id="panel-installer" role="tabpanel" aria-labelledby="tab-installer">
|
|
<div class="callout callout-success reveal">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
|
|
<span data-t="option_installer_info">Recommended for most users. A browser-based wizard configures your .env, starts Docker, and creates your admin account — no manual steps. Requires Node.js 18+ on the host.</span>
|
|
</div>
|
|
|
|
<div class="step reveal">
|
|
<div class="step-num" aria-hidden="true">1</div>
|
|
<div class="step-content">
|
|
<h3 class="step-title" data-t="step_inst1_title">Clone the repository</h3>
|
|
<p class="step-desc" data-t="step_inst1_desc">Open your terminal and clone Oikos to a folder of your choice.</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Clone repo">git clone https://github.com/ulsklyc/oikos.git
|
|
cd oikos</div>
|
|
<button class="copy-btn" data-copy="git clone https://github.com/ulsklyc/oikos.git cd oikos" aria-label="Copy commands">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="step reveal">
|
|
<div class="step-num" aria-hidden="true">2</div>
|
|
<div class="step-content">
|
|
<h3 class="step-title" data-t="step_inst2_title">Start the installer</h3>
|
|
<p class="step-desc" data-t="step_inst2_desc">Run this command from the repository root. The installer server starts on port 8090.</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Start installer"><span class="cmd">node tools/installer/install-server.js</span></div>
|
|
<button class="copy-btn" data-copy="node tools/installer/install-server.js" aria-label="Copy command">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="step reveal">
|
|
<div class="step-num" aria-hidden="true">3</div>
|
|
<div class="step-content">
|
|
<h3 class="step-title" data-t="step_inst3_title">Open the wizard in your browser</h3>
|
|
<p class="step-desc" data-t="step_inst3_desc">Navigate to the following address. The wizard will guide you through configuration, Docker startup, and admin account creation.</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Installer URL"><span class="val">http://localhost:8090</span></div>
|
|
<button class="copy-btn" data-copy="http://localhost:8090" aria-label="Copy URL">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
<div class="callout callout-info">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
<span data-t="step_inst3_info">The installer shuts down automatically after setup completes. Your Oikos instance keeps running via Docker.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Option B — Pre-built Image -->
|
|
<div class="tab-panel" id="panel-a" role="tabpanel" aria-labelledby="tab-a">
|
|
<div class="callout callout-info reveal">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
<span data-t="option_a_info">No Git, no build step — just two files and a single command. Requires only Docker.</span>
|
|
</div>
|
|
|
|
<div class="step reveal">
|
|
<div class="step-num" aria-hidden="true">1</div>
|
|
<div class="step-content">
|
|
<h3 class="step-title" data-t="step_a1_title">Download the configuration files</h3>
|
|
<p class="step-desc" data-t="step_a1_desc">Open your terminal and run these two commands. They download the Docker configuration and the template for your settings.</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Download commands">curl -O https://raw.githubusercontent.com/ulsklyc/oikos/main/docker-compose.yml
|
|
curl -O https://raw.githubusercontent.com/ulsklyc/oikos/main/.env.example</div>
|
|
<button class="copy-btn" data-copy="curl -O https://raw.githubusercontent.com/ulsklyc/oikos/main/docker-compose.yml curl -O https://raw.githubusercontent.com/ulsklyc/oikos/main/.env.example" aria-label="Copy commands">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="step reveal">
|
|
<div class="step-num" aria-hidden="true">2</div>
|
|
<div class="step-content">
|
|
<h3 class="step-title" data-t="step_a2_title">Create your configuration</h3>
|
|
<p class="step-desc" data-t="step_a2_desc">Copy the template to create your own settings file. Then open <code>.env</code> in a text editor and set the two required secrets.</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Setup env">cp .env.example .env</div>
|
|
<button class="copy-btn" data-copy="cp .env.example .env" aria-label="Copy command">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
<p class="step-desc" data-t="step_a2_gen">Generate a secure value for each secret by running this command twice — paste one result as <code>SESSION_SECRET</code> and one as <code>DB_ENCRYPTION_KEY</code>:</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Generate secret">openssl rand -hex 32</div>
|
|
<button class="copy-btn" data-copy="openssl rand -hex 32" aria-label="Copy command">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
<div class="callout callout-warning">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
|
<span data-t="step_a2_warning">Keep a backup of your <code>.env</code> file somewhere safe. If you lose the <code>DB_ENCRYPTION_KEY</code>, your data cannot be recovered.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="step reveal">
|
|
<div class="step-num" aria-hidden="true">3</div>
|
|
<div class="step-content">
|
|
<h3 class="step-title" data-t="step_a3_title">Start the container</h3>
|
|
<p class="step-desc" data-t="step_a3_desc">Docker will automatically download the Oikos image and start it in the background. The first download takes a minute.</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Start container"><span class="cmd">docker compose up -d</span></div>
|
|
<button class="copy-btn" data-copy="docker compose up -d" aria-label="Copy command">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
<p class="step-desc" data-t="step_a3_verify">You can verify it's running by checking the logs:</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Check logs">docker compose logs -f</div>
|
|
<button class="copy-btn" data-copy="docker compose logs -f" aria-label="Copy command">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
<div class="callout callout-success">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
|
|
<span data-t="step_a3_success">You should see: <code>Server läuft auf Port 3000</code>. Press Ctrl+C to stop following logs — the container keeps running.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="step reveal">
|
|
<div class="step-num" aria-hidden="true">4</div>
|
|
<div class="step-content">
|
|
<h3 class="step-title" data-t="step_setup_title">Create your admin account</h3>
|
|
<p class="step-desc" data-t="step_setup_desc">Run the interactive setup wizard to create the first user account. You'll be asked for a username, display name, and password.</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Run setup"><span class="cmd">docker compose exec oikos node setup.js</span></div>
|
|
<button class="copy-btn" data-copy="docker compose exec oikos node setup.js" aria-label="Copy command">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Option C — Build from Source -->
|
|
<div class="tab-panel" id="panel-b" role="tabpanel" aria-labelledby="tab-b">
|
|
<div class="callout callout-info reveal">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
<span data-t="option_b_info">For contributors or those who want to run a custom version. Requires Git. The first build takes a few minutes.</span>
|
|
</div>
|
|
|
|
<div class="step reveal">
|
|
<div class="step-num" aria-hidden="true">1</div>
|
|
<div class="step-content">
|
|
<h3 class="step-title" data-t="step_b1_title">Clone the repository</h3>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Clone repo">git clone https://github.com/ulsklyc/oikos.git
|
|
cd oikos</div>
|
|
<button class="copy-btn" data-copy="git clone https://github.com/ulsklyc/oikos.git cd oikos" aria-label="Copy commands">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="step reveal">
|
|
<div class="step-num" aria-hidden="true">2</div>
|
|
<div class="step-content">
|
|
<h3 class="step-title" data-t="step_a2_title">Create your configuration</h3>
|
|
<p class="step-desc" data-t="step_a2_desc">Copy the template to create your own settings file. Then open <code>.env</code> in a text editor and set the two required secrets.</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Setup env">cp .env.example .env</div>
|
|
<button class="copy-btn" data-copy="cp .env.example .env" aria-label="Copy command">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
<p class="step-desc" data-t="step_a2_gen">Generate a secure value for each secret — run this twice:</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Generate secret">openssl rand -hex 32</div>
|
|
<button class="copy-btn" data-copy="openssl rand -hex 32" aria-label="Copy command">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="step reveal">
|
|
<div class="step-num" aria-hidden="true">3</div>
|
|
<div class="step-content">
|
|
<h3 class="step-title" data-t="step_b3_title">Build and start</h3>
|
|
<p class="step-desc" data-t="step_b3_desc">The <code>--build</code> flag compiles the Docker image locally. This takes a few minutes the first time.</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Build and start"><span class="cmd">docker compose up -d --build</span></div>
|
|
<button class="copy-btn" data-copy="docker compose up -d --build" aria-label="Copy command">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="step reveal">
|
|
<div class="step-num" aria-hidden="true">4</div>
|
|
<div class="step-content">
|
|
<h3 class="step-title" data-t="step_setup_title">Create your admin account</h3>
|
|
<p class="step-desc" data-t="step_setup_desc">Run the interactive setup wizard to create the first user account. You'll be asked for a username, display name, and password.</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block" role="region" aria-label="Run setup"><span class="cmd">docker compose exec oikos node setup.js</span></div>
|
|
<button class="copy-btn" data-copy="docker compose exec oikos node setup.js" aria-label="Copy command">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
<span data-t="copy">Copy</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Open Oikos -->
|
|
<section class="success-section" id="open">
|
|
<div class="container">
|
|
<div class="success-box reveal">
|
|
<div class="success-icon" aria-hidden="true">🎉</div>
|
|
<h2 data-t="success_title">You're all set!</h2>
|
|
<p data-t="success_desc">Open your browser and navigate to:</p>
|
|
<div class="success-url" aria-label="App URL">http://localhost:3000</div>
|
|
<p style="font-size:0.875rem;color:var(--text-2);margin-bottom:20px" data-t="success_hint">Log in with the admin credentials you just created. You can add more family members from the Settings page.</p>
|
|
<div class="success-links">
|
|
<a href="https://github.com/ulsklyc/oikos" target="_blank" rel="noopener" class="btn-primary">
|
|
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16" aria-hidden="true"><path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/></svg>
|
|
GitHub
|
|
</a>
|
|
<a href="https://github.com/ulsklyc/oikos/blob/main/docs/installation.md" target="_blank" rel="noopener" class="btn-secondary" data-t="success_full_guide">Full Technical Guide</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Key env vars -->
|
|
<section class="env-section" id="env">
|
|
<div class="container">
|
|
<p class="section-label reveal" data-t="env_label">Configuration</p>
|
|
<h2 class="section-title reveal" data-t="env_title">Required settings</h2>
|
|
<p class="section-desc reveal" data-t="env_desc">These two variables in your <code style="font-family:var(--font-mono);font-size:0.875rem;background:var(--accent-soft);color:var(--accent);border-radius:4px;padding:1px 5px">.env</code> file are mandatory. Everything else is optional.</p>
|
|
<div class="env-grid">
|
|
<div class="env-card env-required reveal">
|
|
<div class="env-name">SESSION_SECRET</div>
|
|
<div class="env-label" data-t="env_session_label">Session Secret <span class="env-req-badge">Required</span></div>
|
|
<div class="env-desc" data-t="env_session_desc">Signs and verifies login cookies. Use <code>openssl rand -hex 32</code> to generate a secure value.</div>
|
|
</div>
|
|
<div class="env-card env-required reveal reveal-delay-1">
|
|
<div class="env-name">DB_ENCRYPTION_KEY</div>
|
|
<div class="env-label" data-t="env_db_label">Database Key <span class="env-req-badge">Required</span></div>
|
|
<div class="env-desc" data-t="env_db_desc">Encrypts your entire database with AES-256. Generate with <code>openssl rand -hex 32</code>. Back this up — without it, data is unrecoverable.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Optional -->
|
|
<section class="optional-section" id="optional">
|
|
<div class="container">
|
|
<p class="section-label reveal" data-t="optional_label">Optional</p>
|
|
<h2 class="section-title reveal" data-t="optional_title">Go further</h2>
|
|
<p class="section-desc reveal" data-t="optional_desc">Once Oikos is running, you can set up these extras. All are configured in your <code style="font-family:var(--font-mono);font-size:0.875rem;background:var(--accent-soft);color:var(--accent);border-radius:4px;padding:1px 5px">.env</code> file.</p>
|
|
<div class="optional-grid">
|
|
<div class="optional-card reveal">
|
|
<h3 data-t="opt_https_title">HTTPS & Network Access</h3>
|
|
<p data-t="opt_https_desc">Want to reach Oikos from other devices or the internet? Set up Nginx as a reverse proxy with a free Let's Encrypt SSL certificate. <a href="https://github.com/ulsklyc/oikos/blob/main/docs/installation.md#https--reverse-proxy-nginx" target="_blank" rel="noopener" data-t="opt_guide">Guide →</a></p>
|
|
</div>
|
|
<div class="optional-card reveal reveal-delay-1">
|
|
<h3 data-t="opt_weather_title">Weather Widget</h3>
|
|
<p data-t="opt_weather_desc">Show the local weather on the dashboard. Set <code>OPENWEATHER_API_KEY</code> and <code>OPENWEATHER_CITY</code> — free API key from <a href="https://openweathermap.org/api" target="_blank" rel="noopener">openweathermap.org</a>.</p>
|
|
</div>
|
|
<div class="optional-card reveal reveal-delay-2">
|
|
<h3 data-t="opt_cal_title">Calendar Sync</h3>
|
|
<p data-t="opt_cal_desc">Two-way sync with Google Calendar (OAuth) and Apple iCloud (CalDAV). Set the relevant <code>GOOGLE_*</code> or <code>APPLE_*</code> variables. <a href="https://github.com/ulsklyc/oikos/blob/main/docs/installation.md#environment-variables" target="_blank" rel="noopener" data-t="opt_guide">Guide →</a></p>
|
|
</div>
|
|
<div class="optional-card reveal reveal-delay-1">
|
|
<h3 data-t="opt_backup_title">Automated Backups</h3>
|
|
<p data-t="opt_backup_desc">Add a daily cron job to back up your database. All data lives in the <code>oikos_data</code> Docker volume. <a href="https://github.com/ulsklyc/oikos/blob/main/docs/installation.md#backup--restore" target="_blank" rel="noopener" data-t="opt_guide">Guide →</a></p>
|
|
</div>
|
|
<div class="optional-card reveal reveal-delay-2">
|
|
<h3 data-t="opt_update_title">Updates</h3>
|
|
<p data-t="opt_update_desc">Pull the latest image and restart: <code>docker compose pull && docker compose up -d</code>. Your data persists across updates.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Troubleshooting -->
|
|
<section id="troubleshooting">
|
|
<div class="container">
|
|
<p class="section-label reveal" data-t="trouble_label">Troubleshooting</p>
|
|
<h2 class="section-title reveal" data-t="trouble_title">Something not working?</h2>
|
|
<p class="section-desc reveal" data-t="trouble_desc">Most issues have a simple fix. Check below — if you're still stuck, open an issue on GitHub.</p>
|
|
<div class="trouble-list reveal">
|
|
<details>
|
|
<summary data-t="trouble_port_title">Port 3000 is already in use</summary>
|
|
<div class="details-body">
|
|
<p data-t="trouble_port_desc">Another application is using port 3000. Either stop the conflicting process, or change the port in <code>docker-compose.yml</code>:</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block">lsof -i :3000 <span class="comment"># find what's using the port</span></div>
|
|
</div>
|
|
<p data-t="trouble_port_change">Or edit <code>docker-compose.yml</code> and change <code>3000:3000</code> to e.g. <code>8080:3000</code>.</p>
|
|
</div>
|
|
</details>
|
|
<details>
|
|
<summary data-t="trouble_perm_title">Docker: Permission denied</summary>
|
|
<div class="details-body">
|
|
<p data-t="trouble_perm_desc">Add your user to the Docker group, then log out and back in:</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block">sudo usermod -aG docker $USER</div>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
<details>
|
|
<summary data-t="trouble_reach_title">Container starts but the page is not reachable</summary>
|
|
<div class="details-body">
|
|
<p data-t="trouble_reach_desc">Check the container status and logs:</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block">docker compose ps <span class="comment"># should show "Up" and "healthy"</span>
|
|
docker compose logs <span class="comment"># look for error messages</span>
|
|
docker port oikos <span class="comment"># verify port mapping</span></div>
|
|
</div>
|
|
<p data-t="trouble_reach_firewall">Accessing from another device? Check your firewall rules.</p>
|
|
</div>
|
|
</details>
|
|
<details>
|
|
<summary data-t="trouble_db_title">Database encryption error</summary>
|
|
<div class="details-body">
|
|
<p data-t="trouble_db_desc">The <code>DB_ENCRYPTION_KEY</code> in your <code>.env</code> is missing or doesn't match the key used when the database was created. If this is a fresh install, you can reset:</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block">docker compose down -v
|
|
<span class="cmd">docker compose up -d</span></div>
|
|
</div>
|
|
<div class="callout callout-warning" style="margin-top:10px">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
|
<span data-t="trouble_db_warning"><code>docker compose down -v</code> deletes all data. Only use this on a fresh install with no data.</span>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
<details>
|
|
<summary data-t="trouble_nginx_title">Nginx shows 502 Bad Gateway</summary>
|
|
<div class="details-body">
|
|
<p data-t="trouble_nginx_desc">Nginx can't reach the container. Check that it's running and the port matches:</p>
|
|
<div class="code-wrap">
|
|
<div class="code-block">docker compose ps
|
|
docker compose logs | grep "Server läuft"</div>
|
|
</div>
|
|
<p data-t="trouble_nginx_port">Ensure the <code>proxy_pass</code> port in your Nginx config matches the host port in <code>docker-compose.yml</code> (default: 3000).</p>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<footer class="footer">
|
|
<div class="container">
|
|
<p class="footer-heart" data-t="footer_heart">Built with care for families who value privacy and simplicity.</p>
|
|
<div class="footer-links">
|
|
<a href="index.html" data-t="footer_home">Home</a>
|
|
<a href="https://github.com/ulsklyc/oikos" target="_blank" rel="noopener">GitHub</a>
|
|
<a href="https://github.com/ulsklyc/oikos/blob/main/LICENSE" target="_blank" rel="noopener">MIT License</a>
|
|
<a href="https://github.com/ulsklyc/oikos/blob/main/CONTRIBUTING.md" target="_blank" rel="noopener" data-t="footer_contrib">Contributing</a>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
var i18n = {
|
|
en: {
|
|
hero_eyebrow: 'Installation',
|
|
hero_title: 'Install Oikos',
|
|
hero_desc: 'Get your self-hosted family planner running in a few minutes. No programming experience required \u2014 just Docker.',
|
|
hero_time: '~10 minutes',
|
|
prereq_label: 'Before you start',
|
|
prereq_title: 'What you need',
|
|
prereq_desc: 'Oikos runs as a Docker container \u2014 you don\u2019t need to install Node.js or any other runtime. Just Docker, and you\u2019re good to go.',
|
|
prereq_docker_desc: 'Packages the app so you don\u2019t need to install anything else. Free for personal use.',
|
|
prereq_terminal_title: 'Terminal',
|
|
prereq_terminal_desc: 'A command-line interface to type a few commands. Built into every OS \u2014 no extra install needed.',
|
|
prereq_terminal_hint: 'macOS: Terminal \u00b7 Windows: PowerShell \u00b7 Linux: bash',
|
|
prereq_sys_title: 'System',
|
|
prereq_sys_desc: '256 MB RAM minimum. Runs on a Raspberry Pi, NAS, home server, or any desktop machine.',
|
|
prereq_sys_hint: '~500 MB disk for Docker image',
|
|
steps_label: 'Step by step',
|
|
steps_title: 'Installation',
|
|
tab_installer: 'Option A \u2014 Web Installer',
|
|
tab_a: 'Option B \u2014 Pre-built Image',
|
|
tab_b: 'Option C \u2014 Build from Source',
|
|
option_installer_info: 'Recommended for most users. A browser-based wizard configures your .env, starts Docker, and creates your admin account \u2014 no manual steps. Requires Node.js 18+ on the host.',
|
|
option_a_info: 'No Git, no build step \u2014 just two files and a single command. Requires only Docker.',
|
|
option_b_info: 'For contributors or those who want to run a custom version. Requires Git. The first build takes a few minutes.',
|
|
step_inst1_title: 'Clone the repository',
|
|
step_inst1_desc: 'Open your terminal and clone Oikos to a folder of your choice.',
|
|
step_inst2_title: 'Start the installer',
|
|
step_inst2_desc: 'Run this command from the repository root. The installer server starts on port 8090.',
|
|
step_inst3_title: 'Open the wizard in your browser',
|
|
step_inst3_desc: 'Navigate to the following address. The wizard will guide you through configuration, Docker startup, and admin account creation.',
|
|
step_inst3_info: 'The installer shuts down automatically after setup completes. Your Oikos instance keeps running via Docker.',
|
|
step_a1_title: 'Download the configuration files',
|
|
step_a1_desc: 'Open your terminal and run these two commands. They download the Docker configuration and the template for your settings.',
|
|
step_a2_title: 'Create your configuration',
|
|
step_a2_desc: 'Copy the template to create your own settings file. Then open .env in a text editor and set the two required secrets.',
|
|
step_a2_gen: 'Generate a secure value for each secret by running this command twice \u2014 paste one result as SESSION_SECRET and one as DB_ENCRYPTION_KEY:',
|
|
step_a2_warning: 'Keep a backup of your .env file somewhere safe. If you lose the DB_ENCRYPTION_KEY, your data cannot be recovered.',
|
|
step_a3_title: 'Start the container',
|
|
step_a3_desc: 'Docker will automatically download the Oikos image and start it in the background. The first download takes a minute.',
|
|
step_a3_verify: 'You can verify it\u2019s running by checking the logs:',
|
|
step_a3_success: 'You should see: Server l\u00e4uft auf Port 3000. Press Ctrl+C to stop following logs \u2014 the container keeps running.',
|
|
step_b1_title: 'Clone the repository',
|
|
step_b3_title: 'Build and start',
|
|
step_b3_desc: 'The --build flag compiles the Docker image locally. This takes a few minutes the first time.',
|
|
step_setup_title: 'Create your admin account',
|
|
step_setup_desc: 'Run the interactive setup wizard to create the first user account. You\u2019ll be asked for a username, display name, and password.',
|
|
success_title: 'You\u2019re all set!',
|
|
success_desc: 'Open your browser and navigate to:',
|
|
success_hint: 'Log in with the admin credentials you just created. You can add more family members from the Settings page.',
|
|
success_full_guide: 'Full Technical Guide',
|
|
env_label: 'Configuration',
|
|
env_title: 'Required settings',
|
|
env_desc: 'These two variables in your .env file are mandatory. Everything else is optional.',
|
|
env_session_label: 'Session Secret',
|
|
env_session_desc: 'Signs and verifies login cookies. Use openssl rand -hex 32 to generate a secure value.',
|
|
env_db_label: 'Database Key',
|
|
env_db_desc: 'Encrypts your entire database with AES-256. Generate with openssl rand -hex 32. Back this up \u2014 without it, data is unrecoverable.',
|
|
optional_label: 'Optional',
|
|
optional_title: 'Go further',
|
|
optional_desc: 'Once Oikos is running, you can set up these extras. All are configured in your .env file.',
|
|
opt_https_title: 'HTTPS & Network Access',
|
|
opt_https_desc: 'Want to reach Oikos from other devices or the internet? Set up Nginx as a reverse proxy with a free Let\u2019s Encrypt SSL certificate.',
|
|
opt_guide: 'Guide \u2192',
|
|
opt_weather_title: 'Weather Widget',
|
|
opt_weather_desc: 'Show the local weather on the dashboard. Set OPENWEATHER_API_KEY and OPENWEATHER_CITY \u2014 free API key from openweathermap.org.',
|
|
opt_cal_title: 'Calendar Sync',
|
|
opt_cal_desc: 'Two-way sync with Google Calendar (OAuth) and Apple iCloud (CalDAV). Set the relevant GOOGLE_* or APPLE_* variables.',
|
|
opt_backup_title: 'Automated Backups',
|
|
opt_backup_desc: 'Add a daily cron job to back up your database. All data lives in the oikos_data Docker volume.',
|
|
opt_update_title: 'Updates',
|
|
opt_update_desc: 'Pull the latest image and restart: docker compose pull && docker compose up -d. Your data persists across updates.',
|
|
trouble_label: 'Troubleshooting',
|
|
trouble_title: 'Something not working?',
|
|
trouble_desc: 'Most issues have a simple fix. Check below \u2014 if you\u2019re still stuck, open an issue on GitHub.',
|
|
trouble_port_title: 'Port 3000 is already in use',
|
|
trouble_port_desc: 'Another application is using port 3000. Either stop the conflicting process, or change the port in docker-compose.yml:',
|
|
trouble_port_change: 'Or edit docker-compose.yml and change 3000:3000 to e.g. 8080:3000.',
|
|
trouble_perm_title: 'Docker: Permission denied',
|
|
trouble_perm_desc: 'Add your user to the Docker group, then log out and back in:',
|
|
trouble_reach_title: 'Container starts but the page is not reachable',
|
|
trouble_reach_desc: 'Check the container status and logs:',
|
|
trouble_reach_firewall: 'Accessing from another device? Check your firewall rules.',
|
|
trouble_db_title: 'Database encryption error',
|
|
trouble_db_desc: 'The DB_ENCRYPTION_KEY in your .env is missing or doesn\u2019t match the key used when the database was created. If this is a fresh install, you can reset:',
|
|
trouble_db_warning: 'docker compose down -v deletes all data. Only use this on a fresh install with no data.',
|
|
trouble_nginx_title: 'Nginx shows 502 Bad Gateway',
|
|
trouble_nginx_desc: 'Nginx can\u2019t reach the container. Check that it\u2019s running and the port matches:',
|
|
trouble_nginx_port: 'Ensure the proxy_pass port in your Nginx config matches the host port in docker-compose.yml (default: 3000).',
|
|
footer_home: 'Home',
|
|
footer_heart: 'Built with care for families who value privacy and simplicity.',
|
|
footer_contrib: 'Contributing',
|
|
backLabel: 'Overview',
|
|
copy: 'Copy',
|
|
copied: 'Copied!'
|
|
},
|
|
de: {
|
|
hero_eyebrow: 'Installation',
|
|
hero_title: 'Oikos installieren',
|
|
hero_desc: 'Euren selbstgehosteten Familienplaner in wenigen Minuten zum Laufen bringen. Keine Programmierkenntnisse n\u00f6tig \u2014 nur Docker.',
|
|
hero_time: '~10 Minuten',
|
|
prereq_label: 'Voraussetzungen',
|
|
prereq_title: 'Was ihr braucht',
|
|
prereq_desc: 'Oikos l\u00e4uft als Docker-Container \u2014 Node.js oder andere Laufzeitumgebungen m\u00fcsst ihr nicht installieren. Nur Docker, und ihr seid startklar.',
|
|
prereq_docker_desc: 'Verpackt die App, sodass ihr nichts weiter installieren m\u00fcsst. F\u00fcr den privaten Gebrauch kostenlos.',
|
|
prereq_terminal_title: 'Terminal',
|
|
prereq_terminal_desc: 'Eine Befehlszeile, um ein paar Befehle einzugeben. In jedem Betriebssystem eingebaut \u2014 kein Extra-Install n\u00f6tig.',
|
|
prereq_terminal_hint: 'macOS: Terminal \u00b7 Windows: PowerShell \u00b7 Linux: bash',
|
|
prereq_sys_title: 'System',
|
|
prereq_sys_desc: 'Mindestens 256 MB RAM. L\u00e4uft auf einem Raspberry Pi, NAS, Heimserver oder jedem Desktop-Rechner.',
|
|
prereq_sys_hint: '~500 MB Speicher f\u00fcr das Docker-Image',
|
|
steps_label: 'Schritt f\u00fcr Schritt',
|
|
steps_title: 'Installation',
|
|
tab_installer: 'Option A \u2014 Web-Installer',
|
|
tab_a: 'Option B \u2014 Fertiges Image',
|
|
tab_b: 'Option C \u2014 Aus Quellcode bauen',
|
|
option_installer_info: 'Empfohlen f\u00fcr die meisten Nutzer. Ein browserbasierter Assistent konfiguriert eure .env, startet Docker und erstellt euer Admin-Konto \u2014 ganz ohne manuelle Schritte. Erfordert Node.js 18+ auf dem Host.',
|
|
option_a_info: 'Kein Git, kein Build-Schritt \u2014 nur zwei Dateien und ein einziger Befehl. Nur Docker erforderlich.',
|
|
option_b_info: 'F\u00fcr Mitwirkende oder wer eine eigene Version ausf\u00fchren m\u00f6chte. Erfordert Git. Der erste Build dauert einige Minuten.',
|
|
step_inst1_title: 'Repository klonen',
|
|
step_inst1_desc: '\u00d6ffnet euer Terminal und klont Oikos in einen Ordner eurer Wahl.',
|
|
step_inst2_title: 'Installer starten',
|
|
step_inst2_desc: 'F\u00fchrt diesen Befehl vom Projektordner aus. Der Installer-Server startet auf Port 8090.',
|
|
step_inst3_title: 'Assistenten im Browser \u00f6ffnen',
|
|
step_inst3_desc: 'Navigiert zur folgenden Adresse. Der Assistent f\u00fchrt euch durch Konfiguration, Docker-Start und Admin-Konto-Erstellung.',
|
|
step_inst3_info: 'Der Installer beendet sich automatisch nach Abschluss der Einrichtung. Eure Oikos-Instanz l\u00e4uft weiter \u00fcber Docker.',
|
|
step_a1_title: 'Konfigurationsdateien herunterladen',
|
|
step_a1_desc: '\u00d6ffnet euer Terminal und f\u00fchrt diese zwei Befehle aus. Sie laden die Docker-Konfiguration und die Vorlage f\u00fcr eure Einstellungen herunter.',
|
|
step_a2_title: 'Konfiguration erstellen',
|
|
step_a2_desc: 'Kopiert die Vorlage, um eure eigene Einstellungsdatei zu erstellen. \u00d6ffnet dann .env in einem Texteditor und setzt die zwei erforderlichen Secrets.',
|
|
step_a2_gen: 'Generiert einen sicheren Wert f\u00fcr jedes Secret, indem ihr diesen Befehl zweimal ausf\u00fchrt \u2014 ein Ergebnis als SESSION_SECRET, eines als DB_ENCRYPTION_KEY:',
|
|
step_a2_warning: 'Behaltet eine Sicherungskopie eurer .env-Datei an einem sicheren Ort. Wenn ihr den DB_ENCRYPTION_KEY verliert, k\u00f6nnen eure Daten nicht wiederhergestellt werden.',
|
|
step_a3_title: 'Container starten',
|
|
step_a3_desc: 'Docker l\u00e4dt automatisch das Oikos-Image herunter und startet es im Hintergrund. Der erste Download dauert eine Minute.',
|
|
step_a3_verify: 'Ihr k\u00f6nnt \u00fcberpr\u00fcfen, ob alles l\u00e4uft, indem ihr die Logs ansieht:',
|
|
step_a3_success: 'Ihr solltet sehen: Server l\u00e4uft auf Port 3000. Ctrl+C stoppt das Log-Verfolgen \u2014 der Container l\u00e4uft weiter.',
|
|
step_b1_title: 'Repository klonen',
|
|
step_b3_title: 'Bauen und starten',
|
|
step_b3_desc: 'Das Flag --build kompiliert das Docker-Image lokal. Das dauert beim ersten Mal einige Minuten.',
|
|
step_setup_title: 'Admin-Konto erstellen',
|
|
step_setup_desc: 'F\u00fchrt den interaktiven Einrichtungsassistenten aus, um das erste Benutzerkonto zu erstellen. Ihr werdet nach Benutzername, Anzeigename und Passwort gefragt.',
|
|
success_title: 'Alles bereit!',
|
|
success_desc: '\u00d6ffnet euren Browser und geht zu:',
|
|
success_hint: 'Meldet euch mit den Admin-Zugangsdaten an, die ihr gerade erstellt habt. Weitere Familienmitglieder k\u00f6nnt ihr auf der Einstellungsseite hinzuf\u00fcgen.',
|
|
success_full_guide: 'Technische Vollst\u00e4ndiganleitung',
|
|
env_label: 'Konfiguration',
|
|
env_title: 'Pflichteinstellungen',
|
|
env_desc: 'Diese zwei Variablen in eurer .env-Datei sind Pflicht. Alles andere ist optional.',
|
|
env_session_label: 'Session-Secret',
|
|
env_session_desc: 'Signiert und verifiziert Anmelde-Cookies. Mit openssl rand -hex 32 einen sicheren Wert generieren.',
|
|
env_db_label: 'Datenbank-Schl\u00fcssel',
|
|
env_db_desc: 'Verschl\u00fcsselt die gesamte Datenbank mit AES-256. Mit openssl rand -hex 32 generieren. Unbedingt sichern \u2014 ohne diesen Schl\u00fcssel sind die Daten nicht wiederherstellbar.',
|
|
optional_label: 'Optional',
|
|
optional_title: 'Mehr entdecken',
|
|
optional_desc: 'Sobald Oikos l\u00e4uft, k\u00f6nnt ihr diese Extras einrichten. Alles wird in eurer .env-Datei konfiguriert.',
|
|
opt_https_title: 'HTTPS & Netzwerkzugang',
|
|
opt_https_desc: 'Oikos von anderen Ger\u00e4ten oder aus dem Internet erreichbar machen? Nginx als Reverse Proxy mit kostenlosem Let\u2019s Encrypt SSL-Zertifikat einrichten.',
|
|
opt_guide: 'Anleitung \u2192',
|
|
opt_weather_title: 'Wetter-Widget',
|
|
opt_weather_desc: 'Lokales Wetter auf dem Dashboard anzeigen. OPENWEATHER_API_KEY und OPENWEATHER_CITY setzen \u2014 kostenloser API-Key von openweathermap.org.',
|
|
opt_cal_title: 'Kalender-Sync',
|
|
opt_cal_desc: 'Zwei-Wege-Sync mit Google Calendar (OAuth) und Apple iCloud (CalDAV). Die entsprechenden GOOGLE_*- oder APPLE_*-Variablen setzen.',
|
|
opt_backup_title: 'Automatische Backups',
|
|
opt_backup_desc: 'Einen t\u00e4glichen Cron-Job einrichten, um die Datenbank zu sichern. Alle Daten liegen im oikos_data Docker-Volume.',
|
|
opt_update_title: 'Updates',
|
|
opt_update_desc: 'Neuestes Image holen und neu starten: docker compose pull && docker compose up -d. Eure Daten bleiben bei Updates erhalten.',
|
|
trouble_label: 'Probleml\u00f6sung',
|
|
trouble_title: 'Etwas funktioniert nicht?',
|
|
trouble_desc: 'Die meisten Probleme haben eine einfache L\u00f6sung. Schaut unten nach \u2014 wenn ihr weiterhin Schwierigkeiten habt, \u00f6ffnet ein Issue auf GitHub.',
|
|
trouble_port_title: 'Port 3000 ist bereits belegt',
|
|
trouble_port_desc: 'Eine andere Anwendung nutzt Port 3000. Entweder den Konfliktprozess stoppen oder den Port in docker-compose.yml \u00e4ndern:',
|
|
trouble_port_change: 'Oder docker-compose.yml bearbeiten und 3000:3000 zu z.\u00a0B. 8080:3000 \u00e4ndern.',
|
|
trouble_perm_title: 'Docker: Permission denied',
|
|
trouble_perm_desc: 'Euren Benutzer zur Docker-Gruppe hinzuf\u00fcgen, dann ab- und wieder anmelden:',
|
|
trouble_reach_title: 'Container startet, aber die Seite ist nicht erreichbar',
|
|
trouble_reach_desc: 'Container-Status und Logs pr\u00fcfen:',
|
|
trouble_reach_firewall: 'Zugriff von einem anderen Ger\u00e4t? Firewall-Regeln pr\u00fcfen.',
|
|
trouble_db_title: 'Datenbank-Verschl\u00fcsselungsfehler',
|
|
trouble_db_desc: 'Der DB_ENCRYPTION_KEY in eurer .env fehlt oder stimmt nicht mit dem Schl\u00fcssel \u00fcberein, der beim Erstellen der Datenbank verwendet wurde. Bei einer Neuinstallation k\u00f6nnt ihr zur\u00fccksetzen:',
|
|
trouble_db_warning: 'docker compose down -v l\u00f6scht alle Daten. Nur bei einer Neuinstallation ohne Daten verwenden.',
|
|
trouble_nginx_title: 'Nginx zeigt 502 Bad Gateway',
|
|
trouble_nginx_desc: 'Nginx kann den Container nicht erreichen. Pr\u00fcfen, ob er l\u00e4uft und der Port stimmt:',
|
|
trouble_nginx_port: 'Sicherstellen, dass der proxy_pass-Port in der Nginx-Konfiguration mit dem Host-Port in docker-compose.yml \u00fcbereinstimmt (Standard: 3000).',
|
|
footer_home: 'Startseite',
|
|
footer_heart: 'Mit Sorgfalt gebaut f\u00fcr Familien, die Privatsph\u00e4re und Einfachheit sch\u00e4tzen.',
|
|
footer_contrib: 'Mitmachen',
|
|
backLabel: '\u00dcbersicht',
|
|
copy: 'Kopieren',
|
|
copied: 'Kopiert!'
|
|
}
|
|
};
|
|
|
|
var currentLang = localStorage.getItem('oikos-lang') || 'en';
|
|
var currentTheme = localStorage.getItem('oikos-theme');
|
|
|
|
/* Theme */
|
|
function isDark() {
|
|
if (currentTheme === 'dark') return true;
|
|
if (currentTheme === 'light') return false;
|
|
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
}
|
|
function applyTheme() {
|
|
var dark = isDark();
|
|
if (currentTheme) {
|
|
document.documentElement.setAttribute('data-theme', currentTheme);
|
|
} else {
|
|
document.documentElement.removeAttribute('data-theme');
|
|
}
|
|
document.getElementById('themeIconSun').style.display = dark ? 'none' : 'block';
|
|
document.getElementById('themeIconMoon').style.display = dark ? 'block' : 'none';
|
|
}
|
|
document.getElementById('themeToggle').addEventListener('click', function() {
|
|
currentTheme = isDark() ? 'light' : 'dark';
|
|
localStorage.setItem('oikos-theme', currentTheme);
|
|
applyTheme();
|
|
});
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
|
|
if (!currentTheme) applyTheme();
|
|
});
|
|
|
|
/* Language */
|
|
function applyLang() {
|
|
document.documentElement.lang = currentLang;
|
|
var strings = i18n[currentLang];
|
|
document.querySelectorAll('[data-t]').forEach(function(el) {
|
|
var key = el.getAttribute('data-t');
|
|
if (strings[key] !== undefined) el.textContent = strings[key];
|
|
});
|
|
document.getElementById('langLabel').textContent = currentLang === 'en' ? 'DE' : 'EN';
|
|
document.getElementById('backLabel').textContent = strings.backLabel;
|
|
document.title = currentLang === 'en'
|
|
? 'Install Oikos \u2014 Self-Hosted Family Planner'
|
|
: 'Oikos installieren \u2014 Selbstgehosteter Familienplaner';
|
|
}
|
|
document.getElementById('langToggle').addEventListener('click', function() {
|
|
currentLang = currentLang === 'en' ? 'de' : 'en';
|
|
localStorage.setItem('oikos-lang', currentLang);
|
|
applyLang();
|
|
});
|
|
|
|
/* Tabs */
|
|
document.querySelectorAll('.tab-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var panelId = btn.getAttribute('aria-controls');
|
|
document.querySelectorAll('.tab-btn').forEach(function(b) {
|
|
b.classList.remove('active');
|
|
b.setAttribute('aria-selected', 'false');
|
|
});
|
|
document.querySelectorAll('.tab-panel').forEach(function(p) { p.classList.remove('active'); });
|
|
btn.classList.add('active');
|
|
btn.setAttribute('aria-selected', 'true');
|
|
document.getElementById(panelId).classList.add('active');
|
|
});
|
|
});
|
|
|
|
/* Copy buttons */
|
|
document.querySelectorAll('.copy-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var text = btn.getAttribute('data-copy');
|
|
if (!text) return;
|
|
navigator.clipboard.writeText(text).then(function() {
|
|
var span = btn.querySelector('span');
|
|
var orig = span.textContent;
|
|
btn.classList.add('copied');
|
|
span.textContent = i18n[currentLang].copied || 'Copied!';
|
|
setTimeout(function() {
|
|
btn.classList.remove('copied');
|
|
span.textContent = i18n[currentLang].copy || 'Copy';
|
|
}, 2000);
|
|
});
|
|
});
|
|
});
|
|
|
|
/* Scroll Reveal */
|
|
document.documentElement.classList.add('js');
|
|
if ('IntersectionObserver' in window) {
|
|
var observer = new IntersectionObserver(function(entries) {
|
|
entries.forEach(function(e) {
|
|
if (e.isIntersecting) { e.target.classList.add('visible'); observer.unobserve(e.target); }
|
|
});
|
|
}, { threshold: 0.1, rootMargin: '0px 0px -30px 0px' });
|
|
document.querySelectorAll('.reveal').forEach(function(el) { observer.observe(el); });
|
|
}
|
|
|
|
applyTheme();
|
|
applyLang();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|