Compare commits
36 Commits
7a6e4d17ac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
e1bf53bd70
|
|||
|
94bcfe915d
|
|||
|
ec771edc01
|
|||
|
8a9a361a4b
|
|||
|
c0a8e3ced8
|
|||
|
e9c1dfa2e5
|
|||
|
ad81392dc7
|
|||
|
500f4746e8
|
|||
|
9159ecc834
|
|||
|
def75c1e41
|
|||
|
88c6bd4891
|
|||
|
a26e239291
|
|||
|
590a3d360f
|
|||
|
0f709b0a98
|
|||
|
bc87cef815
|
|||
|
f294a065e2
|
|||
|
5ce67a697b
|
|||
|
1878032ec4
|
|||
|
b986fe41c3
|
|||
|
f43a191908
|
|||
|
a53e808aec
|
|||
|
d0383b0d4e
|
|||
|
da27216fc1
|
|||
|
f7d0018314
|
|||
|
f05b96e0bf
|
|||
|
522fac7b01
|
|||
|
9730c5b129
|
|||
|
8cda888a87
|
|||
|
cdd1dca030
|
|||
|
1a88883bad
|
|||
|
2fa26ef552
|
|||
|
9d380ad300
|
|||
|
1b29e8d833
|
|||
|
b3a2400bc2
|
|||
|
573e70a8d2
|
|||
|
6457674de7
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,7 +9,7 @@ public/*
|
||||
.hugo_build.lock
|
||||
deploy
|
||||
node_modules
|
||||
env
|
||||
.env
|
||||
Package.resolved
|
||||
|
||||
# Local Netlify folder
|
||||
|
||||
52
Caddyfile
Normal file
52
Caddyfile
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
# Configure caddy-security.
|
||||
order authenticate before respond
|
||||
|
||||
security {
|
||||
oauth identity provider generic {
|
||||
delay_start 3
|
||||
realm generic
|
||||
driver generic
|
||||
client_id {env.OAUTH_CLIENT_ID} # Replace with your own client ID
|
||||
client_secret {env.OAUTH_CLIENT_SECRET} # Replace with your own client secret
|
||||
scopes openid email profile
|
||||
base_auth_url http://pocket-id
|
||||
metadata_url http://pocket-id/.well-known/openid-configuration
|
||||
}
|
||||
|
||||
authentication portal myportal {
|
||||
crypto default token lifetime 3600 # Seconds until you have to re-authenticate
|
||||
enable identity provider generic
|
||||
cookie insecure off # Set to "on" if you're not using HTTPS
|
||||
|
||||
transform user {
|
||||
match realm generic
|
||||
action add role user
|
||||
}
|
||||
}
|
||||
|
||||
authorization policy mypolicy {
|
||||
set auth url /caddy-security/oauth2/generic
|
||||
allow roles user
|
||||
inject headers with claims
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:80 {
|
||||
@auth {
|
||||
path /caddy-security/*
|
||||
}
|
||||
|
||||
route @auth {
|
||||
authenticate with myportal
|
||||
}
|
||||
|
||||
route /* {
|
||||
authorize with mypolicy
|
||||
root * /app
|
||||
file_server
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -30,19 +30,26 @@ WORKDIR /build
|
||||
RUN npm install -g pnpm@latest-10
|
||||
|
||||
COPY . .
|
||||
COPY --from=build /build/deploy ./deploy
|
||||
|
||||
RUN pnpm install && pnpm run css-build
|
||||
RUN npx -y pagefind --site deploy
|
||||
|
||||
# ==================================================
|
||||
# Run Image
|
||||
# ==================================================
|
||||
FROM caddy:2.9.1-alpine
|
||||
#FROM caddy:2.9.1-alpine
|
||||
FROM ghcr.io/authcrunch/authcrunch:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=build /build/deploy .
|
||||
COPY --from=css /build/deploy .
|
||||
COPY --from=css /build/content/static/output.css ./static/output.css
|
||||
COPY --from=css /build/deploy/pagefind ./pagefind
|
||||
COPY Caddyfile /etc/caddy/Caddyfile
|
||||
|
||||
RUN /usr/bin/caddy fmt --overwrite /etc/caddy/Caddyfile
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["/usr/bin/caddy", "file-server", "--root", "/app", "--listen", ":80"]
|
||||
CMD ["/usr/bin/caddy", "run", "--config", "/etc/caddy/Caddyfile"]
|
||||
|
||||
34
README.md
34
README.md
@@ -1,32 +1,10 @@
|
||||
---
|
||||
date: 2025-4-02
|
||||
updated: 2025-04-02
|
||||
author: "Michael Housh"
|
||||
---
|
||||
|
||||
# Homelab Documentation
|
||||
|
||||
Documentation about how the homelab is setup.
|
||||
A static website that holds documentation related to the company.
|
||||
|
||||
## Containers
|
||||
## Usage
|
||||
|
||||
Services run inside of docker containers that are spread between several
|
||||
servers, which run them. The containers are deployed using a container
|
||||
orchestrator, currently using [komo](https://komo.housh.dev).
|
||||
|
||||
All of the services have a corresponding repository for their configuration that
|
||||
is hosted on an [internal git server](https://git.housh.dev/homelab). The
|
||||
configuration will consist of a docker compose file (generally named
|
||||
`compose.yaml`). There is often an `example.env` file for the service, these are
|
||||
examples for documentation and variable naming purposes. The environment
|
||||
variables themselves are setup in the container orchestrator for the service.
|
||||
|
||||
### Container orchestrator
|
||||
|
||||
The container orchestrator is where the actual configuration for the service is
|
||||
done. It configures which physical server that the service will run on, it is
|
||||
responsible for pulling the proper container images, pulls the configuration /
|
||||
`compoose.yaml` file from the repository, sets up environment variables, and
|
||||
deploys the service onto the server.
|
||||
|
||||
It also has some features for monitoring CPU and Memory usage of the servers.
|
||||
1. Install dependencies `pnpm install`
|
||||
1. Run & watch for css changes, this requires two terminal processes.
|
||||
1. `pnpm run css-watch`
|
||||
1. `just run`
|
||||
|
||||
@@ -54,6 +54,40 @@ extension Item where M == ArticleMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Item<ArticleMetadata> {
|
||||
|
||||
/// Iterate through the aritcles, getting all the years that articles
|
||||
/// have been written or updated.
|
||||
func years() -> Set<String> {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy"
|
||||
return reduce(into: Set()) { set, item in
|
||||
let date = dateFormatter.string(from: item.getDate())
|
||||
set.insert(date)
|
||||
if let updatedDate = item.getUpdatedDate() {
|
||||
set.insert(dateFormatter.string(from: updatedDate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate through the articles and get the unique tags.
|
||||
func uniqueTags() -> Set<String> {
|
||||
reduce(into: Set()) { set, item in
|
||||
for tag in item.metadata.tags {
|
||||
set.insert(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate through the articles and get the unique tags along with the count of
|
||||
/// how many times the tag is used.
|
||||
func uniqueTagsWithCount() -> [(String, Int)] {
|
||||
let tags = flatMap { $0.metadata.tags }
|
||||
let tagsWithCounts = tags.reduce(into: [:]) { $0[$1, default: 0] += 1 }
|
||||
return tagsWithCounts.sorted { $0.1 > $1.1 }
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Most of these are taken from https://github.com/loopwerk/loopwerk.io
|
||||
|
||||
extension String {
|
||||
|
||||
@@ -72,5 +72,18 @@ struct Run {
|
||||
// All the remaining files that were not parsed to markdown, so for example images, raw html files and css,
|
||||
// are copied as-is to the output folder.
|
||||
.staticFiles()
|
||||
|
||||
// Run saga again on articles, to collect search index.
|
||||
// try await Saga(input: "content", output: "deploy")
|
||||
// .register(
|
||||
// folder: "articles",
|
||||
// metadata: ArticleMetadata.self,
|
||||
// readers: [.plainReader],
|
||||
// filter: \.public,
|
||||
// writers: [
|
||||
// .listWriter(renderJson, output: "../search.json")
|
||||
// ]
|
||||
// )
|
||||
// .run()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
/// This is used to render base layouts appropriately for the given section.
|
||||
enum Section: String {
|
||||
case home
|
||||
case about
|
||||
case articles
|
||||
case notFound
|
||||
}
|
||||
|
||||
110
Sources/Docs/Templates/ArticleGrid.swift
Normal file
110
Sources/Docs/Templates/ArticleGrid.swift
Normal file
@@ -0,0 +1,110 @@
|
||||
import Foundation
|
||||
import HTML
|
||||
import Saga
|
||||
|
||||
/// Displays lists of articles sectioned by a key.
|
||||
///
|
||||
///
|
||||
struct ArticleGrid: NodeConvertible {
|
||||
|
||||
let articles: [(key: String, value: [Item<ArticleMetadata>])]
|
||||
let canocicalURL: String
|
||||
let title: String
|
||||
let rssLink: String
|
||||
let extraHeader: NodeConvertible
|
||||
let renderArticle: (Item<ArticleMetadata>) -> Node
|
||||
let header: (String) -> Node
|
||||
|
||||
init(
|
||||
articles: [(key: String, value: [Item<ArticleMetadata>])],
|
||||
canocicalURL: String,
|
||||
title: String,
|
||||
rssLink: String,
|
||||
extraHeader: any NodeConvertible = Node.fragment([]),
|
||||
renderArticle: @escaping (Item<ArticleMetadata>) -> Node = { renderArticleForGrid(article: $0, border: false) },
|
||||
header: @escaping (String) -> Node
|
||||
) {
|
||||
self.articles = articles
|
||||
self.canocicalURL = canocicalURL
|
||||
self.title = title
|
||||
self.rssLink = rssLink
|
||||
self.extraHeader = extraHeader
|
||||
self.renderArticle = renderArticle
|
||||
self.header = header
|
||||
}
|
||||
|
||||
private var allItems: [Item<ArticleMetadata>] {
|
||||
articles.reduce(into: [Item<ArticleMetadata>]()) { $0 += $1.value }
|
||||
}
|
||||
|
||||
private var sortedYears: [String] {
|
||||
allItems.years().sorted { $0 > $1 }
|
||||
}
|
||||
|
||||
private var sortedTags: [(String, Int)] {
|
||||
allItems.uniqueTagsWithCount()
|
||||
}
|
||||
|
||||
private func sidebarLink(
|
||||
_ label: String,
|
||||
href: String
|
||||
) -> Node {
|
||||
a(
|
||||
class: "text-slate-300 font-semibold [&:hover]:text-slate-200",
|
||||
href: href
|
||||
) {
|
||||
div(class: "flex w-full p-2 [&:hover]:border-b border-orange-400\(href == canocicalURL ? " active" : "")") {
|
||||
span(class: "mx-8") { label }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func asNode() -> Node {
|
||||
baseLayout(
|
||||
canocicalURL: canocicalURL,
|
||||
section: .articles,
|
||||
title: title,
|
||||
rssLink: rssLink,
|
||||
extraHeader: extraHeader
|
||||
) {
|
||||
div(class: "grid grid-cols-4") {
|
||||
// Sidebar
|
||||
div(class: "overflow-auto border-r border-slate-200") {
|
||||
section(class: "pt-2") {
|
||||
sidebarLink("All Articles", href: "/articles/")
|
||||
}
|
||||
// Years
|
||||
section(class: "pt-2") {
|
||||
div(class: "flex ps-2") {
|
||||
span(class: "mt-2 ps-2 font-extrabold text-slate-400") { "YEARS" }
|
||||
}
|
||||
sortedYears.map { year in
|
||||
sidebarLink(year, href: "/articles/\(year)/")
|
||||
}
|
||||
}
|
||||
// Tags
|
||||
section(class: "pt-2") {
|
||||
div(class: "flex ps-2 pt-2") {
|
||||
span(class: "mt-2 ps-2 font-extrabold text-slate-400") { "TAGS" }
|
||||
}
|
||||
sortedTags.map { tag, count in
|
||||
sidebarLink("\(tag) (\(count))", href: "/articles/tag/\(tag)/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Articles
|
||||
div(class: "col-span-3") {
|
||||
articles.map { key, articles in
|
||||
section {
|
||||
header(key)
|
||||
div(class: "grid gap-10 mx-6 mb-16") {
|
||||
articles.map(renderArticle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,15 +20,25 @@ func baseLayout(
|
||||
return [
|
||||
.documentType("html"),
|
||||
html(lang: "en-US") {
|
||||
generateHeader(pageTitle, extraHeader)
|
||||
body(class: "text-white text-lg pb-5 font-avenir \(section.rawValue)") {
|
||||
generateHead(pageTitle, extraHeader)
|
||||
body(class: "text-white text-lg font-avenir \(section.rawValue)") {
|
||||
siteHeader(section)
|
||||
|
||||
div(class: "container") {
|
||||
// mx-10
|
||||
div(class: "mb-auto") {
|
||||
children()
|
||||
}
|
||||
|
||||
footer(rssLink)
|
||||
if section == .articles {
|
||||
footer(rssLink)
|
||||
}
|
||||
// NOTE: These need to stay at / near bottom of page, so that icons are
|
||||
// generated properly.
|
||||
script(src: "https://unpkg.com/lucide@latest")
|
||||
Node.raw("""
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
</script>
|
||||
""")
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -46,24 +56,16 @@ private func siteHeader(_ section: Section) -> Node {
|
||||
}
|
||||
}
|
||||
}
|
||||
nav(class: "menu") {
|
||||
ul(class: "flex flex-wrap gap-x-2 lg:gap-x-5") {
|
||||
li {
|
||||
a(class: section == .articles ? "active" : "", href: "/articles/") { "Articles" }
|
||||
}
|
||||
li {
|
||||
a(href: "https://uptime.housh.dev/status/housh-dev", rel: "nofollow", target: "_blank") { "Server-Monitor" }
|
||||
}
|
||||
li {
|
||||
a(class: section == .about ? "active" : "", href: "/about.html") { "About" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Explore search being hidden / triggered by a button and hover above
|
||||
// the page content.
|
||||
div(class: "font-avenir w-full p-4 px-8", id: "search") {}
|
||||
div(class: "mt-2 mb-0 w-full border-b border-slate-200")
|
||||
}
|
||||
}
|
||||
|
||||
private func footer(_ rssLink: String) -> Node {
|
||||
div(class: "site-footer text-slate-400 border-t border-light text-center pt-2 text-sm") {
|
||||
div(class: "text-slate-400 border-t border-light text-center pt-2 text-sm") {
|
||||
div {
|
||||
"Copyright © Michael Housh \(Date().description.prefix(4))."
|
||||
}
|
||||
@@ -78,19 +80,16 @@ private func footer(_ rssLink: String) -> Node {
|
||||
"("
|
||||
%a(
|
||||
class: "[&:hover]:border-b border-green-400",
|
||||
href: "https://github.com/m-housh/mhoush.com",
|
||||
href: "https://git.housh.dev/homelab/docs",
|
||||
rel: "nofollow",
|
||||
target: "_blank"
|
||||
) { "source" }
|
||||
%")."
|
||||
}
|
||||
script(src: "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js")
|
||||
script(src: "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/keep-markup/prism-keep-markup.min.js")
|
||||
script(src: "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js")
|
||||
}
|
||||
}
|
||||
|
||||
private func generateHeader(_ pageTitle: String, _ extraHeader: NodeConvertible) -> Node {
|
||||
private func generateHead(_ pageTitle: String, _ extraHeader: NodeConvertible) -> Node {
|
||||
head {
|
||||
meta(charset: "utf-8")
|
||||
meta(content: "#0e1112", name: "theme-color", customAttributes: ["media": "(prefers-color-scheme: dark)"])
|
||||
@@ -122,11 +121,20 @@ private func generateHeader(_ pageTitle: String, _ extraHeader: NodeConvertible)
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="/static/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<script defer data-domain="housh.dev" src="https://plausible.housh.dev/js/script.outbound-links.js"></script>
|
||||
""")
|
||||
link(href: "/static/output.css", rel: "stylesheet")
|
||||
link(href: "/static/style.css", rel: "stylesheet")
|
||||
link(href: "/articles/feed.xml", rel: "alternate", title: SiteMetadata.name, type: "application/rss+xml")
|
||||
extraHeader
|
||||
script(src: "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js")
|
||||
Node.raw("""
|
||||
<script src="/pagefind/pagefind-ui.js"></script>
|
||||
<link href="/pagefind/pagefind-ui.css" rel="stylesheet">
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
new PagefindUI({ element: "#search", showSubResults: true });
|
||||
});
|
||||
</script>
|
||||
""")
|
||||
link(href: "/static/style.css", rel: "stylesheet")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,19 +40,7 @@ func generateHeader(
|
||||
meta(content: "1014", name: "og:image:width"),
|
||||
meta(content: "530", name: "og:image:height"),
|
||||
script(crossorigin: "anonymous", src: "https://kit.fontawesome.com/f209982030.js"),
|
||||
Node.raw("""
|
||||
<script>
|
||||
MathJax = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
"""),
|
||||
script(defer: true, src: "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js")
|
||||
script(src: "https://cdn.jsdelivr.net/npm/minisearch@7.1.2/dist/umd/index.min.js")
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
75
Sources/Docs/Templates/HomeLink.swift
Normal file
75
Sources/Docs/Templates/HomeLink.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
import HTML
|
||||
|
||||
/// Represents homepage link configuration.
|
||||
struct HomeLink {
|
||||
let icon: String
|
||||
let title: String
|
||||
let description: String
|
||||
let href: String
|
||||
let linkType: LinkType
|
||||
|
||||
enum LinkType {
|
||||
case `internal`
|
||||
case external
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeLink {
|
||||
/// Create an internal link (opens in the same tab).
|
||||
static func `internal`(
|
||||
_ title: String,
|
||||
icon: String,
|
||||
href: String,
|
||||
description: String
|
||||
) -> Self {
|
||||
self.init(icon: icon, title: title, description: description, href: href, linkType: .internal)
|
||||
}
|
||||
|
||||
/// Create an external link (opens in a different tab).
|
||||
static func external(
|
||||
_ title: String,
|
||||
icon: String,
|
||||
href: String,
|
||||
description: String
|
||||
) -> Self {
|
||||
self.init(icon: icon, title: title, description: description, href: href, linkType: .external)
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeLink: NodeConvertible {
|
||||
|
||||
func asNode() -> Node {
|
||||
switch linkType {
|
||||
case .internal: return internalLink()
|
||||
case .external: return externalLink()
|
||||
}
|
||||
}
|
||||
|
||||
private func internalLink() -> Node {
|
||||
a(
|
||||
class: "bg-orange-400 border-2 border-green-600 p-4 rounded-lg [&:hover]:bg-orange-500",
|
||||
href: href
|
||||
) {
|
||||
div(class: "flex text-3xl") {
|
||||
i(class: "mt-1", customAttributes: ["data-lucide": icon])
|
||||
span(class: "ps-2") { title }
|
||||
}
|
||||
span(class: "text-sm") { description }
|
||||
}
|
||||
}
|
||||
|
||||
private func externalLink() -> Node {
|
||||
a(
|
||||
class: "bg-orange-400 border-2 border-green-600 p-4 rounded-lg [&:hover]:bg-orange-500",
|
||||
href: href,
|
||||
rel: "nofollow",
|
||||
target: "_blank'"
|
||||
) {
|
||||
div(class: "flex text-3xl") {
|
||||
i(class: "mt-1", customAttributes: ["data-lucide": icon])
|
||||
span(class: "ps-2") { title }
|
||||
}
|
||||
span(class: "text-sm") { description }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,19 @@ func tagPrefix(index: Int, totalTags: Int) -> Node {
|
||||
return ""
|
||||
}
|
||||
|
||||
func renderArticleInfo(_ article: Item<ArticleMetadata>) -> Node {
|
||||
div(class: "text-slate-400 gray-links text-sm mb-8") {
|
||||
enum RenderArticleInfoContext {
|
||||
case article
|
||||
case preview
|
||||
}
|
||||
|
||||
func renderArticleInfo(_ article: Item<ArticleMetadata>, context: RenderArticleInfoContext = .article) -> Node {
|
||||
div(class: "text-slate-400 text-sm mb-8\(context == .preview ? " -mt-2" : "")") {
|
||||
span(class: "border-r border-gray pr-2 mr-2") {
|
||||
article.getDate().formatted("MMMM dd, yyyy")
|
||||
}
|
||||
|
||||
if let updated = article.getUpdatedDate() {
|
||||
span(class: "border-r border-gray pr-2 mr-2") {
|
||||
span(class: "border-r border-slate-400 pr-2 mr-2") {
|
||||
"Updated: \(updated.formatted("MMMM dd, yyyy"))"
|
||||
}
|
||||
}
|
||||
@@ -30,9 +35,6 @@ func renderArticleInfo(_ article: Item<ArticleMetadata>) -> Node {
|
||||
article.metadata.tags.sorted().enumerated().map { index, tag in
|
||||
Node.fragment([
|
||||
%tagPrefix(index: index, totalTags: article.metadata.tags.count),
|
||||
Node.raw("""
|
||||
<i class="fa fa-home"></i>
|
||||
"""),
|
||||
%a(class: "text-orange-400 [&:hover]:border-b border-green-400", href: "/articles/tag/\(tag.slugified)/") {
|
||||
tag
|
||||
}
|
||||
@@ -100,22 +102,43 @@ func renderArticle(context: ItemRenderingContext<ArticleMetadata>) -> Node {
|
||||
title: context.item.title,
|
||||
extraHeader: generateHeader(.article(context.item))
|
||||
) {
|
||||
article(class: "pt-8") {
|
||||
h1 { context.item.title }
|
||||
div {
|
||||
renderArticleInfo(context.item)
|
||||
article(class: "pt-8 mx-10") {
|
||||
div(class: "relative") {
|
||||
a(class: "absolute top-4 right-4", href: "/articles", id: "close") {
|
||||
i(customAttributes: ["data-lucide": "x"])
|
||||
}
|
||||
}
|
||||
div(class: "bg-slate-800 py-10") {
|
||||
div(class: "mx-10") {
|
||||
h1 { context.item.title }
|
||||
div {
|
||||
renderArticleInfo(context.item)
|
||||
}
|
||||
// Only index the body of the articles for search.
|
||||
div(customAttributes: ["data-pagefind-body": ""]) {
|
||||
Node.raw(context.item.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
Node.raw(context.item.body)
|
||||
}
|
||||
|
||||
div(class: "border-t border-light pt-8 mt-16") {
|
||||
div(class: "border-t border-light p-10 mt-16", id: "recents") {
|
||||
div(class: "grid lg:grid-cols-2") {
|
||||
h4(class: "text-3xl text-amber-500 font-extrabold mb-8") { otherArticles.title }
|
||||
if let tag = otherArticles.tag {
|
||||
a(href: "/articles/tag/\(tag)") {
|
||||
div(class: " [&:hover]:border-b border-orange px-5 flex flex-row gap-5") {
|
||||
img(src: "/static/img/tag.svg", width: "40")
|
||||
span(class: "text-4xl font-extrabold text-orange") { tag }
|
||||
div(class: " [&:hover]:border-b border-green-500 px-5 flex flex-row gap-5") {
|
||||
img(class: "-mt-2", src: "/static/img/tag.svg", width: "40")
|
||||
div(class: "block") {
|
||||
div(class: "block") {
|
||||
span(class: "mt-2 text-4xl font-extrabold text-orange") { tag }
|
||||
}
|
||||
div(class: "block") {
|
||||
span(class: "text-sm text-orange-400") {
|
||||
"View related articles with this tag."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,16 +160,19 @@ func renderArticle(context: ItemRenderingContext<ArticleMetadata>) -> Node {
|
||||
}
|
||||
}
|
||||
|
||||
func renderArticleForGrid(article: Item<ArticleMetadata>) -> Node {
|
||||
section {
|
||||
h3(class: "post-title text-2xl font-bold mb-2") {
|
||||
a(class: "[&:hover]:border-b border-orange-400", href: article.url) { article.title }
|
||||
}
|
||||
renderArticleInfo(article)
|
||||
p {
|
||||
a(href: article.url) {
|
||||
div {
|
||||
article.summary
|
||||
func renderArticleForGrid(article: Item<ArticleMetadata>, border: Bool = true) -> Node {
|
||||
// bg-slate-800
|
||||
div(class: "p-4\(border ? " border border-slate-400 rounded-lg" : "")") {
|
||||
section {
|
||||
h3(class: "text-2xl font-bold") {
|
||||
a(class: "[&:hover]:border-b border-green-500", href: article.url) { article.title }
|
||||
}
|
||||
renderArticleInfo(article, context: .preview)
|
||||
p {
|
||||
a(href: article.url) {
|
||||
div {
|
||||
article.summary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,37 +2,24 @@ import Foundation
|
||||
import HTML
|
||||
import Saga
|
||||
|
||||
func uniqueTagsWithCount(_ articles: [Item<ArticleMetadata>]) -> [(String, Int)] {
|
||||
let tags = articles.flatMap { $0.metadata.tags }
|
||||
let tagsWithCounts = tags.reduce(into: [:]) { $0[$1, default: 0] += 1 }
|
||||
return tagsWithCounts.sorted { $0.1 > $1.1 }
|
||||
}
|
||||
|
||||
func renderArticles(context: ItemsRenderingContext<ArticleMetadata>) -> Node {
|
||||
func renderArticles(context: ItemsRenderingContext<ArticleMetadata>) -> NodeConvertible {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy"
|
||||
|
||||
let articlesPerYear = Dictionary(grouping: context.items, by: { dateFormatter.string(from: $0.date) })
|
||||
let sortedByYearDescending = articlesPerYear.sorted { $0.key > $1.key }
|
||||
|
||||
return baseLayout(canocicalURL: "/articles/", section: .articles, title: "Articles", rssLink: "", extraHeader: "") {
|
||||
// TODO: Add list of tags here that can be navigated to.
|
||||
sortedByYearDescending.map { year, articles in
|
||||
div {
|
||||
div(class: "border-b border-light flex flex-row gap-4 mb-12") {
|
||||
img(src: "/static/img/calendar.svg", width: "40")
|
||||
h1(class: "text-4xl font-extrabold pt-3") { year }
|
||||
}
|
||||
|
||||
div(class: "grid gap-10 mb-16") {
|
||||
articles.map { renderArticleForGrid(article: $0) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return baseRenderArticles(
|
||||
sortedByYearDescending,
|
||||
canocicalURL: "/articles/",
|
||||
title: "Articles",
|
||||
rssLink: "",
|
||||
extraHeader: "",
|
||||
label: yearHeader(_:)
|
||||
)
|
||||
}
|
||||
|
||||
func renderTag<T>(context: PartitionedRenderingContext<T, ArticleMetadata>) -> Node {
|
||||
func renderTag<T>(context: PartitionedRenderingContext<T, ArticleMetadata>) -> NodeConvertible {
|
||||
let extraHeader = link(
|
||||
href: "/articles/tag/\(context.key.slugified)/feed.xml",
|
||||
rel: "alternate",
|
||||
@@ -41,54 +28,86 @@ func renderTag<T>(context: PartitionedRenderingContext<T, ArticleMetadata>) -> N
|
||||
)
|
||||
|
||||
return baseRenderArticles(
|
||||
context.items,
|
||||
(context.key.slugified, context.items),
|
||||
canocicalURL: "/articles/tag/\(context.key.slugified)/",
|
||||
title: "Articles in \(context.key)",
|
||||
rssLink: "tag/\(context.key.slugified)/",
|
||||
extraHeader: extraHeader
|
||||
) {
|
||||
div(class: "border-b border-light grid lg:grid-cols-2 mb-12") {
|
||||
) { tag in
|
||||
div(class: "mb-12 px-6 pt-6") {
|
||||
a(href: "/articles") {
|
||||
div(class: "flex flex-row gap-2") {
|
||||
div(class: "flex flex-row gap-4") {
|
||||
img(src: "/static/img/tag.svg", width: "40")
|
||||
h1(class: "text-4xl font-extrabold") { "\(context.key)" }
|
||||
h1(class: "[&:hover]:border-b border-green text-2xl font-extrabold text-orange px-4") { "«" }
|
||||
h1(class: "text-4xl font-extrabold") { tag }
|
||||
h1(class: "text-2xl font-extrabold") { "«" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renderYear<T>(context: PartitionedRenderingContext<T, ArticleMetadata>) -> Node {
|
||||
baseRenderArticles(context.items, canocicalURL: "/articles/\(context.key)/", title: "Articles in \(context.key)")
|
||||
func renderYear<T>(context: PartitionedRenderingContext<T, ArticleMetadata>) -> NodeConvertible {
|
||||
baseRenderArticles(
|
||||
(context.key.slugified, context.items),
|
||||
canocicalURL: "/articles/\(context.key)/",
|
||||
title: "Articles in \(context.key)",
|
||||
label: { year in
|
||||
div(class: "pt-6 w-full") {
|
||||
a(href: "/articles/") {
|
||||
div(class: "px-6 flex flex-row gap-4 ") {
|
||||
img(src: "/static/img/calendar.svg", width: "40")
|
||||
h1(class: "text-4xl font-extrabold pt-3") { year }
|
||||
h1(class: "text-2xl font-extrabold") { "«" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func yearHeader(_ year: String) -> Node {
|
||||
div(class: "pt-6 w-full") {
|
||||
div(class: "px-6 flex flex-row gap-4 ") {
|
||||
img(src: "/static/img/calendar.svg", width: "40")
|
||||
h1(class: "text-4xl font-extrabold pt-3") { year }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func baseRenderArticles(
|
||||
_ articles: [Item<ArticleMetadata>],
|
||||
_ articles: [(key: String, value: [Item<ArticleMetadata>])],
|
||||
canocicalURL: String,
|
||||
title pageTitle: String,
|
||||
rssLink: String = "",
|
||||
extraHeader: NodeConvertible = Node.fragment([]),
|
||||
@NodeBuilder label: () -> Node = { Node.fragment([]) }
|
||||
) -> Node {
|
||||
return baseLayout(
|
||||
@NodeBuilder label: @escaping (String) -> Node = { _ in Node.fragment([]) }
|
||||
) -> NodeConvertible {
|
||||
ArticleGrid(
|
||||
articles: articles,
|
||||
canocicalURL: canocicalURL,
|
||||
section: .articles,
|
||||
title: pageTitle,
|
||||
rssLink: rssLink,
|
||||
extraHeader: extraHeader
|
||||
) {
|
||||
label()
|
||||
articles.map { article in
|
||||
section(class: "mb-10") {
|
||||
h1(class: "post-title text-2xl font-bold mb-2") {
|
||||
a(class: "[&:hover]:border-b border-orange", href: article.url) { article.title }
|
||||
}
|
||||
renderArticleInfo(article)
|
||||
p(class: "mt-4") {
|
||||
a(href: article.url) { article.summary }
|
||||
}
|
||||
}
|
||||
}
|
||||
) { key in
|
||||
label(key)
|
||||
}
|
||||
}
|
||||
|
||||
private func baseRenderArticles(
|
||||
_ articles: (key: String, value: [Item<ArticleMetadata>]),
|
||||
canocicalURL: String,
|
||||
title pageTitle: String,
|
||||
rssLink: String = "",
|
||||
extraHeader: NodeConvertible = Node.fragment([]),
|
||||
@NodeBuilder label: @escaping (String) -> Node = { _ in Node.fragment([]) }
|
||||
) -> NodeConvertible {
|
||||
ArticleGrid(
|
||||
articles: [articles],
|
||||
canocicalURL: canocicalURL,
|
||||
title: pageTitle,
|
||||
rssLink: rssLink,
|
||||
extraHeader: extraHeader
|
||||
) { key in
|
||||
label(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,71 @@ func renderPage(context: ItemRenderingContext<PageMetadata>) -> Node {
|
||||
|
||||
func renderHome(body: String) -> Node {
|
||||
div {
|
||||
div(class: "my-24 uppercase font-avenir text-[40px] leading-[1.25] font-thin text-center [&>h1>strong]:font-bold") {
|
||||
div(class: "my-24 font-avenir leading-[1.25] font-thin text-center [&>h1>strong]:font-bold") {
|
||||
Node.raw(body)
|
||||
}
|
||||
div(class: "px-10") {
|
||||
div(class: "bg-slate-800 p-10 rounded-lg border border-slate-400") {
|
||||
h2 { "Quick Links" }
|
||||
div(class: "grid lg:grid-cols-2 gap-6") {
|
||||
HomeLink.internal(
|
||||
"Articles",
|
||||
icon: "newspaper",
|
||||
href: "/articles/",
|
||||
description: "Click here to view all articles."
|
||||
)
|
||||
|
||||
HomeLink.external(
|
||||
"Purchase Orders",
|
||||
icon: "calculator",
|
||||
href: "https://po.housh.dev",
|
||||
description: "Purchase orders application."
|
||||
)
|
||||
|
||||
HomeLink.external(
|
||||
"Service Monitor",
|
||||
icon: "heart-pulse",
|
||||
href: "https://uptime.housh.dev/status/housh-dev",
|
||||
description: "Server and services uptime status page."
|
||||
)
|
||||
|
||||
HomeLink.external(
|
||||
"Unifi Console",
|
||||
icon: "earth",
|
||||
href: "https://unifi.ui.com",
|
||||
description: "Network management."
|
||||
)
|
||||
|
||||
HomeLink.external(
|
||||
"Excalidraw",
|
||||
icon: "pen-tool",
|
||||
href: "https://draw.housh.dev",
|
||||
description: "A drawing utility that runs locally in your browser."
|
||||
)
|
||||
|
||||
HomeLink.external(
|
||||
"Gitea",
|
||||
icon: "git-branch",
|
||||
href: "https://git.housh.dev/explore/repos",
|
||||
description: "Explore source code."
|
||||
)
|
||||
|
||||
HomeLink.external(
|
||||
"Legacy Purchase Orders",
|
||||
icon: "file-archive",
|
||||
href: "https://legach-po.housh.dev",
|
||||
description: "Legacy purchase order application (pre-2025)."
|
||||
)
|
||||
|
||||
HomeLink.external(
|
||||
"HVAC Toolbox",
|
||||
icon: "hammer",
|
||||
href: "https://hvac-toolbox.com",
|
||||
description: "A collection of HVAC calculators."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
40
Sources/Docs/Templates/TagGrid.swift
Normal file
40
Sources/Docs/Templates/TagGrid.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
import HTML
|
||||
import Saga
|
||||
|
||||
// TODO: Currently not used, remove in the future.
|
||||
struct TagGrid: NodeConvertible {
|
||||
|
||||
let items: [Item<ArticleMetadata>]
|
||||
|
||||
func asNode() -> Node {
|
||||
div(class: "mt-1 bg-slate-950 rounded-ss-lg border-b border-slate-200") {
|
||||
// Grid Header
|
||||
div(class: "flex flex-row gap-4 px-4 pt-4") {
|
||||
img(src: "/static/img/tag.svg", width: "40")
|
||||
h1 { "Tags" }
|
||||
}
|
||||
div(class: "px-4 pb-8 -mt-2") {
|
||||
span(class: "text-sm text-green-300") { "Click on a tag to view related articles." }
|
||||
}
|
||||
// Grid items.
|
||||
div(class: "grid sm:grid-cols-2 lg:grid-cols-4 gap-4 px-6 pb-6") {
|
||||
items.uniqueTagsWithCount().map { tag, count in
|
||||
div {
|
||||
a(class: "bg-slate-900 [&:hover]:bg-slate-800", href: "/articles/tag/\(tag)") {
|
||||
div(class: "flex flex-row justify-between bg-slate-900 [&:hover]:bg-slate-800 py-2 px-4 border-2 border-orange-400 rounded-lg") {
|
||||
div(class: "justify-items-start") {
|
||||
span(class: "font-bold text-green-300") { tag }
|
||||
div(class: "text-sm") {
|
||||
span { "\(count) articles" }
|
||||
}
|
||||
}
|
||||
|
||||
img(src: "/static/img/tag.svg", width: "30")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
compose.dev.yaml
Normal file
19
compose.dev.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
services:
|
||||
oauth2-proxy:
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:latest
|
||||
command: --config /oauth2-proxy/oauth2-proxy.cfg
|
||||
volumes:
|
||||
- ./oauth2-proxy:/oauth2-proxy
|
||||
ports:
|
||||
- 4180:4180
|
||||
|
||||
docs:
|
||||
build:
|
||||
context: .
|
||||
container_name: docs
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
ports:
|
||||
- ${PORT:-8081}:80
|
||||
depends_on:
|
||||
- oauth2-proxy
|
||||
@@ -3,6 +3,7 @@ services:
|
||||
image: git.housh.dev/homelab/docs:latest
|
||||
container_name: docs
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
ports:
|
||||
- ${PORT:-8081}:80
|
||||
networks:
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
section: about
|
||||
---
|
||||
|
||||
# About
|
||||
|
||||
Internal documentation site for **Housh - The Home Energy Experts**
|
||||
@@ -1,8 +1,9 @@
|
||||
---
|
||||
date: 2025-4-02
|
||||
updated: 2025-4-03
|
||||
updated: 2025-4-08
|
||||
author: "Michael Housh"
|
||||
tags: network, infrastructure
|
||||
primaryTag: infrastructure
|
||||
---
|
||||
|
||||
# Networking
|
||||
@@ -10,9 +11,31 @@ tags: network, infrastructure
|
||||
All of the networking setup is done through [unifi](https://unifi.ui.com). The
|
||||
network is segmented into several different networks to isolate communication.
|
||||
|
||||
> Note: If you are unable to connect to the unifi management console linked
|
||||
> above or if the internet is down, you can connect directly with the management
|
||||
> console at `http://192.168.1.1`.
|
||||
|
||||
## Backup
|
||||
|
||||
The network management console get's backed up automatically each week (Sundays
|
||||
@2:30am), however you can manually backup the server by going to
|
||||
`Settings -> Control Plane -> Backups`. This is where you can also restore from
|
||||
a backup if needed.
|
||||
|
||||
## Networks
|
||||
|
||||
An overview of the networks that are setup.
|
||||
A brief overview of the networks that are setup, their uses, and why they are
|
||||
needed.
|
||||
|
||||
| Network | VLAN ID | Subnet | Usable IP's |
|
||||
| --------- | ------- | ---------------- | ----------- |
|
||||
| Default | 1 | 192.168.1.0/24 | 249 |
|
||||
| Main | 10 | 192.168.10.0/24 | 205 |
|
||||
| Phones | 20 | 192.168.20.0/28 | 13 |
|
||||
| IoT | 30 | 192.168.30.0/24 | 249 |
|
||||
| housh.dev | 50 | 192.168.50.0/28 | 12 |
|
||||
| Guest | 60 | 192.168.60.0/26 | 61 |
|
||||
| Mangement | 254 | 192.168.254.0/24 | 249 |
|
||||
|
||||
### Default Network
|
||||
|
||||
@@ -21,8 +44,8 @@ unifi networking gear. It is also generally the network a new device will go if
|
||||
it is plugged into an ethernet cable / switch. For this reason this network is
|
||||
isolated from communicating with other networks.
|
||||
|
||||
New devices that end up on this network should be configured to the appropriate
|
||||
network by a network administrator.
|
||||
New devices that end up on this network should be configured / moved to the
|
||||
appropriate network by a network administrator.
|
||||
|
||||
### Management Network
|
||||
|
||||
@@ -33,26 +56,32 @@ someone gained access to the network.
|
||||
### Main Network
|
||||
|
||||
This is where the majority of "trusted" devices should be placed on the network,
|
||||
such as computers, phones, etc. This is also the network when people join the
|
||||
non-guest WiFi.
|
||||
such as computers, mobile phones, etc. This is also the network used when people
|
||||
join the non-guest WiFi.
|
||||
|
||||
This network has the ability to communicate with most all other networks.
|
||||
This network has the ability to communicate with most all other networks,
|
||||
therefore only trusted devices should be allowed on this network.
|
||||
|
||||
### housh.dev Network
|
||||
|
||||
This is the network where all the servers are placed. This network is primarily
|
||||
setup to allow "responses", but not initiate communication with other networks.
|
||||
This is to help reduce the risk if one of the servers gets compromised, an
|
||||
attacker should not easily be able to transition to another network.
|
||||
This is the network where the majority of servers are placed. This network is
|
||||
primarily setup to allow "responses", but not allowed to initiate communication
|
||||
with other networks. This is to help reduce the risk if one of the servers gets
|
||||
compromised, an attacker should not easily be able to transition to another
|
||||
network.
|
||||
|
||||
### Phones Network
|
||||
|
||||
This is the network where all the VoIP phones are on. It is considered
|
||||
"untrusted" and should not be able to communicate with any other network.
|
||||
|
||||
This is merely considered "untrusted" because there's no reason for anything on
|
||||
this network to try and reach anything else. It should only handle phone
|
||||
traffic.
|
||||
|
||||
### IoT Network
|
||||
|
||||
This is the network where all IoT (internet of things) devices are. This is
|
||||
This is the network where IoT (internet of things) devices are. This is
|
||||
considered an "untrusted" network and communications with other networks are
|
||||
minimized to what is actually needed to work. This network is not able to
|
||||
communicate with the internet, because these devices are made by so many
|
||||
@@ -64,6 +93,24 @@ such as home-pods and apple-tv because there are network challenges with these
|
||||
devices operating properly when placed on the IoT network, such as airdrop and
|
||||
screen casting (which may be resolved in the future).
|
||||
|
||||
### Guest Network
|
||||
|
||||
This is the network where guests are placed, it is considered "untrusted" and
|
||||
should only be able to access the internet. Devices on this network are also not
|
||||
able to communicate with other devices attached to the guest network.
|
||||
|
||||
## Wifi Networks
|
||||
|
||||
The following wifi networks are setup and broadcast via the access points. All
|
||||
networks require a password to use. Ask Michael for passwords if you need them.
|
||||
|
||||
| Wifi SSID | Network |
|
||||
| ------------------------ | ----------------------- |
|
||||
| Center of Monroe | Main |
|
||||
| Jarvis | IoT |
|
||||
| Center of Monroe - Guest | Guest |
|
||||
| Housh Home Energy | Main (VPN traffic only) |
|
||||
|
||||
## Firewall
|
||||
|
||||
The unifi management console is what handles firewall rules for the networks. It
|
||||
@@ -74,6 +121,11 @@ is accessed via `Settings -> Security -> Firewall` on the management console.
|
||||
This is where settings are made to either allow or deny traffic on the networks
|
||||
from communicating with other networks or the internet.
|
||||
|
||||
> Note: Be aware that making changes here may break things / render networks or
|
||||
> services to be unusable. It is recommended to make a backup prior to making
|
||||
> changes. One of the biggest things to _not_ do is block traffic from
|
||||
> `Main -> Gateway`, most everything else done is recoverable.
|
||||
|
||||
## DNS
|
||||
|
||||
DNS is what translates IP addresses to domain names (i.e.
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
---
|
||||
date: 2025-04-04
|
||||
updated: 2025-04-08
|
||||
tags: servers, infrastructure, homelab
|
||||
primaryTag: infrastructure
|
||||
---
|
||||
|
||||
# Servers
|
||||
# Servers Overview
|
||||
|
||||
Documentation about how the servers are setup.
|
||||
|
||||
@@ -15,21 +17,28 @@ services based on that. Meaning services that I run primarily for personal items
|
||||
are running on servers that I own, while services that are supporting business
|
||||
functionality run on the companies server.
|
||||
|
||||
All of the servers run the services in `Docker Containers`.
|
||||
All of the servers run the services in `Docker Containers`, which allows for
|
||||
them to be isolated from the host system (server) and makes them more easily
|
||||
portable between servers if needed.
|
||||
|
||||
There is also a `Raspberry-Pi` that runs `Home Assitant`, which is another one
|
||||
of my personal devices.
|
||||
|
||||
| Server | DNS Name | IP Address |
|
||||
| --------------------- | ---------------------- | ------------ |
|
||||
| mighty-mini (company) | mightymini.housh.dev | 192.168.50.6 |
|
||||
| franken-mini (mine) | frankenmini.housh.dev | 192.168.50.5 |
|
||||
| rogue-mini (mine) | roguemini.housh.dev | 192.168.50.4 |
|
||||
| home-assistant (mine) | homeassitant.housh.dev | 192.168.30.5 |
|
||||
| Server | DNS Name | IP Address |
|
||||
| -------------- | ---------------------- | -------------- |
|
||||
| mighty-mini | mightymini.housh.dev | 192.168.50.6 |
|
||||
| franken-mini | frankenmini.housh.dev | 192.168.50.5 |
|
||||
| rogue-mini | roguemini.housh.dev | 192.168.50.4 |
|
||||
| home-assistant | homeassitant.housh.dev | 192.168.30.5 |
|
||||
| NAS | nas.housh.dev | 192.168.10.105 |
|
||||
| Backup NAS | nas.hhe | 192.168.1.10 |
|
||||
|
||||
You can read more about the network setup
|
||||
[here](https://docs.housh.dev/articles/2025/network/).
|
||||
|
||||
> Note: The backup NAS is used to backup our primary NAS, for now it is not easy
|
||||
> to use, and will be used for camera / security footage in the future.
|
||||
|
||||
## Containers
|
||||
|
||||
Services run inside of docker containers that are spread between several
|
||||
@@ -43,7 +52,8 @@ is hosted on an [internal git server](https://git.housh.dev/homelab). The
|
||||
configuration will consist of a docker compose file (generally named
|
||||
`compose.yaml`). There is often an `example.env` file for the service, these are
|
||||
examples for documentation and variable naming purposes. The environment
|
||||
variables themselves are setup in the container orchestrator for the service.
|
||||
variables themselves are setup in the container orchestrator for the service to
|
||||
prevent sensitive data being "leaked".
|
||||
|
||||
### Container orchestrator
|
||||
|
||||
@@ -98,7 +108,7 @@ access may be implemented in the future. If access is required outside of our
|
||||
network then using our VPN is required. The VPN setup is done automatically via
|
||||
unifi (our network router).
|
||||
|
||||
`DNS` is what translates domain names to `IP` addresses, currently the public
|
||||
`DNS` is what translates domain names to `IP addresses`, currently the public
|
||||
DNS records are handled by cloudflare. Cloudflare is used to validate that we
|
||||
own the `housh.dev` domain name in order for Let's Encrypt to issue free `TLS`
|
||||
certificates. TLS is used to encrypt traffic over the web (`https://`).
|
||||
@@ -106,4 +116,7 @@ certificates. TLS is used to encrypt traffic over the web (`https://`).
|
||||
Internal DNS records are setup in our unifi router `Settings -> Routing -> DNS`.
|
||||
The internal DNS is fairly simple and just needs to map to servers appropriately
|
||||
(primarily just to the primary caddy instance, which then handles all the
|
||||
routing to the individual service that is requested).
|
||||
routing to the individual service that is requested). All devices that connect
|
||||
to the network will be able to use the internal DNS to resolve host names
|
||||
properly (meaning it all should just work automatically without any knowledge
|
||||
from the user).
|
||||
|
||||
74
content/articles/2025-04-07-PhoneSystem.md
Normal file
74
content/articles/2025-04-07-PhoneSystem.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
date: 2025-04-07
|
||||
tags: phones, infrastructure, unifi
|
||||
primaryTag: infrastructure
|
||||
---
|
||||
|
||||
# Phone System
|
||||
|
||||
This article is about the phone system at the office.
|
||||
|
||||
The phones are managed through the
|
||||
[unifi management console](https://unifi.ui.com). You must have administrator
|
||||
privileges in order to manage the phones.
|
||||
|
||||
Below is a list of our current phone numbers and the user's extensions.
|
||||
|
||||
| Name | Number | Extension |
|
||||
| ------- | ------------ | --------- |
|
||||
| Alicia | 513-252-2514 | 0100 |
|
||||
| Andy | 513-463-1695 | 0102 |
|
||||
| Michael | 513-953-4519 | 0101 |
|
||||
|
||||
[See network article for information about the phone network](/articles/2025/network/)
|
||||
|
||||
## Primary Numbers
|
||||
|
||||
Our primary numbers (`513-793-6374` & `800-793-6374`) forward to Alicia's
|
||||
number, which is the primary entry to our phone system. The 800 number is manged
|
||||
through [number-barn](https://www.numberbarn.com). The 513 number is managed
|
||||
through Cincinnati-Bell.
|
||||
|
||||
## Unifi Talk Application
|
||||
|
||||
The unifi talk application is where all users, groups, and call handling is
|
||||
managed.
|
||||
|
||||
### Assignments Section
|
||||
|
||||
The assignments section is where the devices, users, and groups are manged. The
|
||||
devices section is where physical phones are assigned to users. The users
|
||||
section is where user profiles and extensions are handled. The groups section is
|
||||
where user are assigned to groups that are selected when client calls in. They
|
||||
are used to direct calls to the appropriate people by ringing phones as a group
|
||||
or sequentially.
|
||||
|
||||
### Engagement Section
|
||||
|
||||
The engagement section of the application is where the call handling is setup.
|
||||
It is where the business hours are managed as well as setting up the menus a
|
||||
client hears when they call into the main number.
|
||||
|
||||

|
||||
|
||||
#### Change Business Hours
|
||||
|
||||
One of the most common tasks that needs managed is changing the business hours
|
||||
when there is a holiday during a weekday that we would typically be open. To
|
||||
change the hours click on `Engagement -> Business Hours` and remove the day that
|
||||
is a holiday.
|
||||
|
||||
> Note: When changing business hours for a holiday it is important to set them
|
||||
> back once the holiday is finished, so create a reminder so that you remember
|
||||
> to do that.
|
||||
|
||||
Once the day is removed then the `Non-Business Hours` flow will be used to route
|
||||
calls.
|
||||
|
||||

|
||||
|
||||
### Call Log
|
||||
|
||||
You can access history and phone recordings through the call log tab of the
|
||||
unifi management console. The AI tab transcribes calls into text that can be
|
||||
reviewed as well.
|
||||
51
content/articles/2025-04-07-TimeMachine.md
Normal file
51
content/articles/2025-04-07-TimeMachine.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
date: 2025-04-07
|
||||
tags: how-to, backups, nas
|
||||
primaryTag: how-to
|
||||
---
|
||||
|
||||
# Time Machine Backups
|
||||
|
||||
In this article we'll walk through how to setup time machine backups using the
|
||||
on site NAS (network attached storage).
|
||||
|
||||
## NAS Access
|
||||
|
||||
You should have received an email when your account was setup on the NAS that
|
||||
gives credentials to access the NAS, if you do not have access to that anymore
|
||||
than let Michael know and he can send them again.
|
||||
|
||||
Once setup, this should mount the folders that you have access to automatically
|
||||
when your computer is attached to the network and you have the
|
||||
[unifi identity app installed](https://www.ui.com/download/app/identity-desktop).
|
||||
|
||||
### Manually Connecting to NAS
|
||||
|
||||
You can also manually mount NAS folders by using the `Finder` application if the
|
||||
unifi identity application is not working for you.
|
||||
|
||||
1. Open the Finder application
|
||||
1. Choose connect to server from `Go -> Connect to server...` (or type ⌘K)
|
||||
1. In the server address field type: `smb://192.168.10.105`
|
||||
1. Enter your credentials to login to the server (attain username and password
|
||||
from Michael)
|
||||
1. Choose the `Personal-Drive` folder
|
||||
|
||||
## Time Machine
|
||||
|
||||
After you are connected to the NAS you can then setup time machine backups for
|
||||
your computer.
|
||||
|
||||
The time machine settings are found in the
|
||||
`System Settings -> General -> Time Machine` section of your system settings
|
||||
application.
|
||||
|
||||
1. Click the plus icon to add a new time machine backup location.
|
||||
1. Select the drive named `Personal-Drive`
|
||||
|
||||
> Note: Any of the other drives that appear will not work, the drive that you
|
||||
> select must be the Personal-Drive.
|
||||
|
||||
You should end up with something that looks similar to the image below.
|
||||
|
||||

|
||||
40
content/articles/2025-04-08-LinkSharing.md
Normal file
40
content/articles/2025-04-08-LinkSharing.md
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
date: 2025-04-08
|
||||
tags: how-to, nas
|
||||
primaryTag: how-to
|
||||
---
|
||||
|
||||
# Link Sharing
|
||||
|
||||
In this article, I'll share how you can share links from our internal NAS with
|
||||
someone.
|
||||
|
||||
## 1. Open the Web Console
|
||||
|
||||
Sharing links is done through the web console of the NAS. You can access the web
|
||||
console by clicking the `Your UniFI Drive` button in the `Unifi Identitiy`
|
||||
application in your toolbar.
|
||||
|
||||

|
||||
|
||||
## 2. Select File to Share
|
||||
|
||||
Next, you will want to navigate to the file you would like to share and select
|
||||
the three dots to the far right of the file and select `Share Link`.
|
||||
|
||||
This will open a menu that allows you to optionally set the expiration for the
|
||||
link, a limit on how many times the file can be accessed, and a password
|
||||
required to view the file. Once done you can click on `Create and Copy Link`.
|
||||
|
||||

|
||||
|
||||
## 3. Manage Shared Links
|
||||
|
||||
You can manage the shared link in the same way by navigating to the file you've
|
||||
shared and selecting `Share Link`, as we did above. Or you can access from the
|
||||
home dashboard by clicking `Manage Links` in the top right of the home page.
|
||||
|
||||
The `Manage Links` section gives you a list of all the files you've shared using
|
||||
links. You can reconfigure the link there or delete it.
|
||||
|
||||

|
||||
119
content/articles/2025-04-09-ServerManagementConsole.md
Normal file
119
content/articles/2025-04-09-ServerManagementConsole.md
Normal file
@@ -0,0 +1,119 @@
|
||||
---
|
||||
date: 2025-04-09
|
||||
tags: infrastructure, servers, homelab
|
||||
primaryTag: infrastructure
|
||||
---
|
||||
|
||||
# Server Management Console
|
||||
|
||||
This article I'll describe some steps to manage and / or trouble shoot the
|
||||
servers.
|
||||
|
||||
## Management Console
|
||||
|
||||
The servers have a management console that is accessible from the internal
|
||||
network. You will need to get the login name and password from Michael.
|
||||
|
||||
| Server | Link |
|
||||
| ------------ | ---------------------------------------------------------------------- |
|
||||
| mighty-mini | [console.mightymini.housh.dev](https://console.mightymini.housh.dev) |
|
||||
| franken-mini | [console.frankenmini.housh.dev](https://console.frankenmini.housh.dev) |
|
||||
| rogue-mini | [console.roguemini.housh.dev](https://console.roguemini.housh.dev) |
|
||||
|
||||
The management console allows you to update the server, check logs, and access a
|
||||
terminal on the machine. If you are updating the server via the management
|
||||
console, it is often required to reboot the server. All of the services are
|
||||
setup to restart upon a reboot of the server, so that should not cause problems,
|
||||
but you will be disconnected from the management console when the server shuts
|
||||
down. It does take a few minutes generally for the servers to go through the
|
||||
full boot process.
|
||||
|
||||
> Note: If something is not running the easiest thing to do would be to just
|
||||
> reboot the servers and the services should restart.
|
||||
|
||||
[You can view the server and services status here.](https://uptime.housh.dev/status/housh-dev)
|
||||
|
||||
## Reboot the server
|
||||
|
||||
You can reboot the server from the management console in the `Overview` section
|
||||
or by typing the following command in the terminal.
|
||||
|
||||
```bash
|
||||
sudo reboot --now
|
||||
```
|
||||
|
||||
## Useful Tips
|
||||
|
||||
There are several commands that may help trouble shoot the services on the
|
||||
server. For these you will need to make sure to turn on administrative access by
|
||||
clicking the button, if needed.
|
||||
|
||||

|
||||
|
||||
All of the following commands can be entered into the `Terminal` section of the
|
||||
console.
|
||||
|
||||
### Check the services are running
|
||||
|
||||
```bash
|
||||
sudo docker ps --all
|
||||
```
|
||||
|
||||
If working on a small screen or the output is bunched up then you can use the
|
||||
following command to only reveal a smaller portion of the output.
|
||||
|
||||
```bash
|
||||
sudo docker ps --format 'table {{.Names}}\t{{.Status}}'
|
||||
```
|
||||
|
||||

|
||||
|
||||
Here you would look for services where the **_STATUS_** says `Exited` or if any
|
||||
of the services say `unhealthy`.
|
||||
|
||||
### Service locations
|
||||
|
||||
The services are primary located in `/etc/komodo/stacks` or `~/containers`
|
||||
directories. You can list the contents of those directories using the following
|
||||
command.
|
||||
|
||||
```bash
|
||||
ls -lah ~/containers
|
||||
```
|
||||
|
||||
```bash
|
||||
ls -lah /etc/komodo/stacks
|
||||
```
|
||||
|
||||
### Starting services from the terminal
|
||||
|
||||
If you would like to ensure a service is up and running from the terminal move
|
||||
into the directory of the service.
|
||||
|
||||
```bash
|
||||
cd ~/containers/purchase-orders
|
||||
```
|
||||
|
||||
And issue the following command
|
||||
|
||||
```bash
|
||||
sudo docker compose up -d
|
||||
```
|
||||
|
||||
### Check the logs of a running container
|
||||
|
||||
You can check the logs of a container in several different ways. The easiest is
|
||||
if you know the containers name.
|
||||
|
||||
```bash
|
||||
sudo docker logs -f purchase_orders
|
||||
```
|
||||
|
||||
Or if you know the directory you can move into the directory using the `cd`
|
||||
command and use the following.
|
||||
|
||||
```bash
|
||||
sudo docker compose logs -f
|
||||
```
|
||||
|
||||
To stop viewing the logs hit `Ctrl-c`.
|
||||
71
content/articles/2025-04-23-NonInvasiveTesting.md
Normal file
71
content/articles/2025-04-23-NonInvasiveTesting.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
date: 2025-04-23
|
||||
tags: service, procedure, measureQuick
|
||||
---
|
||||
|
||||
# Non-Invasive Testing
|
||||
|
||||
In this article we will walk through when and when not to use non-invasive
|
||||
testing in measureQuick.
|
||||
|
||||
## The Challenge
|
||||
|
||||
Non-invasive testing is an awesome feature of measureQuick and something that
|
||||
was extremely challenging prior to tools like measureQuick because a service
|
||||
technician needed to know how to do all the math and know what the targets were.
|
||||
|
||||
However, for non-invasive testing to work properly in measureQuick, the system
|
||||
needs to be benchmarked first using an invasive test to set the baseline of the
|
||||
system. This is because measureQuick will compare the current conditions to the
|
||||
benchmarked conditions.
|
||||
|
||||
When running a non-invasive test on a system that has **NOT** been benchmarked
|
||||
then a "score" will not be generated and the reports look incomplete, such as
|
||||
the below image.
|
||||
|
||||

|
||||
|
||||
## Our Responsibility
|
||||
|
||||
It is our responsibility and goal during maintenance visits to truly assess the
|
||||
system operation and performance, so that we can catch premature failures before
|
||||
they occur as well as offer options to improve the system performance.
|
||||
|
||||
Tools like measureQuick and Bluetooth probes make this possible, but only when
|
||||
the tools are used properly.
|
||||
|
||||
Flags and errors need to be individually assessed and not be glossed over. Any
|
||||
flags that can be resolved with normal maintenance should be addressed at the
|
||||
time of the service. A solution / reason should be documented as to why it
|
||||
couldn't be, what is causing it, and options should be offered to resolve if the
|
||||
customer would like to do so.
|
||||
|
||||
## Non-Benchmarked Systems
|
||||
|
||||
Non-benchmarked systems are indicated by a red thumbprint on the profile button,
|
||||
and will say "Not Benchmarked" when clicking into the profile. If a system is
|
||||
not benchmarked, then an invasive test should be performed, all errors should be
|
||||
resolved (or as many as possible), then the system should be benchmarked so that
|
||||
non-invasive tests can be performed in the future.
|
||||
|
||||

|
||||

|
||||
|
||||
## Invasive Testing
|
||||
|
||||
A non-invasive test is best practice, however it does not mean that it is the
|
||||
only thing that should be used. If a non-invasive test indicates there may be a
|
||||
charge problem, then it should be transitioned into an invasive test. In other
|
||||
words, a non-invasive test should be used to know if you need to do an invasive
|
||||
test or not.
|
||||
|
||||
Invasive tests are less problematic than they were when technicians used
|
||||
manifold gauges with long hoses that could be contaminated. With the use of
|
||||
probes and no hoses, these concerns are much less.
|
||||
|
||||
## Conclusion
|
||||
|
||||
In conclusion, do not use non-invasive testing on systems that are not
|
||||
benchmarked. Do use non-invasive testing on systems that are benchmarked. Do not
|
||||
think that a non-invasive test is the only way to do it. When in doubt fall back
|
||||
to an invasive test.
|
||||
@@ -4,4 +4,7 @@ section: home
|
||||
|
||||
# Home
|
||||
|
||||
## Welcome to our internal documentation site
|
||||
## Documentation Site
|
||||
|
||||
Click on one of the links below or search for an article using the search
|
||||
feature.
|
||||
|
||||
BIN
content/static/img/linkshare.identity.png
LFS
Normal file
BIN
content/static/img/linkshare.identity.png
LFS
Normal file
Binary file not shown.
BIN
content/static/img/linkshare.manage.png
LFS
Normal file
BIN
content/static/img/linkshare.manage.png
LFS
Normal file
Binary file not shown.
BIN
content/static/img/linkshare.share.settings.png
LFS
Normal file
BIN
content/static/img/linkshare.share.settings.png
LFS
Normal file
Binary file not shown.
BIN
content/static/img/nonInvasive.1.png
LFS
Normal file
BIN
content/static/img/nonInvasive.1.png
LFS
Normal file
Binary file not shown.
BIN
content/static/img/nonInvasive.2.png
LFS
Normal file
BIN
content/static/img/nonInvasive.2.png
LFS
Normal file
Binary file not shown.
BIN
content/static/img/nonInvasive.3.png
LFS
Normal file
BIN
content/static/img/nonInvasive.3.png
LFS
Normal file
Binary file not shown.
BIN
content/static/img/phones.businesshours.png
LFS
Normal file
BIN
content/static/img/phones.businesshours.png
LFS
Normal file
Binary file not shown.
BIN
content/static/img/phones.engagement.png
LFS
Normal file
BIN
content/static/img/phones.engagement.png
LFS
Normal file
Binary file not shown.
BIN
content/static/img/servermanagement.console.png
LFS
Normal file
BIN
content/static/img/servermanagement.console.png
LFS
Normal file
Binary file not shown.
BIN
content/static/img/servermanagement.dockerps.png
LFS
Normal file
BIN
content/static/img/servermanagement.dockerps.png
LFS
Normal file
Binary file not shown.
BIN
content/static/img/timemachine.png
LFS
Normal file
BIN
content/static/img/timemachine.png
LFS
Normal file
Binary file not shown.
@@ -29,8 +29,9 @@
|
||||
--color: #fff;
|
||||
--border-color: hsla(0, 0%, 100%, 0.1);
|
||||
--phoneWidth: (max-width: 684px);
|
||||
--tabletWidth: (max-width: 900px) --orange: #f5a87f;
|
||||
--green: #a6e3a1;
|
||||
--tabletWidth: (max-width: 900px);
|
||||
--orange: #f5a87f;
|
||||
--green: #7bf2a7;
|
||||
}
|
||||
|
||||
/* Reset */
|
||||
@@ -60,8 +61,8 @@
|
||||
content: "";
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
#ffa86a,
|
||||
#ffa86a 2px,
|
||||
#7bf2a7,
|
||||
#7bf2a7 2px,
|
||||
transparent 0,
|
||||
transparent 10px
|
||||
);
|
||||
@@ -115,19 +116,31 @@ body {
|
||||
@apply bg-slate-900 font-avenir text-xl;
|
||||
}
|
||||
|
||||
/* Apply border to sidebar links, when page is active. */
|
||||
a.active,
|
||||
div.active {
|
||||
@apply border-b-2 border-orange-400;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-6xl pb-2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-5xl mb-8 pt-4;
|
||||
color: var(--green);
|
||||
}
|
||||
h3 {
|
||||
@apply text-2xl text-amber-500 py-4;
|
||||
@apply text-3xl text-violet-500 font-extrabold py-4;
|
||||
}
|
||||
h4 {
|
||||
@apply text-2xl text-sky-400 py-4;
|
||||
}
|
||||
|
||||
p {
|
||||
section h3 {
|
||||
@apply text-orange-400;
|
||||
}
|
||||
|
||||
article p {
|
||||
@apply mb-8;
|
||||
}
|
||||
|
||||
@@ -144,7 +157,15 @@ article a:hover {
|
||||
}
|
||||
|
||||
article code {
|
||||
@apply bg-amber-700;
|
||||
@apply text-white bg-violet-600 px-2;
|
||||
}
|
||||
|
||||
article ol {
|
||||
@apply list-decimal;
|
||||
}
|
||||
|
||||
table {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
table,
|
||||
@@ -155,15 +176,15 @@ td {
|
||||
}
|
||||
|
||||
table {
|
||||
@apply mb-8;
|
||||
@apply py-8 mb-6;
|
||||
}
|
||||
|
||||
table td {
|
||||
@apply px-6;
|
||||
@apply px-6 py-2;
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply px-10;
|
||||
@apply py-20;
|
||||
}
|
||||
|
||||
.container img {
|
||||
@@ -172,9 +193,21 @@ table td {
|
||||
}
|
||||
|
||||
blockquote {
|
||||
@apply border-2 border-blue-600 bg-blue-300 rounded-lg;
|
||||
@apply border-2 border-blue-600 bg-blue-300 rounded-lg my-10;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
@apply px-6 pt-6 text-blue-600 font-semibold;
|
||||
}
|
||||
|
||||
pre {
|
||||
@apply mb-6;
|
||||
}
|
||||
|
||||
#close {
|
||||
@apply text-slate-300;
|
||||
}
|
||||
|
||||
#close:hover {
|
||||
@apply text-slate-400;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,32 @@
|
||||
:root {
|
||||
--pagefind-ui-background: #0e172b;
|
||||
--pagefind-ui-text: white;
|
||||
/* --pagefind-ui-tag: #fd9a00; */
|
||||
/* --pagefind-ui-primary: #fd9a00; */
|
||||
--pagefind-ui-border: #fd9a00;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Helvetica;
|
||||
font-family: Avenir;
|
||||
}
|
||||
|
||||
#search {
|
||||
position: relative;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#search input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#search .pagefind-ui__drawer {
|
||||
background: #3c3c3c;
|
||||
border: 2px solid #fd9a00;
|
||||
}
|
||||
|
||||
#search .pagefind-ui__message {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
PORT=8081
|
||||
OAUTH_CLIENT_ID="<id>"
|
||||
OAUTH_CLIENT_SECRET="<secret>"
|
||||
|
||||
7
justfile
7
justfile
@@ -10,7 +10,12 @@ default:
|
||||
# Run the development server.
|
||||
[group('dev')]
|
||||
run:
|
||||
@swift run watch content Sources deploy
|
||||
#!/usr/bin/env zsh
|
||||
touch .build/browser-dev-sync
|
||||
browser-sync start -p localhost:1414 --watch --files '.build/browser-dev-sync' &
|
||||
watchexec -w Sources -e .swift -r 'swift run && touch .build/browser-dev-sync' &
|
||||
watchexec -w content -e .md -r 'swift run && touch .build/browser-dev-sync' &
|
||||
watchexec -w .build/browser-dev-sync --ignore-nothing -r 'npx -y pagefind --site deploy --serve'
|
||||
|
||||
# Create a new article with given name and tags.
|
||||
new-article name *tags:
|
||||
|
||||
27
oauth2-proxy/oauth2-proxy.cfg
Normal file
27
oauth2-proxy/oauth2-proxy.cfg
Normal file
@@ -0,0 +1,27 @@
|
||||
# Replace with your own credentials
|
||||
client_id="54ac14e4-4e6b-46ce-a870-01b297421e89"
|
||||
client_secret="W8r4ozypT4Qx23P0wa9pGHQAyUtmYOW8"
|
||||
oidc_issuer_url="https://id.housh.dev"
|
||||
|
||||
# Replace with a secure random string
|
||||
cookie_secret="lGaySNwq1tNKd1pcji0IQrz7tPYbt2P8"
|
||||
|
||||
# Upstream servers (e.g http://uptime-kuma:3001)
|
||||
upstreams="http://docs:80"
|
||||
|
||||
# Additional Configuration
|
||||
provider="oidc"
|
||||
scope = "openid email profile groups"
|
||||
|
||||
# If you are using a reverse proxy in front of OAuth2 Proxy
|
||||
reverse_proxy=false
|
||||
|
||||
# Email domains allowed for authentication
|
||||
email_domains="*"
|
||||
insecure_oidc_allow_unverified_email="true"
|
||||
|
||||
# If you are using HTTPS
|
||||
cookie_secure="false"
|
||||
|
||||
# Listen on all interfaces
|
||||
http_address="0.0.0.0:4180"
|
||||
@@ -16,6 +16,7 @@
|
||||
"tailwindcss": "^4.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/cli": "^4.0.8"
|
||||
"@tailwindcss/cli": "^4.0.8",
|
||||
"pagefind": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
55
pnpm-lock.yaml
generated
55
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
'@tailwindcss/cli':
|
||||
specifier: ^4.0.8
|
||||
version: 4.1.1
|
||||
pagefind:
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
devDependencies:
|
||||
tailwindcss:
|
||||
specifier: ^4.0.8
|
||||
@@ -18,6 +21,31 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@pagefind/darwin-arm64@1.3.0':
|
||||
resolution: {integrity: sha512-365BEGl6ChOsauRjyVpBjXybflXAOvoMROw3TucAROHIcdBvXk9/2AmEvGFU0r75+vdQI4LJdJdpH4Y6Yqaj4A==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pagefind/darwin-x64@1.3.0':
|
||||
resolution: {integrity: sha512-zlGHA23uuXmS8z3XxEGmbHpWDxXfPZ47QS06tGUq0HDcZjXjXHeLG+cboOy828QIV5FXsm9MjfkP5e4ZNbOkow==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@pagefind/linux-arm64@1.3.0':
|
||||
resolution: {integrity: sha512-8lsxNAiBRUk72JvetSBXs4WRpYrQrVJXjlRRnOL6UCdBN9Nlsz0t7hWstRk36+JqHpGWOKYiuHLzGYqYAqoOnQ==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@pagefind/linux-x64@1.3.0':
|
||||
resolution: {integrity: sha512-hAvqdPJv7A20Ucb6FQGE6jhjqy+vZ6pf+s2tFMNtMBG+fzcdc91uTw7aP/1Vo5plD0dAOHwdxfkyw0ugal4kcQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@pagefind/windows-x64@1.3.0':
|
||||
resolution: {integrity: sha512-BR1bIRWOMqkf8IoU576YDhij1Wd/Zf2kX/kCI0b2qzCKC8wcc2GQJaaRMCpzvCCrmliO4vtJ6RITp/AnoYUUmQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
@@ -292,6 +320,10 @@ packages:
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
pagefind@1.3.0:
|
||||
resolution: {integrity: sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw==}
|
||||
hasBin: true
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
|
||||
@@ -312,6 +344,21 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pagefind/darwin-arm64@1.3.0':
|
||||
optional: true
|
||||
|
||||
'@pagefind/darwin-x64@1.3.0':
|
||||
optional: true
|
||||
|
||||
'@pagefind/linux-arm64@1.3.0':
|
||||
optional: true
|
||||
|
||||
'@pagefind/linux-x64@1.3.0':
|
||||
optional: true
|
||||
|
||||
'@pagefind/windows-x64@1.3.0':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
optional: true
|
||||
|
||||
@@ -519,6 +566,14 @@ snapshots:
|
||||
|
||||
node-addon-api@7.1.1: {}
|
||||
|
||||
pagefind@1.3.0:
|
||||
optionalDependencies:
|
||||
'@pagefind/darwin-arm64': 1.3.0
|
||||
'@pagefind/darwin-x64': 1.3.0
|
||||
'@pagefind/linux-arm64': 1.3.0
|
||||
'@pagefind/linux-x64': 1.3.0
|
||||
'@pagefind/windows-x64': 1.3.0
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
Reference in New Issue
Block a user