This commit is contained in:
@@ -57,7 +57,7 @@ $Config = @{
|
|||||||
MaxDiscordEmbeds = 10
|
MaxDiscordEmbeds = 10
|
||||||
RetryCount = 3
|
RetryCount = 3
|
||||||
RetryDelaySeconds = 2
|
RetryDelaySeconds = 2
|
||||||
AuthoringTestsEnabled = $false
|
AuthoringTestsEnabled = $true
|
||||||
AuthoringTestsPattern = 'test_authoring_server.py'
|
AuthoringTestsPattern = 'test_authoring_server.py'
|
||||||
AuthoringTestsDirectory = Join-Path $PSScriptRoot 'tests'
|
AuthoringTestsDirectory = Join-Path $PSScriptRoot 'tests'
|
||||||
AuthoringTestTimeoutSec = 120
|
AuthoringTestTimeoutSec = 120
|
||||||
@@ -75,6 +75,7 @@ $Config = @{
|
|||||||
|
|
||||||
# When true, completed successful runs are reported too.
|
# When true, completed successful runs are reported too.
|
||||||
NotifySuccessfulBuilds = $true
|
NotifySuccessfulBuilds = $true
|
||||||
|
MaxPreviousFailures = 5
|
||||||
|
|
||||||
# When true, successful but slow/warned jobs are reported too.
|
# When true, successful but slow/warned jobs are reported too.
|
||||||
# Keep this false if Discord should only receive notifications for failed/cancelled/timed-out jobs.
|
# Keep this false if Discord should only receive notifications for failed/cancelled/timed-out jobs.
|
||||||
@@ -547,15 +548,35 @@ function Get-Severity {
|
|||||||
return 'Info'
|
return 'Info'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-BuildStatus {
|
||||||
|
param([Parameter(Mandatory)][object]$Build)
|
||||||
|
|
||||||
|
$conclusion = Get-ObjectValue -Object $Build -Name 'conclusion'
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$conclusion)) {
|
||||||
|
$conclusion = Get-ObjectValue -Object $Build -Name 'result'
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace([string]$conclusion)) {
|
||||||
|
$conclusion = Get-ObjectValue -Object $Build -Name 'outcome'
|
||||||
|
}
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$conclusion)) {
|
||||||
|
return [string]$conclusion
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = Get-ObjectValue -Object $Build -Name 'status'
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace([string]$status)) {
|
||||||
|
return [string]$status
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
function Analyze-Build {
|
function Analyze-Build {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][hashtable]$Repository,
|
[Parameter(Mandatory)][hashtable]$Repository,
|
||||||
[Parameter(Mandatory)][object]$Job
|
[Parameter(Mandatory)][object]$Job
|
||||||
)
|
)
|
||||||
|
|
||||||
$conclusion = Get-ObjectValue -Object $Job -Name 'conclusion'
|
$status = Get-BuildStatus -Build $Job
|
||||||
$jobStatus = Get-ObjectValue -Object $Job -Name 'status'
|
|
||||||
$status = if ($conclusion) { [string]$conclusion } elseif ($jobStatus) { [string]$jobStatus } else { 'unknown' }
|
|
||||||
$isSuccessfulStatus = Test-IsSuccessfulStatus -Status $status
|
$isSuccessfulStatus = Test-IsSuccessfulStatus -Status $status
|
||||||
$duration = Get-Duration -Job $Job
|
$duration = Get-Duration -Job $Job
|
||||||
$isSlow = $duration.TotalMinutes -ge [double]$Config.SlowBuildThresholdMins
|
$isSlow = $duration.TotalMinutes -ge [double]$Config.SlowBuildThresholdMins
|
||||||
@@ -728,6 +749,128 @@ function Send-DiscordNotification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-BuildStatusEmoji {
|
||||||
|
param([Parameter(Mandatory)][string]$Status)
|
||||||
|
|
||||||
|
if (Test-IsSuccessfulStatus -Status $Status) {
|
||||||
|
return '✅'
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = $Status.ToLowerInvariant()
|
||||||
|
if ($normalized -in @('failure', 'failed', 'cancelled', 'canceled', 'timed_out')) {
|
||||||
|
return '❌'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'ℹ️'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-BuildStatusNotification {
|
||||||
|
param([Parameter(Mandatory)][object[]]$Analyses)
|
||||||
|
|
||||||
|
$latestByRepository = @($Analyses |
|
||||||
|
Group-Object Repository |
|
||||||
|
ForEach-Object {
|
||||||
|
$_.Group | Sort-Object StartedAt -Descending | Select-Object -First 1
|
||||||
|
} |
|
||||||
|
Sort-Object Repository)
|
||||||
|
|
||||||
|
if ($latestByRepository.Count -eq 0) {
|
||||||
|
Write-Log -Message "No completed builds found in the last $($Config.LookbackMinutes) minute(s)."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$embeds = New-Object System.Collections.Generic.List[object]
|
||||||
|
$failedLatestCount = 0
|
||||||
|
$successfulLatestCount = 0
|
||||||
|
$previousFailureCount = 0
|
||||||
|
|
||||||
|
foreach ($latest in $latestByRepository) {
|
||||||
|
$latestStatus = [string]$latest.Status
|
||||||
|
$emoji = Get-BuildStatusEmoji -Status $latestStatus
|
||||||
|
$isLatestSuccessful = Test-IsSuccessfulStatus -Status $latestStatus
|
||||||
|
if ($isLatestSuccessful) {
|
||||||
|
$successfulLatestCount++
|
||||||
|
}
|
||||||
|
elseif ($latest.Severity -eq 'Critical') {
|
||||||
|
$failedLatestCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
$previousFailures = @($Analyses |
|
||||||
|
Where-Object {
|
||||||
|
$_.Repository -eq $latest.Repository -and
|
||||||
|
$_.RunId -ne $latest.RunId -and
|
||||||
|
$_.Severity -eq 'Critical'
|
||||||
|
} |
|
||||||
|
Sort-Object StartedAt -Descending |
|
||||||
|
Select-Object -First ([int]$Config.MaxPreviousFailures))
|
||||||
|
|
||||||
|
$previousFailureCount += $previousFailures.Count
|
||||||
|
$previousFailureText = if ($previousFailures.Count -gt 0) {
|
||||||
|
($previousFailures | ForEach-Object {
|
||||||
|
$shortSha = if ($_.CommitSha -and $_.CommitSha.Length -ge 8) { $_.CommitSha.Substring(0, 8) } else { $_.CommitSha }
|
||||||
|
$commitPart = if ($shortSha) { " ``$shortSha``" } else { '' }
|
||||||
|
"#{0} {1}{2}" -f $_.RunId, $_.Status, $commitPart
|
||||||
|
}) -join "`n"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
'None in this lookback window.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($previousFailureText.Length -gt 950) {
|
||||||
|
$previousFailureText = $previousFailureText.Substring(0, 947) + '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
$shortLatestSha = if ($latest.CommitSha -and $latest.CommitSha.Length -ge 8) { $latest.CommitSha.Substring(0, 8) } else { $latest.CommitSha }
|
||||||
|
$latestCommit = if ($shortLatestSha) {
|
||||||
|
if ($latest.CommitMessage) { "``$shortLatestSha`` $($latest.CommitMessage)" } else { "``$shortLatestSha``" }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
$color = if ($isLatestSuccessful) { 3066993 } elseif ($latest.Severity -eq 'Critical') { 15158332 } else { 3447003 }
|
||||||
|
$embeds.Add(@{
|
||||||
|
title = "$emoji Latest build: $($latest.Repository) #$($latest.RunId)"
|
||||||
|
url = $latest.BuildUrl
|
||||||
|
color = $color
|
||||||
|
description = if ($isLatestSuccessful) { 'Latest completed build is successful.' } elseif ($latest.Severity -eq 'Critical') { 'Latest completed build failed.' } else { 'Latest completed build has a non-success status.' }
|
||||||
|
fields = @(
|
||||||
|
@{ name = 'Latest status'; value = "$emoji $latestStatus"; inline = $true },
|
||||||
|
@{ name = 'Branch'; value = $latest.Branch; inline = $true },
|
||||||
|
@{ name = 'Duration'; value = $latest.DurationText; inline = $true },
|
||||||
|
@{ name = 'Commit'; value = $latestCommit; inline = $false },
|
||||||
|
@{ name = 'Previous failed builds'; value = $previousFailureText; inline = $false }
|
||||||
|
)
|
||||||
|
footer = @{
|
||||||
|
text = "Run ID $($latest.RunId)"
|
||||||
|
}
|
||||||
|
timestamp = (Get-Date).ToUniversalTime().ToString('o')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = @{
|
||||||
|
username = 'Gitea Build Monitor'
|
||||||
|
content = if ($failedLatestCount -gt 0) {
|
||||||
|
"❌ Latest Gitea build status: $failedLatestCount failed, $successfulLatestCount successful. Previous failures in window: $previousFailureCount."
|
||||||
|
}
|
||||||
|
elseif ($successfulLatestCount -gt 0) {
|
||||||
|
"✅ Latest Gitea build status: $successfulLatestCount successful. Previous failures in window: $previousFailureCount."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
"ℹ️ Latest Gitea build status reported. Previous failures in window: $previousFailureCount."
|
||||||
|
}
|
||||||
|
embeds = @($embeds)
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-DiscordWebhook -Payload $payload
|
||||||
|
if ($DryRun) {
|
||||||
|
Write-Log -Message "Prepared build status notification with $($embeds.Count) embed(s) in dry-run mode."
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Log -Message "Sent build status notification with $($embeds.Count) embed(s)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Get-PythonCommand {
|
function Get-PythonCommand {
|
||||||
$linuxVenvPython = Join-Path $PSScriptRoot '.venv/bin/python'
|
$linuxVenvPython = Join-Path $PSScriptRoot '.venv/bin/python'
|
||||||
$windowsVenvPython = Join-Path $PSScriptRoot '.venv/Scripts/python.exe'
|
$windowsVenvPython = Join-Path $PSScriptRoot '.venv/Scripts/python.exe'
|
||||||
@@ -921,6 +1064,25 @@ function Get-CacheKey {
|
|||||||
return "$(Get-RepositorySlug -Repository $Repository):$kind`:$jobId`:state:$state"
|
return "$(Get-RepositorySlug -Repository $Repository):$kind`:$jobId`:state:$state"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Get-BuildSummaryCacheKey {
|
||||||
|
param([Parameter(Mandatory)][object[]]$Analyses)
|
||||||
|
|
||||||
|
$parts = @($Analyses |
|
||||||
|
Group-Object Repository |
|
||||||
|
ForEach-Object {
|
||||||
|
$latest = $_.Group | Sort-Object StartedAt -Descending | Select-Object -First 1
|
||||||
|
$previousFailureIds = @($_.Group |
|
||||||
|
Where-Object { $_.RunId -ne $latest.RunId -and $_.Severity -eq 'Critical' } |
|
||||||
|
Sort-Object StartedAt -Descending |
|
||||||
|
Select-Object -First ([int]$Config.MaxPreviousFailures) |
|
||||||
|
ForEach-Object { $_.RunId })
|
||||||
|
'{0}:latest:{1}:{2}:prevfail:{3}' -f $latest.Repository, $latest.RunId, $latest.Status, ($previousFailureIds -join ',')
|
||||||
|
} |
|
||||||
|
Sort-Object)
|
||||||
|
|
||||||
|
return 'build-summary:' + ($parts -join '|')
|
||||||
|
}
|
||||||
|
|
||||||
function Start-BuildMonitor {
|
function Start-BuildMonitor {
|
||||||
Assert-Config
|
Assert-Config
|
||||||
|
|
||||||
@@ -961,17 +1123,9 @@ function Start-BuildMonitor {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
$cacheKey = Get-CacheKey -Repository $repository -Job $job
|
|
||||||
if ($cache.ContainsKey($cacheKey)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$analysis = Analyze-Build -Repository $repository -Job $job
|
$analysis = Analyze-Build -Repository $repository -Job $job
|
||||||
$analyses.Add($analysis)
|
$analyses.Add($analysis)
|
||||||
if (-not $DryRun) {
|
|
||||||
$cache[$cacheKey] = Get-Date
|
|
||||||
}
|
|
||||||
Write-Log -Message ("Analyzed job {0} for {1}: severity={2}, status={3}, duration={4}" -f (Get-ObjectValue -Object $job -Name 'id'), $analysis.Repository, $analysis.Severity, $analysis.Status, $analysis.DurationText)
|
Write-Log -Message ("Analyzed job {0} for {1}: severity={2}, status={3}, duration={4}" -f (Get-ObjectValue -Object $job -Name 'id'), $analysis.Repository, $analysis.Severity, $analysis.Status, $analysis.DurationText)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
@@ -981,10 +1135,19 @@ function Start-BuildMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($analyses.Count -gt 0) {
|
if ($analyses.Count -gt 0) {
|
||||||
Send-DiscordNotification -Analyses $analyses.ToArray()
|
$summaryCacheKey = Get-BuildSummaryCacheKey -Analyses $analyses.ToArray()
|
||||||
|
if ($cache.ContainsKey($summaryCacheKey)) {
|
||||||
|
Write-Log -Message 'Build status summary has already been sent.'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Send-BuildStatusNotification -Analyses $analyses.ToArray()
|
||||||
|
if (-not $DryRun) {
|
||||||
|
$cache[$summaryCacheKey] = Get-Date
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Write-Log -Message "No new jobs found in the last $($Config.LookbackMinutes) minute(s)."
|
Write-Log -Message "No completed builds found in the last $($Config.LookbackMinutes) minute(s)."
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
See the categories: @@html:<a href="../home/categories.html">Categories</a>@@
|
||||||
|
|
||||||
* Posts:
|
* Posts:
|
||||||
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">08-05-2026 15:52</span>@@
|
- [[file:career/career-list.org][Career List]] @@html:<span class="post-date">08-05-2026 16:04</span>@@
|
||||||
- [[file:posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:<span class="post-date">14-04-2026 16:36</span>@@
|
- [[file:posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:<span class="post-date">14-04-2026 16:36</span>@@
|
||||||
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/ha-dr.org][High Availability, Disaster Recovery and Business Continuity]] @@html:<span class="post-date">11-03-2026 17:18</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
- [[file:career/javascript.org][Understands the Javascript language]] @@html:<span class="post-date">11-03-2026 16:52</span>@@ @@html:<a href="/tags/learning.html"><span class="post-tag">learning</span></a>@@ @@html:<a href="/tags/notes.html"><span class="post-tag">notes</span></a>@@
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
- [[file:blogs/2026/04-april/ending-the-week-17-04-26.org][End of week thoughts]] @@html:<span class="post-date">2026-04-17 16:17</span>@@
|
- [[file:blogs/2026/04-april/ending-the-week-17-04-26.org][End of week thoughts]] @@html:<span class="post-date">2026-04-17 16:17</span>@@
|
||||||
- [[file:blogs/2026/04-april/16-04-26.org][Rambles]] @@html:<span class="post-date">2026-04-16 16:02</span>@@
|
- [[file:blogs/2026/04-april/16-04-26.org][Rambles]] @@html:<span class="post-date">2026-04-16 16:02</span>@@
|
||||||
- [[file:posts/posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:<span class="post-date">2026-04-14 16:36</span>@@
|
- [[file:posts/posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]] @@html:<span class="post-date">2026-04-14 16:36</span>@@
|
||||||
- [[file:home/categories.sync-conflict-20260417-233429-VT6366A.org][Categories]] @@html:<span class="post-date">2026-04-14 16:36</span>@@
|
|
||||||
- [[file:blogs/2026/04-april/comparison-14-04.org][Comparison is the thief of joy]] @@html:<span class="post-date">2026-04-14 09:55</span>@@
|
- [[file:blogs/2026/04-april/comparison-14-04.org][Comparison is the thief of joy]] @@html:<span class="post-date">2026-04-14 09:55</span>@@
|
||||||
- [[file:blogs/2026/04-april/12-04-week-review.org][[12-04-2026] - Weekly Review]] @@html:<span class="post-date">2026-04-12 12:00</span>@@
|
- [[file:blogs/2026/04-april/12-04-week-review.org][[12-04-2026] - Weekly Review]] @@html:<span class="post-date">2026-04-12 12:00</span>@@
|
||||||
- [[file:blogs/2026/04-april/starting-new-rotation-08-04-26.org][Starting new rotation]] @@html:<span class="post-date">2026-04-08 16:15</span>@@
|
- [[file:blogs/2026/04-april/starting-new-rotation-08-04-26.org][Starting new rotation]] @@html:<span class="post-date">2026-04-08 16:15</span>@@
|
||||||
@@ -28,3 +27,4 @@
|
|||||||
- [[file:blogs/2026/03-march/intellectually-challenging-myself-30-03-26.org][Intellectually challenging oneself]] @@html:<span class="post-date">2026-03-31 11:14</span>@@
|
- [[file:blogs/2026/03-march/intellectually-challenging-myself-30-03-26.org][Intellectually challenging oneself]] @@html:<span class="post-date">2026-03-31 11:14</span>@@
|
||||||
- [[file:blogs/2026/03-march/cooking-dinner-29-03-26.org][Cooking Dinner (num)]] @@html:<span class="post-date">2026-03-30 11:49</span>@@
|
- [[file:blogs/2026/03-march/cooking-dinner-29-03-26.org][Cooking Dinner (num)]] @@html:<span class="post-date">2026-03-30 11:49</span>@@
|
||||||
- [[file:blogs/2026/03-march/29-03-week-review.org][[29-03-2026] - Weekly Review]] @@html:<span class="post-date">2026-03-29 12:00</span>@@
|
- [[file:blogs/2026/03-march/29-03-week-review.org][[29-03-2026] - Weekly Review]] @@html:<span class="post-date">2026-03-29 12:00</span>@@
|
||||||
|
- [[file:blogs/2026/03-march/training-on-friday-afternoon-27-03-26.org][Training time on Friday afternoon]] @@html:<span class="post-date">2026-03-27 15:09</span>@@
|
||||||
|
|||||||
31
sitemap.org
31
sitemap.org
@@ -13,24 +13,11 @@
|
|||||||
- [[file:tags/website.org][Tag: website]]
|
- [[file:tags/website.org][Tag: website]]
|
||||||
- [[file:tags/life.org][Tag: life]]
|
- [[file:tags/life.org][Tag: life]]
|
||||||
- [[file:tags/update.org][Tag: update]]
|
- [[file:tags/update.org][Tag: update]]
|
||||||
- [[file:tags/insights.org][Tag: insights]]
|
|
||||||
- [[file:tags/emacs.org][Tag: emacs]]
|
|
||||||
- [[file:tags/education.org][Tag: education]]
|
- [[file:tags/education.org][Tag: education]]
|
||||||
|
- [[file:tags/insights.org][Tag: insights]]
|
||||||
- [[file:tags/reading.org][Tag: reading]]
|
- [[file:tags/reading.org][Tag: reading]]
|
||||||
|
- [[file:tags/emacs.org][Tag: emacs]]
|
||||||
- [[file:tags/maths.org][Tag: maths]]
|
- [[file:tags/maths.org][Tag: maths]]
|
||||||
- home
|
|
||||||
- [[file:home/countdown.org][Countdown]]
|
|
||||||
- [[file:home/contact.org][Contact]]
|
|
||||||
- [[file:home/backlog.org][Backlog]]
|
|
||||||
- [[file:home/notes.org][Notes]]
|
|
||||||
- [[file:home/services.org][Service]]
|
|
||||||
- [[file:home/status.org][Competency Status Board]]
|
|
||||||
- [[file:home/categories.sync-conflict-20260417-233429-VT6366A.org][Categories]]
|
|
||||||
- [[file:home/wird-tracker.org][Wird Tracker]]
|
|
||||||
- [[file:home/categories.org][Categories]]
|
|
||||||
- guide
|
|
||||||
- [[file:home/guide/setup.org][Setup]]
|
|
||||||
- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
|
|
||||||
- posts
|
- posts
|
||||||
- [[file:posts/posts-intro.org][Posts Introduction]]
|
- [[file:posts/posts-intro.org][Posts Introduction]]
|
||||||
- [[file:posts/posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]]
|
- [[file:posts/posts-list.sync-conflict-20260417-233432-VT6366A.org][Posts List]]
|
||||||
@@ -64,4 +51,16 @@
|
|||||||
- clean-code
|
- clean-code
|
||||||
- [[file:books/clean-code/clean-code-notes.org][Clean Code Notes]]
|
- [[file:books/clean-code/clean-code-notes.org][Clean Code Notes]]
|
||||||
- lima
|
- lima
|
||||||
- [[file:lima/lima-list.org][Lima]]
|
- [[file:lima/lima-list.org][Lima]]
|
||||||
|
- home
|
||||||
|
- [[file:home/countdown.org][Countdown]]
|
||||||
|
- [[file:home/contact.org][Contact]]
|
||||||
|
- [[file:home/backlog.org][Backlog]]
|
||||||
|
- [[file:home/notes.org][Notes]]
|
||||||
|
- [[file:home/services.org][Service]]
|
||||||
|
- [[file:home/status.org][Competency Status Board]]
|
||||||
|
- [[file:home/wird-tracker.org][Wird Tracker]]
|
||||||
|
- [[file:home/categories.org][Categories]]
|
||||||
|
- guide
|
||||||
|
- [[file:home/guide/setup.org][Setup]]
|
||||||
|
- [[file:home/guide/wird-tracker-guide.org][Wird Tracker — Technical Guide]]
|
||||||
Reference in New Issue
Block a user