Skip to content

Commit b012923

Browse files
authored
feat: sync master with latest changes
feat: Sync `master` with Latest Changes
2 parents f04e6dc + 95b2a2a commit b012923

41 files changed

Lines changed: 3206 additions & 1753 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 149 additions & 172 deletions
Large diffs are not rendered by default.

apps/api/nest-cli.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
"sourceRoot": "src",
55
"compilerOptions": {
66
"builder": "swc",
7-
"deleteOutDir": false,
7+
"deleteOutDir": true,
88
"tsConfigPath": "tsconfig.build.json",
9-
"typeCheck": false
9+
"typeCheck": false,
10+
"assets": ["**/*.yaml", "**/*.json"]
1011
}
1112
}

apps/api/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import { TemplateModule } from './modules/templates/templates.module'
77
import { UsersModule } from './modules/users/user.module'
88
import { config } from '@/common/config'
99
import { dbOptions } from '@/database/datasource'
10+
import { SeedersModule } from '@/database/seeders/seeders.module'
1011

1112
@Module({
1213
imports: [
1314
ConfigModule.forRoot({ load: [() => config()] }),
15+
SeedersModule,
1416
TypeOrmModule.forRoot({ ...dbOptions, autoLoadEntities: true }),
1517

1618
AuthModule,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Module } from '@nestjs/common'
2+
import { TypeOrmModule } from '@nestjs/typeorm'
3+
import { Config } from '@/modules/configs/configs.entity'
4+
import { ConfigTag } from '@/modules/configs/config-tag.entity'
5+
import { TemplatesSeederService } from './templates-seeder.service'
6+
7+
@Module({
8+
imports: [TypeOrmModule.forFeature([Config, ConfigTag])],
9+
providers: [TemplatesSeederService],
10+
})
11+
export class SeedersModule {}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import * as fs from 'node:fs'
2+
import * as path from 'node:path'
3+
import { Injectable, Logger, OnModuleInit } from '@nestjs/common'
4+
import { InjectRepository } from '@nestjs/typeorm'
5+
import { Repository } from 'typeorm'
6+
import { Config } from '@/modules/configs/configs.entity'
7+
import { ConfigTag } from '@/modules/configs/config-tag.entity'
8+
import { TemplateCategory } from '@/common/utils/enums'
9+
10+
/**
11+
* Sidecar JSON shape for a seeded template. The `description` field is kept
12+
* in the source JSON for documentation and future-proofing, but is NOT
13+
* persisted — the `Config` entity has no description column. Templates UI
14+
* currently uses `title` only.
15+
*/
16+
interface TemplateMeta {
17+
slug: string
18+
title: string
19+
description: string
20+
category: TemplateCategory
21+
tags: string[]
22+
}
23+
24+
@Injectable()
25+
export class TemplatesSeederService implements OnModuleInit {
26+
private readonly logger = new Logger(TemplatesSeederService.name)
27+
28+
constructor(
29+
@InjectRepository(Config) private readonly configRepo: Repository<Config>,
30+
@InjectRepository(ConfigTag) private readonly tagRepo: Repository<ConfigTag>,
31+
) {}
32+
33+
async onModuleInit(): Promise<void> {
34+
const force = process.env.FORCE_TEMPLATE_RESEED === 'true'
35+
let seedersDir = path.join(__dirname, 'templates')
36+
37+
if (!fs.existsSync(seedersDir)) {
38+
const srcDir = path.join(__dirname, '../../../src/database/seeders/templates')
39+
if (fs.existsSync(srcDir)) {
40+
seedersDir = srcDir
41+
this.logger.log(
42+
'Templates seeder: Development mode detected, reading templates directly from src.',
43+
)
44+
} else {
45+
this.logger.warn(
46+
`Templates seeder: directory not found at ${seedersDir} or ${srcDir}; skipping.`,
47+
)
48+
return
49+
}
50+
}
51+
52+
const entries = fs.readdirSync(seedersDir)
53+
const jsonFiles = entries.filter((f) => f.endsWith('.json')).sort()
54+
55+
let created = 0
56+
let updated = 0
57+
let skipped = 0
58+
let failed = 0
59+
60+
for (const jsonFile of jsonFiles) {
61+
try {
62+
const result = await this.seedOne(seedersDir, jsonFile, force)
63+
if (result === 'created') created++
64+
else if (result === 'updated') updated++
65+
else if (result === 'skipped') skipped++
66+
} catch (err) {
67+
failed++
68+
this.logger.error(
69+
`Templates seeder: failed to process ${jsonFile}: ${(err as Error).message}`,
70+
)
71+
}
72+
}
73+
74+
this.logger.log(
75+
`Templates seeder: created=${created}, updated=${updated}, skipped=${skipped}, failed=${failed} (force=${force})`,
76+
)
77+
}
78+
79+
private async seedOne(
80+
seedersDir: string,
81+
jsonFile: string,
82+
force: boolean,
83+
): Promise<'created' | 'updated' | 'skipped'> {
84+
const basename = jsonFile.replace(/\.json$/, '')
85+
const yamlPath = path.join(seedersDir, `${basename}.yaml`)
86+
87+
if (!fs.existsSync(yamlPath)) {
88+
this.logger.warn(`Template metadata ${jsonFile} has no matching YAML; skipping.`)
89+
return 'skipped'
90+
}
91+
92+
const meta: TemplateMeta = JSON.parse(fs.readFileSync(path.join(seedersDir, jsonFile), 'utf8'))
93+
const yaml = fs.readFileSync(yamlPath, 'utf8')
94+
95+
const existing = await this.configRepo.findOne({
96+
where: { slug: meta.slug },
97+
relations: ['tags'],
98+
})
99+
100+
if (existing) {
101+
if (!force) return 'skipped'
102+
103+
existing.title = meta.title
104+
existing.yaml = yaml
105+
existing.templateCategory = meta.category
106+
existing.isTemplate = true
107+
await this.configRepo.save(existing)
108+
109+
await this.tagRepo.delete({ configId: existing.id })
110+
if (meta.tags.length > 0) {
111+
const tagEntities = meta.tags.map((tag) =>
112+
this.tagRepo.create({ configId: existing.id, tag }),
113+
)
114+
await this.tagRepo.save(tagEntities)
115+
}
116+
117+
return 'updated'
118+
}
119+
120+
const fresh = this.configRepo.create({
121+
slug: meta.slug,
122+
title: meta.title,
123+
yaml,
124+
isTemplate: true,
125+
templateCategory: meta.category,
126+
userId: null,
127+
})
128+
const saved = await this.configRepo.save(fresh)
129+
130+
if (meta.tags.length > 0) {
131+
const tagEntities = meta.tags.map((tag) => this.tagRepo.create({ configId: saved.id, tag }))
132+
await this.tagRepo.save(tagEntities)
133+
}
134+
135+
return 'created'
136+
}
137+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"slug": "single-host-docker",
3+
"title": "Single-Host Docker",
4+
"description": "Mini-PC running Portainer, Pi-hole, Jellyfin, and Nextcloud in Docker, connected to a 5-port consumer router.",
5+
"category": "virtualization",
6+
"tags": ["DOCKER", "SINGLE HOST", "BEGINNER"]
7+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
meta:
2+
title: Single-Host Docker
3+
subtitle: One mini-PC running a small service stack
4+
tags:
5+
- DOCKER
6+
- SINGLE HOST
7+
- BEGINNER
8+
9+
networks:
10+
- id: lan
11+
name: Home LAN
12+
subnet: 192.168.1.0/24
13+
14+
groups:
15+
- id: edge
16+
name: Network Edge
17+
style: dashed
18+
color: "#00e5ff"
19+
- id: host
20+
name: Docker Host
21+
style: solid
22+
color: "#ffab00"
23+
24+
devices:
25+
- id: router
26+
name: Home Router
27+
type: router
28+
ip: 192.168.1.1
29+
network: lan
30+
group: edge
31+
interfaces:
32+
ethernet:
33+
count: 5
34+
speed: 1G
35+
ports:
36+
- { label: WAN }
37+
- { label: LAN1 }
38+
- { label: LAN2 }
39+
- { label: LAN3 }
40+
- { label: LAN4 }
41+
42+
- id: docker-host
43+
name: docker-host
44+
type: server
45+
ip: 192.168.1.10
46+
network: lan
47+
group: host
48+
specs:
49+
cpu: Intel N100
50+
ram: 16 GB
51+
storage: 1 TB NVMe
52+
os: Debian 12
53+
interfaces:
54+
ethernet:
55+
count: 1
56+
speed: 2.5G
57+
services:
58+
- name: Portainer
59+
port: 9000
60+
runtime: docker
61+
- name: Pi-hole
62+
port: 80
63+
runtime: docker
64+
- name: Jellyfin
65+
port: 8096
66+
runtime: docker
67+
- name: Nextcloud
68+
port: 8080
69+
runtime: docker
70+
- name: Uptime Kuma
71+
port: 3001
72+
runtime: docker
73+
74+
connections:
75+
- from: router
76+
to: docker-host
77+
fromPort: LAN1
78+
type: ethernet
79+
speed: 2.5G
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"slug": "proxmox-cluster",
3+
"title": "Proxmox 3-Node Cluster",
4+
"description": "Three Proxmox nodes (EPYC, 128 GB RAM each) with bonded 10G uplinks to a core switch, plus Ceph storage for HA.",
5+
"category": "virtualization",
6+
"tags": ["PROXMOX", "CLUSTER", "CEPH"]
7+
}

0 commit comments

Comments
 (0)