DOCS-1: Init document work

This commit is contained in:
Blake Ridgway
2026-07-28 07:20:32 -05:00
parent 8f02a3fc8e
commit 0cbcc962f7
66 changed files with 12224 additions and 71 deletions

184
templates/admin_edit.html Normal file
View File

@@ -0,0 +1,184 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<title>{{if .Page}}Edit: {{.Page.Title}}{{else}}New Page{{end}} — Admin — Arcline</title>
<meta name="robots" content="noindex">
<style>
.admin-wrap { max-width:860px; margin:2.5rem auto; padding:0 1.5rem; }
.admin-header{ display:flex; align-items:center; gap:1rem; margin-bottom:1.75rem; }
.admin-header h1 { font-size:1.375rem; font-weight:800; color:var(--text-strong); margin:0; }
.form-grid { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }
.form-group { display:flex; flex-direction:column; gap:.375rem; }
.form-group--full { grid-column:1/-1; }
label { font-size:.8125rem; font-weight:600; color:var(--text-strong); }
input[type=text], input[type=number], select, textarea {
font-family:var(--mono); font-size:.875rem;
padding:.5rem .75rem; border:1px solid var(--border);
border-radius:var(--radius-sm); background:var(--white);
color:var(--text-strong); width:100%; box-sizing:border-box;
transition:border-color var(--t);
}
input:focus, select:focus, textarea:focus { outline:none; border-color:var(--accent); }
textarea { font-family:var(--mono); font-size:.8125rem; min-height:420px; resize:vertical; line-height:1.6; }
.vis-row { display:flex; gap:.75rem; align-items:center; flex-wrap:wrap; }
.vis-radio { display:flex; align-items:center; gap:.375rem; font-size:.875rem; cursor:pointer; }
.form-hint { font-size:.75rem; color:var(--text-muted); margin-top:.25rem; }
.form-actions{ display:flex; gap:.75rem; margin-top:1.5rem; align-items:center; }
.error-box { background:#fef2f2; border:1px solid #fca5a5; border-left:3px solid #ef4444;
border-radius:var(--radius-sm); padding:.875rem 1.125rem; margin-bottom:1.25rem;
font-size:.9rem; color:#991b1b; }
#vis-plan-wrap, #vis-customer-wrap { display:none; }
@media(max-width:640px){ .form-grid{ grid-template-columns:1fr; } }
</style>
</head>
<body>
{{template "nav" .}}
<div class="admin-wrap">
<div class="admin-header">
<h1>{{if .Page}}Edit page{{else}}New page{{end}}</h1>
</div>
{{if .Error}}
<div class="error-box">{{.Error}}</div>
{{end}}
<form method="POST"
action="{{if .Page}}/admin/pages/{{.Page.ID}}{{else}}/admin/pages{{end}}">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<div class="form-grid">
<div class="form-group">
<label for="title">Title</label>
<input type="text" id="title" name="title" required
value="{{if .Page}}{{.Page.Title}}{{end}}"
placeholder="e.g. Server Setup Guide">
</div>
<div class="form-group">
<label for="slug">Slug</label>
<input type="text" id="slug" name="slug" required
value="{{if .Page}}{{.Page.Slug}}{{end}}"
placeholder="e.g. server-setup-guide">
<span class="form-hint">URL: /client/<em>slug</em>/</span>
</div>
<div class="form-group form-group--full">
<label for="description">Description <span style="font-weight:400;color:var(--text-muted)">(optional)</span></label>
<input type="text" id="description" name="description"
value="{{if .Page}}{{.Page.Description}}{{end}}"
placeholder="Short summary shown in listings">
</div>
<div class="form-group">
<label for="section">Section</label>
<input type="text" id="section" name="section"
value="{{if .Page}}{{.Page.Section}}{{else}}client{{end}}"
placeholder="client">
<span class="form-hint">Use "client" for customer-facing docs.</span>
</div>
<div class="form-group">
<label for="display_order">Display order</label>
<input type="number" id="display_order" name="display_order" min="0"
value="{{if .Page}}{{.Page.DisplayOrder}}{{else}}0{{end}}">
</div>
<div class="form-group form-group--full">
<label>Visibility</label>
<div class="vis-row">
<label class="vis-radio">
<input type="radio" name="vis_type" value="public"
{{if or (not .Page) (eq .Page.Visibility "public")}}checked{{end}}>
Public
</label>
<label class="vis-radio">
<input type="radio" name="vis_type" value="plan"
{{if and .Page (hasPrefix .Page.Visibility "plan:")}}checked{{end}}>
Plan
</label>
<label class="vis-radio">
<input type="radio" name="vis_type" value="customer"
{{if and .Page (hasPrefix .Page.Visibility "customer:")}}checked{{end}}>
Customer
</label>
</div>
<div id="vis-plan-wrap" style="margin-top:.5rem;">
<select name="vis_plan" id="vis_plan">
{{range .PlanOptions}}
<option value="{{.Key}}"
{{if and $.Page (eq $.Page.Visibility (print "plan:" .Key))}}selected{{end}}>
{{.Name}}
</option>
{{end}}
</select>
<span class="form-hint">Visible to all customers on this plan with an active subscription.</span>
</div>
<div id="vis-customer-wrap" style="margin-top:.5rem;">
<select name="vis_customer" id="vis_customer">
{{range .Customers}}
<option value="{{.ID}}"
{{if and $.Page (eq $.Page.Visibility (print "customer:" .ID))}}selected{{end}}>
{{.FirstName}} {{.LastName}} ({{.Email}})
</option>
{{end}}
</select>
<span class="form-hint">Visible to this customer only.</span>
</div>
</div>
<div class="form-group form-group--full">
<label for="content">Content <span style="font-weight:400;color:var(--text-muted)">(Markdown)</span></label>
<textarea id="content" name="content" placeholder="Write markdown here...">{{if .Page}}{{.Page.Content}}{{end}}</textarea>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn--primary">{{if .Page}}Save changes{{else}}Create page{{end}}</button>
<a href="/admin/" class="btn btn--ghost">Cancel</a>
{{if .Page}}
<a href="/client/{{.Page.Slug}}/" class="btn btn--ghost btn--sm" target="_blank"
style="margin-left:auto;">View page →</a>
{{end}}
</div>
</form>
</div>
{{template "footer-simple" .}}
<script>
(function() {
var radios = document.querySelectorAll('[name="vis_type"]');
var planWrap = document.getElementById('vis-plan-wrap');
var custWrap = document.getElementById('vis-customer-wrap');
function update() {
var val = document.querySelector('[name="vis_type"]:checked');
if (!val) return;
planWrap.style.display = val.value === 'plan' ? 'block' : 'none';
custWrap.style.display = val.value === 'customer' ? 'block' : 'none';
}
radios.forEach(function(r) { r.addEventListener('change', update); });
update();
var titleInput = document.getElementById('title');
var slugInput = document.getElementById('slug');
var slugTouched = slugInput.value !== '';
titleInput.addEventListener('input', function() {
if (slugTouched) return;
slugInput.value = titleInput.value
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
});
slugInput.addEventListener('input', function() { slugTouched = true; });
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<title>Admin — Docs — Arcline</title>
<meta name="robots" content="noindex">
<style>
.admin-wrap { max-width:900px; margin:2.5rem auto; padding:0 1.5rem; }
.admin-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:1.75rem; }
.admin-header h1 { font-size:1.5rem; font-weight:800; color:var(--text-strong); }
.admin-table { width:100%; border-collapse:collapse; font-size:0.875rem; }
.admin-table th { background:var(--bg); font-size:.72rem; font-weight:700; letter-spacing:.07em; text-transform:uppercase; color:var(--text-muted); padding:.625rem 1rem; text-align:left; border-bottom:1px solid var(--border); }
.admin-table td { padding:.75rem 1rem; color:var(--text); border-bottom:1px solid var(--border-subtle); vertical-align:middle; line-height:1.4; }
.admin-table tr:last-child td { border-bottom:none; }
.admin-table tr:hover td { background:var(--bg); }
.vis-badge { display:inline-block; font-size:.7rem; font-weight:600; padding:.2em .5em; border-radius:3px; background:var(--border-subtle); color:var(--text-muted); white-space:nowrap; }
.vis-badge--public { background:var(--accent-light); color:var(--accent); }
.vis-badge--plan { background:#fef3c7; color:#92400e; }
.vis-badge--cust { background:#ede9fe; color:#5b21b6; }
.action-btns { display:flex; gap:.5rem; }
</style>
</head>
<body>
{{template "nav" .}}
<div class="admin-wrap">
<div class="admin-header">
<h1>Docs Admin</h1>
<a href="/admin/pages/new" class="btn btn--primary btn--sm">+ New page</a>
</div>
{{if .Pages}}
<table class="admin-table">
<thead>
<tr>
<th>Title</th>
<th>Slug</th>
<th>Section</th>
<th>Visibility</th>
<th>Order</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .Pages}}
<tr>
<td>
<strong>{{.Title}}</strong>
{{if .Description}}<br><span style="color:var(--text-muted);font-size:.8rem;">{{.Description}}</span>{{end}}
</td>
<td><code style="font-size:.8rem;">{{.Slug}}</code></td>
<td>{{.Section}}</td>
<td>
{{$v := visLabel .Visibility}}
{{if eq .Visibility "public"}}
<span class="vis-badge vis-badge--public">{{$v}}</span>
{{else if hasPrefix .Visibility "plan:"}}
<span class="vis-badge vis-badge--plan">{{$v}}</span>
{{else}}
<span class="vis-badge vis-badge--cust">{{$v}}</span>
{{end}}
</td>
<td>{{.DisplayOrder}}</td>
<td>
<div class="action-btns">
<a href="/client/{{.Slug}}/" class="btn btn--ghost btn--sm" target="_blank">View</a>
<a href="/admin/pages/{{.ID}}/edit" class="btn btn--ghost btn--sm">Edit</a>
<form method="POST" action="/admin/pages/{{.ID}}/delete" style="margin:0;"
onsubmit="return confirm('Delete {{.Title}}?')">
<input type="hidden" name="csrf_token" value="{{$.CSRFToken}}">
<button class="btn btn--danger btn--sm" type="submit">Delete</button>
</form>
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<p style="color:var(--text-muted);">No pages yet. <a href="/admin/pages/new">Create the first one.</a></p>
{{end}}
</div>
{{template "footer-simple" .}}
</body>
</html>

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<title>My Docs — Arcline</title>
</head>
<body>
{{template "nav" .}}
<main class="docs-main">
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/">Docs</a>
<span class="breadcrumb__sep">/</span>
<span>My Docs</span>
</nav>
<article class="docs-article">
<h1>My Docs</h1>
<p>Private documentation and guides for your Arcline account.</p>
</article>
{{if .Pages}}
<div class="section-index">
{{range .Pages}}
<a href="/client/{{.Slug}}/" class="section-index__card">
<span class="section-index__title">{{.Title}}</span>
{{if .Description}}<span class="section-index__desc">{{.Description}}</span>{{end}}
</a>
{{end}}
</div>
{{else}}
<p style="color:var(--text-muted);margin-top:1.5rem;">No private documentation has been added to your account yet.</p>
{{end}}
</main>
{{template "footer-simple" .}}
</body>
</html>

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<title>{{.Page.Title}} — My Docs — Arcline</title>
{{if .Page.Description}}<meta name="description" content="{{.Page.Description}}">{{end}}
<meta name="robots" content="noindex">
</head>
<body>
{{template "nav" .}}
<main class="docs-main">
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/">Docs</a>
<span class="breadcrumb__sep">/</span>
<a href="/client/">My Docs</a>
<span class="breadcrumb__sep">/</span>
<span>{{.Page.Title}}</span>
</nav>
<article class="docs-article">
{{if .IsAdmin}}
<div style="margin-bottom:1.25rem;">
<a href="/admin/pages/{{.Page.ID}}/edit" class="btn btn--ghost btn--sm">Edit this page</a>
</div>
{{end}}
{{.Content}}
</article>
</main>
{{template "footer-simple" .}}
</body>
</html>

181
templates/layout.html Normal file
View File

@@ -0,0 +1,181 @@
{{define "head"}}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/css/fonts.css">
<link rel="stylesheet" href="/css/base.css">
<link rel="stylesheet" href="/css/docs.css">
<link rel="icon" type="image/svg+xml" href="/public/favicon.svg">
<meta property="og:image" content="https://arcline.it/public/brand/og-image.png">
<meta property="og:site_name" content="Arcline Docs">
{{end}}
{{define "nav"}}
<header class="nav">
<div class="container nav__inner">
<a href="https://arcline.it" class="nav__logo" aria-label="Arcline home">
<svg width="22" height="22" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<path d="M5 27L16 5L27 27" stroke="#00c8f0" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 20H23" stroke="#00c8f0" stroke-width="2.5" stroke-linecap="round"/>
</svg>
Arcline
</a>
<a href="/" class="nav__docs-badge">docs</a>
{{if .Nav}}
<div class="nav__docs-menu">
<button class="nav__browse-btn" id="browse-btn" aria-haspopup="true" aria-expanded="false">
<span class="label">Browse</span>
</button>
<nav class="nav__dropdown" id="browse-dropdown" role="menu" aria-label="Documentation sections">
{{range .Nav}}
<p class="nav__dropdown-section">{{.Title}}</p>
{{range .Pages}}
<a href="{{.URL}}" class="nav__dropdown-link{{if .Active}} nav__dropdown-link--active{{end}}" role="menuitem">{{.Title}}</a>
{{end}}
{{end}}
</nav>
</div>
<div class="nav__search-wrap search-wrap">
<input type="search" id="search-input" class="nav__search" placeholder="Search docs… (⌘K)" autocomplete="off" aria-label="Search documentation">
<div id="search-results" class="search-results" hidden role="listbox" aria-label="Search results"></div>
</div>
{{end}}
<div class="nav__actions">
{{if .Customer}}
<a href="/client/" class="btn btn--ghost btn--sm">My Docs</a>
{{if .IsAdmin}}<a href="/admin/" class="btn btn--ghost btn--sm">Admin</a>{{end}}
<a href="/logout" class="btn btn--ghost btn--sm">Log out</a>
{{else}}
<a href="https://arcline.it/pricing" class="btn btn--ghost btn--sm">Hosting plans</a>
<a href="https://arcline.it/contact" class="btn btn--primary btn--sm">Get started</a>
{{end}}
</div>
<button class="nav__hamburger" aria-label="Toggle navigation" aria-expanded="false" id="hamburger">
<span></span><span></span><span></span>
</button>
</div>
</header>
{{if .Nav}}
<nav class="nav__mobile" id="mobile-nav" aria-label="Mobile navigation">
{{range .Nav}}
<p class="nav__mobile-section">{{.Title}}</p>
{{range .Pages}}
<a href="{{.URL}}"{{if .Active}} class="active"{{end}}>{{.Title}}</a>
{{end}}
{{end}}
<div class="nav__mobile-actions">
{{if $.Customer}}
<a href="/client/" class="btn btn--ghost btn--sm">My Docs</a>
<a href="/logout" class="btn btn--ghost btn--sm">Log out</a>
{{else}}
<a href="https://arcline.it/contact" class="btn btn--primary btn--sm">Get started</a>
{{end}}
</div>
</nav>
{{end}}
<script>
(function() {
// Hamburger toggle (mobile)
var btn = document.getElementById('hamburger');
var nav = document.getElementById('mobile-nav');
if (btn && nav) {
btn.addEventListener('click', function() {
var open = nav.classList.toggle('open');
btn.setAttribute('aria-expanded', open);
});
}
// Browse dropdown toggle (desktop)
var browseBtn = document.getElementById('browse-btn');
var dropdown = document.getElementById('browse-dropdown');
if (browseBtn && dropdown) {
browseBtn.addEventListener('click', function(e) {
e.stopPropagation();
var open = dropdown.classList.toggle('open');
browseBtn.setAttribute('aria-expanded', open);
});
document.addEventListener('click', function(e) {
if (!e.target.closest('.nav__docs-menu')) {
dropdown.classList.remove('open');
browseBtn.setAttribute('aria-expanded', 'false');
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
dropdown.classList.remove('open');
browseBtn.setAttribute('aria-expanded', 'false');
}
});
}
})();
</script>
{{end}}
{{define "footer"}}
<footer class="footer">
<div class="container">
<div class="footer__grid">
<div class="footer__brand">
<a href="https://arcline.it" class="footer__logo" aria-label="Arcline home">
<svg width="18" height="18" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<path d="M5 27L16 5L27 27" stroke="#00c8f0" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 20H23" stroke="#00c8f0" stroke-width="2.5" stroke-linecap="round"/>
</svg>
Arcline
</a>
<p class="footer__tagline">IT infrastructure built for reliability, privacy, and independence. No cloud lock-in. No middlemen. No surprises.</p>
<a href="https://arcline.it/status" class="footer__status">All Systems Operational</a>
</div>
<div>
<p class="footer__col-title">Services</p>
<ul class="footer__links">
<li><a href="https://arcline.it/services" class="footer__link">Server setup &amp; management</a></li>
<li><a href="https://arcline.it/services" class="footer__link">Network security</a></li>
<li><a href="https://arcline.it/pricing" class="footer__link">Web hosting</a></li>
<li><a href="https://arcline.it/contact" class="footer__link">Contact us</a></li>
</ul>
</div>
<div>
<p class="footer__col-title">Docs</p>
<ul class="footer__links">
<li><a href="/getting-started/" class="footer__link">Getting started</a></li>
<li><a href="/migrate/" class="footer__link">Migration guides</a></li>
{{if .Customer}}<li><a href="/client/" class="footer__link">My Docs</a></li>{{end}}
</ul>
</div>
<div>
<p class="footer__col-title">Support</p>
<ul class="footer__links">
<li><a href="https://arcline.it/contact" class="footer__link">Open a ticket</a></li>
<li><a href="https://arcline.it/faq" class="footer__link">FAQ</a></li>
<li><a href="https://arcline.it/about" class="footer__link">About Arcline</a></li>
</ul>
</div>
</div>
<div class="footer__bottom">
<p class="footer__copy">&copy; {{.Year}} Arcline IT LLC. All rights reserved.</p>
<nav class="footer__legal" aria-label="Legal links">
<a href="https://arcline.it/tos">Terms of service</a>
<a href="https://arcline.it/privacy">Privacy policy</a>
<a href="https://arcline.it/aup">AUP</a>
</nav>
</div>
</div>
</footer>
{{end}}
{{define "footer-simple"}}
<footer class="footer">
<div class="container">
<div class="footer__bottom">
<p class="footer__copy">&copy; {{.Year}} Arcline IT LLC. All rights reserved.</p>
<nav class="footer__legal" aria-label="Legal links">
<a href="https://arcline.it/tos">Terms of service</a>
<a href="https://arcline.it/privacy">Privacy policy</a>
<a href="https://arcline.it/aup">AUP</a>
</nav>
</div>
</div>
</footer>
{{end}}

94
templates/page.html Normal file
View File

@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<title>{{.Title}}</title>
{{if .Description}}<meta name="description" content="{{.Description}}">{{end}}
{{if .Canonical}}<link rel="canonical" href="{{.Canonical}}">{{end}}
<meta name="docs-root" content="/">
<meta property="og:type" content="website">
<meta property="og:title" content="{{.Title}}">
{{if .Description}}<meta property="og:description" content="{{.Description}}">{{end}}
</head>
<body>
{{template "nav" .}}
<main class="docs-main" id="main-content">
{{if .Breadcrumbs}}
<nav class="breadcrumb" aria-label="Breadcrumb">
<a href="/">Docs</a>
{{range .Breadcrumbs}}
<span class="breadcrumb__sep">/</span>
{{if .URL}}<a href="{{.URL}}">{{.Label}}</a>{{else}}<span>{{.Label}}</span>{{end}}
{{end}}
</nav>
{{end}}
<article class="docs-article">
{{if .Nav}}
<nav class="docs-toc" aria-label="Table of contents">
<details class="docs-toc__details" open>
<summary class="docs-toc__summary">On this page</summary>
<div class="docs-toc__links" id="docs-toc-links">
<!-- Populated by JS from h2/h3 headings -->
</div>
</details>
</nav>
{{end}}
{{.Content}}
</article>
{{if or .Prev .Next}}
<nav class="docs-pager" aria-label="Next and previous pages">
<div class="docs-pager__side">
{{if .Prev}}
<a href="{{.Prev.URL}}" class="docs-pager__link docs-pager__link--prev">
<span class="docs-pager__dir">&larr; Previous</span>
<span class="docs-pager__title">{{.Prev.Title}}</span>
</a>
{{end}}
</div>
<div class="docs-pager__side docs-pager__side--right">
{{if .Next}}
<a href="{{.Next.URL}}" class="docs-pager__link docs-pager__link--next">
<span class="docs-pager__dir">Next &rarr;</span>
<span class="docs-pager__title">{{.Next.Title}}</span>
</a>
{{end}}
</div>
</nav>
{{end}}
</main>
{{template "footer" .}}
<script src="/js/search.js"></script>
<script>
(function() {
// Inline table of contents — auto-generated from h2 and h3 headings.
var toc = document.getElementById('docs-toc-links');
if (!toc) return;
var headings = document.querySelectorAll('.docs-article h2, .docs-article h3');
if (headings.length < 2) { toc.parentElement.hidden = true; return; }
var html = '<ul class="docs-toc__list">';
var stack = ['ul'];
var prevLevel = 2;
headings.forEach(function(h) {
var level = h.tagName === 'H2' ? 2 : 3;
var id = h.id || '';
if (!id) { id = h.textContent.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); h.id = id; }
if (level > prevLevel) { html += '<ul class="docs-toc__sublist">'; stack.push('ul'); }
while (level < prevLevel && stack.length > 1) { html += '</' + stack.pop() + '>'; prevLevel--; }
html += '<li class="docs-toc__item' + (level === 3 ? ' docs-toc__item--sub' : '') + '">' +
'<a href="#' + id + '">' + h.textContent + '</a></li>';
prevLevel = level;
});
while (stack.length > 1) { html += '</' + stack.pop() + '>'; }
html += '</ul>';
toc.innerHTML = html;
})();
</script>
</body>
</html>