Compare commits

...

36 Commits

Author SHA1 Message Date
e1bf53bd70 feat: Adds non-invasive testing article.
All checks were successful
CI / release (push) Successful in 7m7s
2025-04-23 09:49:15 -04:00
94bcfe915d feat: Updates plausible script to use housh.dev domain.
All checks were successful
CI / release (push) Successful in 5m46s
2025-04-16 13:06:28 -04:00
ec771edc01 feat: Updates plausible script to include outbound links.
All checks were successful
CI / release (push) Successful in 5m46s
2025-04-16 11:41:14 -04:00
8a9a361a4b feat: Adds plausible analytics.
All checks were successful
CI / release (push) Successful in 5m58s
2025-04-16 11:22:34 -04:00
c0a8e3ced8 feat: Adds more navigation items to sidebar and removes top navigation bar.
All checks were successful
CI / release (push) Successful in 6m53s
2025-04-16 09:16:24 -04:00
e9c1dfa2e5 fix: Fixes extra trailing comma in one of the functions, which prevented builds.
All checks were successful
CI / release (push) Successful in 5m36s
2025-04-15 15:06:35 -04:00
ad81392dc7 feat: Updates styles for sidebar and articles list view.
Some checks failed
CI / release (push) Failing after 2m31s
2025-04-15 15:00:28 -04:00
500f4746e8 feat: Adds sidebar to articles view.
Some checks failed
CI / release (push) Failing after 2m34s
2025-04-15 14:41:10 -04:00
9159ecc834 feat: Unifies article lists to have similar view styles.
All checks were successful
CI / release (push) Successful in 5m36s
2025-04-15 11:49:33 -04:00
def75c1e41 feat: Adds a tag grid at top of articles view.
All checks were successful
CI / release (push) Successful in 6m34s
2025-04-15 10:17:04 -04:00
88c6bd4891 feat: Style updates to articles list view.
All checks were successful
CI / release (push) Successful in 7m38s
2025-04-14 16:50:47 -04:00
a26e239291 fix: Fixes Caddyfile for pocket-id to work correctly in production.
All checks were successful
CI / release (push) Successful in 5m32s
2025-04-11 12:43:16 -04:00
590a3d360f feat: Try to reach pocket-id via container name.
All checks were successful
CI / release (push) Successful in 5m36s
2025-04-11 11:52:03 -04:00
0f709b0a98 feat: Adds pocket id authentication to caddy.
All checks were successful
CI / release (push) Successful in 5m32s
2025-04-11 11:05:40 -04:00
bc87cef815 feat: Working oauth2 proxy, on localhost.
All checks were successful
CI / release (push) Successful in 5m31s
2025-04-11 10:32:08 -04:00
f294a065e2 feat: Removes caddy security from this image and to primary proxy.
All checks were successful
CI / release (push) Successful in 5m38s
2025-04-11 08:58:18 -04:00
5ce67a697b feat: Removes caddy security from this image and to primary proxy. 2025-04-11 08:57:05 -04:00
1878032ec4 fix: Fixes host in Caddyfile.
All checks were successful
CI / release (push) Successful in 5m31s
2025-04-11 08:38:20 -04:00
b986fe41c3 feat: Adds pocket id authentication to caddy, adds server management article.
All checks were successful
CI / release (push) Successful in 6m31s
2025-04-11 08:26:51 -04:00
f43a191908 feat: Renames servers article to servers overview.
All checks were successful
CI / release (push) Successful in 5m45s
2025-04-08 15:04:13 -04:00
a53e808aec feat: Updates to network article.
All checks were successful
CI / release (push) Successful in 6m9s
2025-04-08 13:36:39 -04:00
d0383b0d4e feat: Adds legacy po link to home page.
All checks were successful
CI / release (push) Successful in 6m6s
2025-04-08 11:18:23 -04:00
da27216fc1 feat: Adds link sharing document.
All checks were successful
CI / release (push) Successful in 6m5s
2025-04-08 10:37:53 -04:00
f7d0018314 feat: Adds hover effect to links on home page.
All checks were successful
CI / release (push) Successful in 6m7s
2025-04-08 09:56:03 -04:00
f05b96e0bf feat: Adds dashboard style links to home page.
Some checks failed
CI / release (push) Has been cancelled
2025-04-08 09:52:54 -04:00
522fac7b01 feat: Changes to css.
All checks were successful
CI / release (push) Successful in 7m6s
2025-04-07 17:33:02 -04:00
9730c5b129 feat: Updates README.
All checks were successful
CI / release (push) Successful in 6m5s
2025-04-07 14:30:06 -04:00
8cda888a87 feat: Updates to phone system article, updates stylesheets.
All checks were successful
CI / release (push) Successful in 6m3s
2025-04-07 11:44:37 -04:00
cdd1dca030 feat: Adds phone system article.
All checks were successful
CI / release (push) Successful in 6m5s
2025-04-07 11:07:03 -04:00
1a88883bad feat: Adds time-machine setup article.
All checks were successful
CI / release (push) Successful in 6m2s
2025-04-07 09:29:40 -04:00
2fa26ef552 feat: Removes unused about section.
All checks were successful
CI / release (push) Successful in 7m6s
2025-04-07 08:47:36 -04:00
9d380ad300 feat: Updates dockerfile for pagefind build.
All checks were successful
CI / release (push) Successful in 6m9s
2025-04-06 16:19:02 -04:00
1b29e8d833 feat: Updates for search integration.
Some checks failed
CI / release (push) Failing after 2m32s
2025-04-06 15:28:10 -04:00
b3a2400bc2 feat: Initial search implementation using pagefind.
Some checks failed
CI / release (push) Failing after 13m42s
2025-04-06 10:46:23 -04:00
573e70a8d2 feat: Initial search implementation using pagefind. 2025-04-06 10:40:58 -04:00
6457674de7 feat: Working on rendering a search index / json file. 2025-04-06 00:15:04 -04:00
45 changed files with 1224 additions and 194 deletions

2
.gitignore vendored
View File

@@ -9,7 +9,7 @@ public/*
.hugo_build.lock .hugo_build.lock
deploy deploy
node_modules node_modules
env .env
Package.resolved Package.resolved
# Local Netlify folder # Local Netlify folder

52
Caddyfile Normal file
View 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
}
}

View File

@@ -30,19 +30,26 @@ WORKDIR /build
RUN npm install -g pnpm@latest-10 RUN npm install -g pnpm@latest-10
COPY . . COPY . .
COPY --from=build /build/deploy ./deploy
RUN pnpm install && pnpm run css-build RUN pnpm install && pnpm run css-build
RUN npx -y pagefind --site deploy
# ================================================== # ==================================================
# Run Image # Run Image
# ================================================== # ==================================================
FROM caddy:2.9.1-alpine #FROM caddy:2.9.1-alpine
FROM ghcr.io/authcrunch/authcrunch:latest
WORKDIR /app 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/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 EXPOSE 80
CMD ["/usr/bin/caddy", "file-server", "--root", "/app", "--listen", ":80"] CMD ["/usr/bin/caddy", "run", "--config", "/etc/caddy/Caddyfile"]

View File

@@ -1,32 +1,10 @@
---
date: 2025-4-02
updated: 2025-04-02
author: "Michael Housh"
---
# Homelab Documentation # 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 1. Install dependencies `pnpm install`
servers, which run them. The containers are deployed using a container 1. Run & watch for css changes, this requires two terminal processes.
orchestrator, currently using [komo](https://komo.housh.dev). 1. `pnpm run css-watch`
1. `just run`
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.

View File

@@ -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 // NOTE: Most of these are taken from https://github.com/loopwerk/loopwerk.io
extension String { extension String {

View File

@@ -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, // 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. // are copied as-is to the output folder.
.staticFiles() .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()
} }
} }

View File

@@ -3,7 +3,6 @@
/// This is used to render base layouts appropriately for the given section. /// This is used to render base layouts appropriately for the given section.
enum Section: String { enum Section: String {
case home case home
case about
case articles case articles
case notFound case notFound
} }

View 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)
}
}
}
}
}
}
}
}

View File

@@ -20,15 +20,25 @@ func baseLayout(
return [ return [
.documentType("html"), .documentType("html"),
html(lang: "en-US") { html(lang: "en-US") {
generateHeader(pageTitle, extraHeader) generateHead(pageTitle, extraHeader)
body(class: "text-white text-lg pb-5 font-avenir \(section.rawValue)") { body(class: "text-white text-lg font-avenir \(section.rawValue)") {
siteHeader(section) siteHeader(section)
div(class: "container") { // mx-10
div(class: "mb-auto") {
children() children()
} }
if section == .articles {
footer(rssLink) 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") { // TODO: Explore search being hidden / triggered by a button and hover above
li { // the page content.
a(class: section == .articles ? "active" : "", href: "/articles/") { "Articles" } 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")
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" }
}
}
}
} }
} }
private func footer(_ rssLink: String) -> Node { 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 { div {
"Copyright © Michael Housh \(Date().description.prefix(4))." "Copyright © Michael Housh \(Date().description.prefix(4))."
} }
@@ -78,19 +80,16 @@ private func footer(_ rssLink: String) -> Node {
"(" "("
%a( %a(
class: "[&:hover]:border-b border-green-400", class: "[&:hover]:border-b border-green-400",
href: "https://github.com/m-housh/mhoush.com", href: "https://git.housh.dev/homelab/docs",
rel: "nofollow", rel: "nofollow",
target: "_blank" target: "_blank"
) { "source" } ) { "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 { head {
meta(charset: "utf-8") meta(charset: "utf-8")
meta(content: "#0e1112", name: "theme-color", customAttributes: ["media": "(prefers-color-scheme: dark)"]) 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-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/static/ms-icon-144x144.png"> <meta name="msapplication-TileImage" content="/static/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff"> <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/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") link(href: "/articles/feed.xml", rel: "alternate", title: SiteMetadata.name, type: "application/rss+xml")
extraHeader 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")
} }
} }

View File

@@ -40,19 +40,7 @@ func generateHeader(
meta(content: "1014", name: "og:image:width"), meta(content: "1014", name: "og:image:width"),
meta(content: "530", name: "og:image:height"), meta(content: "530", name: "og:image:height"),
script(crossorigin: "anonymous", src: "https://kit.fontawesome.com/f209982030.js"), script(crossorigin: "anonymous", src: "https://kit.fontawesome.com/f209982030.js"),
Node.raw(""" script(src: "https://cdn.jsdelivr.net/npm/minisearch@7.1.2/dist/umd/index.min.js")
<script>
MathJax = {
tex: {
inlineMath: [['$', '$']]
},
svg: {
fontCache: 'global'
}
};
</script>
"""),
script(defer: true, src: "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js")
]) ])
} }
} }

View 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 }
}
}
}

View File

@@ -13,14 +13,19 @@ func tagPrefix(index: Int, totalTags: Int) -> Node {
return "" return ""
} }
func renderArticleInfo(_ article: Item<ArticleMetadata>) -> Node { enum RenderArticleInfoContext {
div(class: "text-slate-400 gray-links text-sm mb-8") { 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") { span(class: "border-r border-gray pr-2 mr-2") {
article.getDate().formatted("MMMM dd, yyyy") article.getDate().formatted("MMMM dd, yyyy")
} }
if let updated = article.getUpdatedDate() { 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"))" "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 article.metadata.tags.sorted().enumerated().map { index, tag in
Node.fragment([ Node.fragment([
%tagPrefix(index: index, totalTags: article.metadata.tags.count), %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)/") { %a(class: "text-orange-400 [&:hover]:border-b border-green-400", href: "/articles/tag/\(tag.slugified)/") {
tag tag
} }
@@ -100,22 +102,43 @@ func renderArticle(context: ItemRenderingContext<ArticleMetadata>) -> Node {
title: context.item.title, title: context.item.title,
extraHeader: generateHeader(.article(context.item)) extraHeader: generateHeader(.article(context.item))
) { ) {
article(class: "pt-8") { article(class: "pt-8 mx-10") {
h1 { context.item.title } div(class: "relative") {
div { a(class: "absolute top-4 right-4", href: "/articles", id: "close") {
renderArticleInfo(context.item) 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") { div(class: "grid lg:grid-cols-2") {
h4(class: "text-3xl text-amber-500 font-extrabold mb-8") { otherArticles.title } h4(class: "text-3xl text-amber-500 font-extrabold mb-8") { otherArticles.title }
if let tag = otherArticles.tag { if let tag = otherArticles.tag {
a(href: "/articles/tag/\(tag)") { a(href: "/articles/tag/\(tag)") {
div(class: " [&:hover]:border-b border-orange px-5 flex flex-row gap-5") { div(class: " [&:hover]:border-b border-green-500 px-5 flex flex-row gap-5") {
img(src: "/static/img/tag.svg", width: "40") img(class: "-mt-2", src: "/static/img/tag.svg", width: "40")
span(class: "text-4xl font-extrabold text-orange") { tag } 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 { func renderArticleForGrid(article: Item<ArticleMetadata>, border: Bool = true) -> Node {
section { // bg-slate-800
h3(class: "post-title text-2xl font-bold mb-2") { div(class: "p-4\(border ? " border border-slate-400 rounded-lg" : "")") {
a(class: "[&:hover]:border-b border-orange-400", href: article.url) { article.title } section {
} h3(class: "text-2xl font-bold") {
renderArticleInfo(article) a(class: "[&:hover]:border-b border-green-500", href: article.url) { article.title }
p { }
a(href: article.url) { renderArticleInfo(article, context: .preview)
div { p {
article.summary a(href: article.url) {
div {
article.summary
}
} }
} }
} }

View File

@@ -2,37 +2,24 @@ import Foundation
import HTML import HTML
import Saga import Saga
func uniqueTagsWithCount(_ articles: [Item<ArticleMetadata>]) -> [(String, Int)] { func renderArticles(context: ItemsRenderingContext<ArticleMetadata>) -> NodeConvertible {
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 {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy" dateFormatter.dateFormat = "yyyy"
let articlesPerYear = Dictionary(grouping: context.items, by: { dateFormatter.string(from: $0.date) }) let articlesPerYear = Dictionary(grouping: context.items, by: { dateFormatter.string(from: $0.date) })
let sortedByYearDescending = articlesPerYear.sorted { $0.key > $1.key } let sortedByYearDescending = articlesPerYear.sorted { $0.key > $1.key }
return baseLayout(canocicalURL: "/articles/", section: .articles, title: "Articles", rssLink: "", extraHeader: "") { return baseRenderArticles(
// TODO: Add list of tags here that can be navigated to. sortedByYearDescending,
sortedByYearDescending.map { year, articles in canocicalURL: "/articles/",
div { title: "Articles",
div(class: "border-b border-light flex flex-row gap-4 mb-12") { rssLink: "",
img(src: "/static/img/calendar.svg", width: "40") extraHeader: "",
h1(class: "text-4xl font-extrabold pt-3") { year } label: yearHeader(_:)
} )
div(class: "grid gap-10 mb-16") {
articles.map { renderArticleForGrid(article: $0) }
}
}
}
}
} }
func renderTag<T>(context: PartitionedRenderingContext<T, ArticleMetadata>) -> Node { func renderTag<T>(context: PartitionedRenderingContext<T, ArticleMetadata>) -> NodeConvertible {
let extraHeader = link( let extraHeader = link(
href: "/articles/tag/\(context.key.slugified)/feed.xml", href: "/articles/tag/\(context.key.slugified)/feed.xml",
rel: "alternate", rel: "alternate",
@@ -41,54 +28,86 @@ func renderTag<T>(context: PartitionedRenderingContext<T, ArticleMetadata>) -> N
) )
return baseRenderArticles( return baseRenderArticles(
context.items, (context.key.slugified, context.items),
canocicalURL: "/articles/tag/\(context.key.slugified)/", canocicalURL: "/articles/tag/\(context.key.slugified)/",
title: "Articles in \(context.key)", title: "Articles in \(context.key)",
rssLink: "tag/\(context.key.slugified)/", rssLink: "tag/\(context.key.slugified)/",
extraHeader: extraHeader extraHeader: extraHeader
) { ) { tag in
div(class: "border-b border-light grid lg:grid-cols-2 mb-12") { div(class: "mb-12 px-6 pt-6") {
a(href: "/articles") { 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") img(src: "/static/img/tag.svg", width: "40")
h1(class: "text-4xl font-extrabold") { "\(context.key)" } h1(class: "text-4xl font-extrabold") { tag }
h1(class: "[&:hover]:border-b border-green text-2xl font-extrabold text-orange px-4") { "«" } h1(class: "text-2xl font-extrabold") { "«" }
} }
} }
} }
} }
} }
func renderYear<T>(context: PartitionedRenderingContext<T, ArticleMetadata>) -> Node { func renderYear<T>(context: PartitionedRenderingContext<T, ArticleMetadata>) -> NodeConvertible {
baseRenderArticles(context.items, canocicalURL: "/articles/\(context.key)/", title: "Articles in \(context.key)") 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( private func baseRenderArticles(
_ articles: [Item<ArticleMetadata>], _ articles: [(key: String, value: [Item<ArticleMetadata>])],
canocicalURL: String, canocicalURL: String,
title pageTitle: String, title pageTitle: String,
rssLink: String = "", rssLink: String = "",
extraHeader: NodeConvertible = Node.fragment([]), extraHeader: NodeConvertible = Node.fragment([]),
@NodeBuilder label: () -> Node = { Node.fragment([]) } @NodeBuilder label: @escaping (String) -> Node = { _ in Node.fragment([]) }
) -> Node { ) -> NodeConvertible {
return baseLayout( ArticleGrid(
articles: articles,
canocicalURL: canocicalURL, canocicalURL: canocicalURL,
section: .articles,
title: pageTitle, title: pageTitle,
rssLink: rssLink, rssLink: rssLink,
extraHeader: extraHeader extraHeader: extraHeader
) { ) { key in
label() label(key)
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 } private func baseRenderArticles(
} _ articles: (key: String, value: [Item<ArticleMetadata>]),
renderArticleInfo(article) canocicalURL: String,
p(class: "mt-4") { title pageTitle: String,
a(href: article.url) { article.summary } 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)
} }
} }

View File

@@ -27,9 +27,71 @@ func renderPage(context: ItemRenderingContext<PageMetadata>) -> Node {
func renderHome(body: String) -> Node { func renderHome(body: String) -> Node {
div { 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) 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."
)
}
}
}
} }
} }

View 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
View 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

View File

@@ -3,6 +3,7 @@ services:
image: git.housh.dev/homelab/docs:latest image: git.housh.dev/homelab/docs:latest
container_name: docs container_name: docs
restart: unless-stopped restart: unless-stopped
env_file: .env
ports: ports:
- ${PORT:-8081}:80 - ${PORT:-8081}:80
networks: networks:

View File

@@ -1,7 +0,0 @@
---
section: about
---
# About
Internal documentation site for **Housh - The Home Energy Experts**

View File

@@ -1,8 +1,9 @@
--- ---
date: 2025-4-02 date: 2025-4-02
updated: 2025-4-03 updated: 2025-4-08
author: "Michael Housh" author: "Michael Housh"
tags: network, infrastructure tags: network, infrastructure
primaryTag: infrastructure
--- ---
# Networking # Networking
@@ -10,9 +11,31 @@ tags: network, infrastructure
All of the networking setup is done through [unifi](https://unifi.ui.com). The All of the networking setup is done through [unifi](https://unifi.ui.com). The
network is segmented into several different networks to isolate communication. 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 ## 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 ### 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 it is plugged into an ethernet cable / switch. For this reason this network is
isolated from communicating with other networks. isolated from communicating with other networks.
New devices that end up on this network should be configured to the appropriate New devices that end up on this network should be configured / moved to the
network by a network administrator. appropriate network by a network administrator.
### Management Network ### Management Network
@@ -33,26 +56,32 @@ someone gained access to the network.
### Main Network ### Main Network
This is where the majority of "trusted" devices should be placed on the 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 such as computers, mobile phones, etc. This is also the network used when people
non-guest WiFi. 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 ### housh.dev Network
This is the network where all the servers are placed. This network is primarily This is the network where the majority of servers are placed. This network is
setup to allow "responses", but not initiate communication with other networks. primarily setup to allow "responses", but not allowed to initiate communication
This is to help reduce the risk if one of the servers gets compromised, an with other networks. This is to help reduce the risk if one of the servers gets
attacker should not easily be able to transition to another network. compromised, an attacker should not easily be able to transition to another
network.
### Phones Network ### Phones Network
This is the network where all the VoIP phones are on. It is considered 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. "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 ### 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 considered an "untrusted" network and communications with other networks are
minimized to what is actually needed to work. This network is not able to 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 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 devices operating properly when placed on the IoT network, such as airdrop and
screen casting (which may be resolved in the future). 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 ## Firewall
The unifi management console is what handles firewall rules for the networks. It 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 This is where settings are made to either allow or deny traffic on the networks
from communicating with other networks or the internet. 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
DNS is what translates IP addresses to domain names (i.e. DNS is what translates IP addresses to domain names (i.e.

View File

@@ -1,9 +1,11 @@
--- ---
date: 2025-04-04 date: 2025-04-04
updated: 2025-04-08
tags: servers, infrastructure, homelab tags: servers, infrastructure, homelab
primaryTag: infrastructure
--- ---
# Servers # Servers Overview
Documentation about how the servers are setup. 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 are running on servers that I own, while services that are supporting business
functionality run on the companies server. 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 There is also a `Raspberry-Pi` that runs `Home Assitant`, which is another one
of my personal devices. of my personal devices.
| Server | DNS Name | IP Address | | Server | DNS Name | IP Address |
| --------------------- | ---------------------- | ------------ | | -------------- | ---------------------- | -------------- |
| mighty-mini (company) | mightymini.housh.dev | 192.168.50.6 | | mighty-mini | mightymini.housh.dev | 192.168.50.6 |
| franken-mini (mine) | frankenmini.housh.dev | 192.168.50.5 | | franken-mini | frankenmini.housh.dev | 192.168.50.5 |
| rogue-mini (mine) | roguemini.housh.dev | 192.168.50.4 | | rogue-mini | roguemini.housh.dev | 192.168.50.4 |
| home-assistant (mine) | homeassitant.housh.dev | 192.168.30.5 | | 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 You can read more about the network setup
[here](https://docs.housh.dev/articles/2025/network/). [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 ## Containers
Services run inside of docker containers that are spread between several 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 configuration will consist of a docker compose file (generally named
`compose.yaml`). There is often an `example.env` file for the service, these are `compose.yaml`). There is often an `example.env` file for the service, these are
examples for documentation and variable naming purposes. The environment 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 ### 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 network then using our VPN is required. The VPN setup is done automatically via
unifi (our network router). 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 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` 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://`). 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`. 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 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 (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).

View 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.
![engagement](/static/img/phones.engagement.png)
#### 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.
![business-hours](/static/img/phones.businesshours.png)
### 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.

View 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.
![time-machine](/static/img/timemachine.png)

View 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.
![link-share](/static/img/linkshare.identity.png)
## 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`.
![settings](/static/img/linkshare.share.settings.png)
## 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.
![manage](/static/img/linkshare.manage.png)

View 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.
![console](/static/img/servermanagement.console.png)
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}}'
```
![output](/static/img/servermanagement.dockerps.png)
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`.

View 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.
![non-invasive](/static/img/nonInvasive.1.png)
## 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.
![profile-1](/static/img/nonInvasive.2.png)
![profile-2](/static/img/nonInvasive.3.png)
## 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.

View File

@@ -4,4 +4,7 @@ section: home
# 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -29,8 +29,9 @@
--color: #fff; --color: #fff;
--border-color: hsla(0, 0%, 100%, 0.1); --border-color: hsla(0, 0%, 100%, 0.1);
--phoneWidth: (max-width: 684px); --phoneWidth: (max-width: 684px);
--tabletWidth: (max-width: 900px) --orange: #f5a87f; --tabletWidth: (max-width: 900px);
--green: #a6e3a1; --orange: #f5a87f;
--green: #7bf2a7;
} }
/* Reset */ /* Reset */
@@ -60,8 +61,8 @@
content: ""; content: "";
background: repeating-linear-gradient( background: repeating-linear-gradient(
90deg, 90deg,
#ffa86a, #7bf2a7,
#ffa86a 2px, #7bf2a7 2px,
transparent 0, transparent 0,
transparent 10px transparent 10px
); );
@@ -115,19 +116,31 @@ body {
@apply bg-slate-900 font-avenir text-xl; @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 { h1 {
@apply text-6xl pb-2; @apply text-6xl pb-2;
} }
h2 { h2 {
@apply text-5xl mb-8 pt-4; @apply text-5xl mb-8 pt-4;
color: var(--green); color: var(--green);
} }
h3 { 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; @apply mb-8;
} }
@@ -144,7 +157,15 @@ article a:hover {
} }
article code { article code {
@apply bg-amber-700; @apply text-white bg-violet-600 px-2;
}
article ol {
@apply list-decimal;
}
table {
@apply w-full;
} }
table, table,
@@ -155,15 +176,15 @@ td {
} }
table { table {
@apply mb-8; @apply py-8 mb-6;
} }
table td { table td {
@apply px-6; @apply px-6 py-2;
} }
.container { .container {
@apply px-10; @apply py-20;
} }
.container img { .container img {
@@ -172,9 +193,21 @@ table td {
} }
blockquote { 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 { blockquote p {
@apply px-6 pt-6 text-blue-600 font-semibold; @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

View File

@@ -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 { 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;
} }
* { * {

View File

@@ -1 +1,3 @@
PORT=8081 PORT=8081
OAUTH_CLIENT_ID="<id>"
OAUTH_CLIENT_SECRET="<secret>"

View File

@@ -10,7 +10,12 @@ default:
# Run the development server. # Run the development server.
[group('dev')] [group('dev')]
run: 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. # Create a new article with given name and tags.
new-article name *tags: new-article name *tags:

View 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"

View File

@@ -16,6 +16,7 @@
"tailwindcss": "^4.0.8" "tailwindcss": "^4.0.8"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/cli": "^4.0.8" "@tailwindcss/cli": "^4.0.8",
"pagefind": "^1.3.0"
} }
} }

55
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@tailwindcss/cli': '@tailwindcss/cli':
specifier: ^4.0.8 specifier: ^4.0.8
version: 4.1.1 version: 4.1.1
pagefind:
specifier: ^1.3.0
version: 1.3.0
devDependencies: devDependencies:
tailwindcss: tailwindcss:
specifier: ^4.0.8 specifier: ^4.0.8
@@ -18,6 +21,31 @@ importers:
packages: 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': '@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
@@ -292,6 +320,10 @@ packages:
node-addon-api@7.1.1: node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
pagefind@1.3.0:
resolution: {integrity: sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw==}
hasBin: true
picocolors@1.1.1: picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -312,6 +344,21 @@ packages:
snapshots: 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': '@parcel/watcher-android-arm64@2.5.1':
optional: true optional: true
@@ -519,6 +566,14 @@ snapshots:
node-addon-api@7.1.1: {} 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: {} picocolors@1.1.1: {}
picomatch@2.3.1: {} picomatch@2.3.1: {}