Sync infra config: HA/Zabbix relocation, Immich storage move, new services

Accumulated uncommitted infra changes:
- Caddyfile: repoint HA/Zabbix to 192.168.1.4/.3, add ~20 new site routes
- Immich: move media to /mnt/smsg, enable CUDA ML, mem limits, rewrite backup.sh
- Add service stacks: agap-mcp, anki, family, freshrss, iperf3, kanboard,
  linkwarden, qbittorrent, radicale, syncthing, vikunja, windows
- openwebui: enable API keys; ollama: drop CPU fallback
- seafile/zabbix: extra_hosts entries; matrix: add user juris
- Remove pihole stack and stale wiki/migrate.py
- Ignore marketplace-mcp (standalone repo) and linkwarden runtime data

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
Alvis
2026-07-04 13:28:04 +00:00
parent 4363130163
commit b8efe4732d
51 changed files with 2813 additions and 374 deletions

73
agap-mcp/src/gitea.js Normal file
View File

@@ -0,0 +1,73 @@
import { execSync } from 'child_process';
import { writeFileSync, mkdirSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
const BASE = () => process.env.GITEA_URL || 'http://localhost:3000';
let _token = null;
export function initGitea(token) {
_token = token;
console.log('Gitea: ready');
}
function token() {
if (!_token) throw new Error('Gitea not initialized');
return _token;
}
async function api(path, opts = {}) {
const res = await fetch(`${BASE()}/api/v1${path}`, {
...opts,
headers: { Authorization: `token ${token()}`, 'Content-Type': 'application/json', ...opts.headers },
});
if (!res.ok) throw new Error(`Gitea API ${path}: ${res.status} ${await res.text()}`);
return res.json();
}
export async function giteaListRepos() {
return api('/repos/search?limit=50').then(r => r.data.map(r => ({
name: r.full_name, description: r.description, stars: r.stars_count,
})));
}
export async function giteaReadFile(repo, path, ref = 'HEAD') {
const data = await api(`/repos/${repo}/contents/${path}?ref=${ref}`);
return Buffer.from(data.content, 'base64').toString('utf8');
}
export async function giteaWikiList(repo = 'alvis/AgapHost') {
const data = await api(`/repos/${repo}/wiki/pages?limit=50`);
return data.map(p => ({ name: p.title, updated: p.last_commit?.created }));
}
export async function giteaWikiRead(page, repo = 'alvis/AgapHost') {
const data = await api(`/repos/${repo}/wiki/page/${encodeURIComponent(page)}`);
return Buffer.from(data.content_base64, 'base64').toString('utf8');
}
export async function giteaWikiWrite(page, content, message, repo = 'alvis/AgapHost') {
const dir = join(tmpdir(), 'agap-mcp-wiki');
const wikiUrl = `${BASE().replace('http://', `http://alvis:${token()}@`)}/alvis/AgapHost.wiki.git`;
const gitEnv = { ...process.env, GIT_AUTHOR_NAME: 'agap-mcp', GIT_AUTHOR_EMAIL: 'allogn@gmail.com', GIT_COMMITTER_NAME: 'agap-mcp', GIT_COMMITTER_EMAIL: 'allogn@gmail.com' };
try {
execSync(`git -C ${dir} pull ${wikiUrl} main`, { env: gitEnv, stdio: 'pipe' });
} catch {
execSync(`git clone ${wikiUrl} ${dir}`, { env: gitEnv, stdio: 'pipe' });
}
const file = join(dir, `${page}.md`);
writeFileSync(file, content);
execSync(`git -C ${dir} add "${page}.md"`, { env: gitEnv, stdio: 'pipe' });
execSync(`git -C ${dir} commit -m "${message || `Update ${page}`}"`, { env: gitEnv, stdio: 'pipe' });
execSync(`git -C ${dir} push ${wikiUrl} main`, { env: gitEnv, stdio: 'pipe' });
return `${page} updated`;
}
export async function giteaListIssues(repo, state = 'open') {
return api(`/repos/${repo}/issues?state=${state}&type=issues&limit=50`).then(issues =>
issues.map(i => ({ number: i.number, title: i.title, state: i.state, labels: i.labels.map(l => l.name) }))
);
}