Compare commits

...

348 Commits

Author SHA1 Message Date
forust ace23ad1f9 fix: remove www.xdfnx.cfd from ingress, add Gitea access log config
ci / lint-prettier (push) Successful in 13s
ci / lint-ruff (push) Successful in 7s
ci / lint-yaml (push) Successful in 17s
ci / lint-dockerfiles (push) Successful in 14s
ci / validate (push) Successful in 15s
ci / build (push) Successful in 26s
2026-07-20 11:40:41 +02:00
forust 92aa731e44 deleted crowdsec stack from the repo. will figure something else
ci / lint-prettier (push) Successful in 18s
ci / lint-ruff (push) Successful in 6s
ci / lint-yaml (push) Successful in 6s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 6s
ci / build (push) Successful in 43s
Signed-off-by: mr-forust <vzlomdsisma@gmail.com>
2026-07-19 23:16:14 +02:00
forust 87ca3fd40c chore: updatet chernuha's pfp, added projects sections for forust.xyz
ci / lint-prettier (push) Successful in 8s
ci / lint-ruff (push) Successful in 3s
ci / lint-yaml (push) Successful in 7s
ci / lint-dockerfiles (push) Successful in 6s
ci / validate (push) Successful in 7s
ci / build (push) Successful in 18s
2026-07-05 02:59:31 +02:00
forust 82bcd30ed8 updated xdfnx's page
ci / lint-prettier (push) Successful in 8s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 6s
ci / lint-dockerfiles (push) Successful in 4s
ci / validate (push) Successful in 11s
ci / build (push) Successful in 19s
2026-07-04 01:01:05 +02:00
forust 620262d98c ci: fetch full history for change detection after rebase
ci / lint-prettier (push) Successful in 9s
ci / lint-ruff (push) Successful in 6s
ci / lint-yaml (push) Successful in 9s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 5s
ci / build (push) Successful in 3s
2026-07-04 00:55:46 +02:00
forust 831f3a46b0 updated xdfnx's page
ci / lint-prettier (push) Successful in 8s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 6s
ci / lint-dockerfiles (push) Successful in 5s
ci / validate (push) Successful in 5s
ci / build (push) Successful in 3s
2026-07-04 00:46:13 +02:00
forust f7902e74e8 ci: require lint and validate before build
ci / lint-prettier (push) Successful in 7s
ci / lint-ruff (push) Successful in 4s
ci / lint-yaml (push) Successful in 6s
ci / lint-dockerfiles (push) Successful in 3s
ci / validate (push) Successful in 5s
ci / build (push) Successful in 2s
2026-06-30 01:23:53 +02:00
forust eb8d1b361e ci: add branch tags to docker builds
ci / lint (push) Successful in 15s
ci / validate (push) Successful in 8s
ci / build (push) Successful in 3s
2026-06-30 01:20:51 +02:00
forust 6e2cafb206 ci: merge lint validate and docker builds
ci / lint (push) Successful in 14s
ci / validate (push) Successful in 19s
ci / build (push) Successful in 2s
2026-06-30 01:17:31 +02:00
forust 67b0c0824f ci: restore docker-based lint and validate jobs
lint / prettier (push) Successful in 7s
lint / ruff (push) Successful in 3s
lint / yamllint (push) Successful in 5s
lint / hadolint (push) Successful in 4s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 4s
2026-06-30 01:12:23 +02:00
forust 8e72e0a920 ci: run lint and validate tools locally
lint / prettier (push) Failing after 2s
lint / ruff (push) Successful in 2s
lint / yamllint (push) Successful in 3s
lint / hadolint (push) Failing after 2s
validate / yaml (push) Successful in 2s
validate / k8s (push) Failing after 1s
2026-06-30 00:14:14 +02:00
forust 73a1132beb Align Traefik Helm log values with chart v41
lint / prettier (push) Successful in 1m59s
lint / ruff (push) Successful in 4s
lint / yamllint (push) Successful in 5s
lint / hadolint (push) Successful in 4s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 3s
2026-06-29 23:02:26 +02:00
forust a128523c24 Fix CrowdSec middleware references in Traefik ingresses 2026-06-29 22:47:21 +02:00
forust ee881acd0e refactor: update xdfnx homepage index.html
lint / prettier (push) Successful in 14s
lint / ruff (push) Successful in 5s
lint / yamllint (push) Successful in 9s
lint / hadolint (push) Successful in 17s
validate / yaml (push) Successful in 8s
validate / k8s (push) Successful in 7s
2026-06-28 11:59:57 +02:00
forust bacb2f4b9f fix: correct Traefik rule syntax for local IngressRoutes
Move parentheses outside Host() calls so that || and && operators
are properly grouped in Traefik rule expressions.
2026-06-28 11:59:52 +02:00
forust 91211e7b78 fix: resolve CrowdSec Helm upgrade failure - remove duplicate DISABLE_ONLINE_API env and add metrics config 2026-06-28 11:59:46 +02:00
forust 9b3a7aadb4 fix: resolve pyrogram imports by adding venv and pyright config
lint / prettier (push) Successful in 7s
lint / ruff (push) Successful in 4s
lint / yamllint (push) Successful in 6s
lint / hadolint (push) Successful in 4s
validate / yaml (push) Successful in 5s
validate / k8s (push) Successful in 5s
- Create .venv with pyrofork and all dependencies installed
- Add pyrightconfig.json at workspace root and in userbot/
- Enable pyright as language server for Python in Zed settings
- Update uv.lock with resolved dependency tree
2026-06-21 22:33:04 +02:00
forust b123621ead chore: remove github obsolete prod deploy workflow
lint / prettier (push) Successful in 7s
lint / ruff (push) Successful in 4s
lint / yamllint (push) Successful in 6s
lint / hadolint (push) Successful in 4s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 4s
2026-06-21 22:24:30 +02:00
forust a2df8504f5 Fix DL3013: pin pip version in webinar-checker Dockerfile
lint / prettier (push) Successful in 8s
lint / ruff (push) Successful in 4s
lint / yamllint (push) Successful in 6s
lint / hadolint (push) Successful in 4s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 4s
2026-06-21 22:07:44 +02:00
forust fb43306571 Fix all lint issues: Dockerfiles (DL3015/DL3013/DL4006) + Ruff (173→0 errors)
lint / prettier (push) Successful in 8s
lint / ruff (push) Successful in 5s
lint / yamllint (push) Successful in 7s
lint / hadolint (push) Failing after 4s
validate / yaml (push) Successful in 5s
validate / k8s (push) Successful in 5s
Dockerfile fixes:
- edu_master/phpsessid-bot: add --no-install-recommends, pin pip versions
- edu_master/webinar-checker: pin pip versions with --no-cache-dir
- userbot: add SHELL with pipefail for pipe operations

Ruff fixes (173 → 0):
- W293/W291/W292: whitespace clean via ruff format
- N806: camelCase → snake_case (anilist, safone, hearts, flux, etc.)
- ARG001/ARG002: prefix unused params with _
- SIM115: use context managers for file I/O
- SIM117: combine nested with statements
- S608: noqa on SQL f-strings (module name is validated)
- E402/N812/N817: import fixes
- B023: pass loop variable as argument
- I001: auto-sorted imports
- syntax: fixed = vs == in dtek_notif/main.py
2026-06-21 22:01:51 +02:00
forust f424d91405 Tighten lint workflow scope
lint / prettier (push) Successful in 8s
lint / ruff (push) Failing after 5s
lint / yamllint (push) Successful in 6s
lint / hadolint (push) Failing after 4s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 4s
2026-06-21 21:51:20 +02:00
forust 88a8f2987f Unify workflow job naming
lint / prettier (push) Failing after 9s
lint / ruff (push) Failing after 5s
lint / yamllint (push) Successful in 7s
lint / hadolint (push) Failing after 13s
validate / yaml (push) Successful in 6s
validate / k8s (push) Successful in 5s
2026-06-21 21:46:22 +02:00
forust 17027b232b Rename workflows to yaml 2026-06-21 21:45:16 +02:00
forust 6ecbbb39b4 Fix yamllint workflow invocation
validate / yaml (push) Successful in 7s
validate / k8s (push) Successful in 5s
2026-06-21 21:40:11 +02:00
forust 175cbc8860 Fix validation workflows for self-hosted runner
validate / yaml (push) Failing after 15s
validate / k8s (push) Successful in 9s
2026-06-21 21:37:30 +02:00
forust 6363d050b0 Add YAML validation workflows 2026-06-21 21:27:31 +02:00
forust c855764bc6 feat: add vaultwarden deployment config (compose + k8s)
Deploy to Server / deploy (push) Failing after 1s
2026-06-21 01:02:09 +02:00
forust 7d92b85e21 Merge commit '4355f451d4b4288d43468e08cad1f377e511f98a'
Deploy to Server / deploy (push) Has been cancelled
2026-06-19 23:23:28 +02:00
forust 9364392bc0 Merge commit '3eaef5dc90b716cc0fa391cbe2394be56d5f6041' as 'userbot'
Deploy to Server / deploy (push) Has been cancelled
2026-06-19 23:20:23 +02:00
forust 4355f451d4 Merge commit '3eaef5dc90b716cc0fa391cbe2394be56d5f6041' as 'userbot' 2026-06-19 23:20:23 +02:00
forust 3eaef5dc90 Squashed 'userbot/' content from commit 7fb0a0e
git-subtree-dir: userbot
git-subtree-split: 7fb0a0e179
2026-06-19 23:20:23 +02:00
forust 69ffd3682e refactor: userbot to subtree 2026-06-19 23:20:13 +02:00
forust 1930600c40 Revert "refactor: extract userbot to standalone repo, add as git submodule"
This reverts commit 3c383db9a7.
2026-06-19 23:11:27 +02:00
forust d74a705d53 chore(k8s): rewritten ingressroute to include headplane AND headscale-admin ui
Deploy to Server / deploy (push) Has been cancelled
new routes groupping style
2026-06-19 23:02:22 +02:00
forust 3c383db9a7 refactor: extract userbot to standalone repo, add as git submodule
Deploy to Server / deploy (push) Has been cancelled
userbot now lives at ssh://git@gitssh.forust.xyz:2221/forust/userbot.git
and is included in homelab as a submodule at userbot/
2026-06-19 15:39:00 +02:00
forust 7fb0a0e179 chore: batch lint fixes across userbot and edu_master
- S113: Add timeout=10 to all requests calls (74 fixes)
- E722: Replace bare except: with except Exception:
- B904: Replace redundant re-raise with bare raise
- E402: Add noqa for intentional late imports after import_library()
- S102/S307/S310/S311/S603/S605/S606/S607/S108: Add noqa for intentional usage
- F601: Fix duplicate dict key in unsplash.py
- N802: Rename ReplyCheck -> reply_check with backward compat alias
- N813: Rename bs -> BS in icons.py
- B007/B020: Rename loop var _j in animations.py
- SIM102: Collapse nested if in autofwd.py
- SIM113: Use enumerate() in calculator.py
- A002: Add noqa for builtin shadowing in admlist.py
- F811: Add noqa for cohere redefinition
- edu_master: Fix ARG001, S108, S110, SIM117, apply --unsafe-fixes
- Add modules_list.txt with full module inventory
2026-06-19 15:36:25 +02:00
forust 227e5fda27 chore: batch lint fixes across userbot and edu_master
- S113: Add timeout=10 to all requests calls (74 fixes)
- E722: Replace bare except: with except Exception:
- B904: Replace redundant re-raise with bare raise
- E402: Add noqa for intentional late imports after import_library()
- S102/S307/S310/S311/S603/S605/S606/S607/S108: Add noqa for intentional usage
- F601: Fix duplicate dict key in unsplash.py
- N802: Rename ReplyCheck -> reply_check with backward compat alias
- N813: Rename bs -> BS in icons.py
- B007/B020: Rename loop var _j in animations.py
- SIM102: Collapse nested if in autofwd.py
- SIM113: Use enumerate() in calculator.py
- A002: Add noqa for builtin shadowing in admlist.py
- F811: Add noqa for cohere redefinition
- edu_master: Fix ARG001, S108, S110, SIM117, apply --unsafe-fixes
- Add modules_list.txt with full module inventory
2026-06-19 15:36:25 +02:00
forust 20bce5b31c fix(userbot): add missing safone.py imports, apply ruff --unsafe-fixes
- Add missing aiohttp, PIL, BytesIO imports to safone.py (F821 runtime bugs)
- Apply ruff --unsafe-fixes (27 fixes): ternary operators, .get() patterns,
  contextlib.suppress, enumerate(), collapsible if/else, remove .keys()
2026-06-19 15:27:12 +02:00
forust 70d7855f06 fix(userbot): add missing safone.py imports, apply ruff --unsafe-fixes
- Add missing aiohttp, PIL, BytesIO imports to safone.py (F821 runtime bugs)
- Apply ruff --unsafe-fixes (27 fixes): ternary operators, .get() patterns,
  contextlib.suppress, enumerate(), collapsible if/else, remove .keys()
2026-06-19 15:27:12 +02:00
forust c905bbd039 chore(userbot): optimize Dockerfile with multi-stage build and static ffmpeg
- Multi-stage build: pip deps built in separate stage
- Static ffmpeg binary instead of apt package (avoid 200+ deps)
- Keep only git, mediainfo, wget via apt
2026-06-19 14:56:37 +02:00
forust fc83176522 chore(userbot): optimize Dockerfile with multi-stage build and static ffmpeg
Deploy to Server / deploy (push) Has been cancelled
- Multi-stage build: pip deps built in separate stage
- Static ffmpeg binary instead of apt package (avoid 200+ deps)
- Keep only git, mediainfo, wget via apt
2026-06-19 14:56:37 +02:00
forust 7ba6bc44f2 chore(userbot): apply ruff check --fix and ruff format
- ruff check --fix: 210 auto-fixed errors (import sorting, trailing
  whitespace, unused imports, f-string fixups, deprecated annotations)
- ruff format: 104 files reformatted to consistent style
- 268 non-auto-fixable issues remain (S113 requests timeout, etc.)
2026-06-19 12:19:01 +02:00
forust 0506aaaac8 chore(userbot): apply ruff check --fix and ruff format
- ruff check --fix: 210 auto-fixed errors (import sorting, trailing
  whitespace, unused imports, f-string fixups, deprecated annotations)
- ruff format: 104 files reformatted to consistent style
- 268 non-auto-fixable issues remain (S113 requests timeout, etc.)
2026-06-19 12:19:01 +02:00
forust bd9724da69 Revert "chore: switch yaml formatter to prettier, disable yaml-language-server validation (yamllint covers it)"
This reverts commit 3bad433f1a.
2026-06-19 12:15:14 +02:00
forust 3bad433f1a chore: switch yaml formatter to prettier, disable yaml-language-server validation (yamllint covers it) 2026-06-19 12:11:58 +02:00
forust 2bd7a5176f Revert "chore: disable yaml-language-server formatting (handled by prettier), set yamlVersion 1.2"
This reverts commit a9ff01261b.
2026-06-19 12:08:42 +02:00
forust a9ff01261b chore: disable yaml-language-server formatting (handled by prettier), set yamlVersion 1.2 2026-06-19 12:06:16 +02:00
forust 95cec59263 chore(userbot): apply prettier formatting across manifests 2026-06-19 12:03:13 +02:00
forust 1362ebc3c2 chore(userbot): apply prettier formatting across manifests 2026-06-19 12:03:13 +02:00
forust 2de6131ba7 chore: apply prettier formatting across compose files and configs 2026-06-19 12:03:11 +02:00
forust 1762962f32 chore(k8s): fix indentation in ingress manifests (3-space -> 2-space) 2026-06-19 12:03:02 +02:00
forust 7fb9e46c05 chore: remove singleQuote from prettier config to preserve original quotes 2026-06-19 12:02:59 +02:00
forust b33488342a chore: add truthy allowed-values to yamllint config 2026-06-19 12:01:11 +02:00
forust d53b14b1de chore: apply yaml lint fixes across compose files
- Fix trailing whitespace in compose files
- Add missing final newlines (EOF)
- Fix indentation in dockmon (3-space -> 2-space) and glance monitor.yml
- Align comments consistently
2026-06-19 11:57:14 +02:00
forust a6a6d933da chore: add linter configs (editorconfig, hadolint, markdownlint, prettier, yamllint, ruff, zed) 2026-06-19 11:57:04 +02:00
forust 0803f3efff chore(k8s): returned to Host || Host standart instead of regexp.
Deploy to Server / deploy (push) Has been cancelled
Yaml lint (yamllint)
2026-06-18 21:02:40 +02:00
forust ec0420962b hotfix: update headscale config for v0.29.0 breaking changes
- ephemeral_node_inactivity_timeout → node.ephemeral.inactivity_timeout
- randomize_client_port → randomizeClientPort in policy file
2026-06-18 14:26:58 +02:00
forust b407202e53 chore(k8s): imagepullpolicy always for homepages
Deploy to Server / deploy (push) Has been cancelled
tools column on the forust.xyz landing
2026-06-17 01:53:00 +02:00
forust b9b8474455 feat(k8s): protect all prod routers with crowdsec middleware 2026-06-17 01:50:33 +02:00
forust 76853637bc feat: traefik prometheus metrics 2026-06-17 01:41:45 +02:00
forust dac3777fc4 fix: authentik k8s url 2026-06-17 01:41:13 +02:00
forust bb5a3697f2 feat: bentopdf k8s manifests 2026-06-16 13:35:03 +02:00
forust e73aacb900 feat: add bentopdf pdf suite to converters stack 2026-06-16 12:36:31 +02:00
forust ea483da645 feat: kube-prometeus-stack 2026-06-16 12:36:31 +02:00
forust 85d35f86a7 feat: switch adguard dns to a dedicated metallb IP 2026-06-16 12:36:31 +02:00
forust 3d03ab1ea4 fix: priority for homepage's router 2026-06-16 12:36:26 +02:00
forust 4ca3ccdad3 chore(k8s): router rewrite
- returned to Host matcher instead of Hostregexp
- switched dockercompose labels to letsencrypt
- renamed DoH route
2026-06-16 12:34:15 +02:00
forust 10e26cda72 feat: crowdsec
- moved bouncer tokens to secrets
- experimental host ssh log parsing
- moved crowdsec to it's own directory
2026-06-16 00:23:29 +02:00
forust c648dfd147 chore(k8s): renamed gitea ingressroute to ingress
Deploy to Server / deploy (push) Has been cancelled
2026-06-14 23:24:24 +02:00
forust 2f97821dc6 feat: crowdsec lapi helm
Deploy to Server / deploy (push) Has been cancelled
2026-06-14 23:21:37 +02:00
forust 3d78b90f3a updated kener to v4.1.0 2026-06-14 23:02:35 +02:00
forust 3dc8228e22 matched dns record and the searxng's route 2026-06-14 23:01:58 +02:00
forust 93171ad8e6 fixed and tweaked glance app 2026-06-14 23:00:29 +02:00
forust dca7ad0902 fix: race condition convertx and homepage
typo in glance stripprefix
glance placed at /glance
2026-06-14 22:48:50 +02:00
forust 26d10f4e71 changed traefik bgp ip
Deploy to Server / deploy (push) Has been cancelled
2026-06-11 19:44:07 +02:00
forust 68c5eac164 chore: compact ingress rules with regex 2026-06-11 14:20:03 +02:00
forust 30f0f05150 chore(k8): system requirements fine-tuning
- reduced at homepages
- reduced at netronome
- reduced at dockmon
- reduced at gitea
- reduced at kener
2026-06-11 03:51:16 +02:00
forust a97560eaf3 ui(honepage): minor changes on forust homepage
- add kubernetes skill gauge
- add links to services
2026-06-11 03:44:17 +02:00
forust 2dce972be2 feat(k8s): traefik metallb and minecraft ports
- MetalLB binded on 172.20.10.2
- disabled hostnetwork

- 25565 MC Java
- 19132 UDP MC Bedrock
2026-06-11 03:42:59 +02:00
forust c2ad1122e5 fix: add www subdomain routers 2026-06-11 03:41:46 +02:00
forust 4c356924e7 feat(k8s): authentik server+worker deployments and postgres statefulset 2026-06-11 03:35:28 +02:00
forust 51777f904f feat(k8s): searchx metasearch engine 2026-06-11 02:52:03 +02:00
forust 37550da086 chore(k8s): portainer sys reqs 2026-06-10 23:43:41 +02:00
forust 12d59cb6f9 feat(k8s): n8n 2026-06-10 23:32:59 +02:00
forust 714d4896c6 feat(k8s): glance deployments 2026-06-10 23:07:39 +02:00
forust e369875117 chore(k8s): change termix statefulset to deployment
Signed-off-by: mr-forust <vzlomdsisma@gmail.com>
2026-06-10 21:51:33 +02:00
forust 31e61a9513 chore(k8s): convertx system reqs 2026-06-10 21:40:12 +02:00
forust 9b21f8056c chore(k8s): added more predictivity 2026-06-10 21:22:01 +02:00
forust 4623fbd8bd chore: make metube downloads writed to a temporary storage 2026-06-10 21:17:43 +02:00
forust 18d1e21690 fix(k8s): traefik log spam for non-existing local-tls secret, because of namespace isolation 2026-06-10 21:14:10 +02:00
forust 20b8c93275 chore(k8s): make dockmon statefulset
+ adjusted userbot sys reqs
2026-06-10 19:40:45 +02:00
forust 5f88ecf02b chore(k8s): make dockmon statefulset
+ adjusted userbot sys reqs
2026-06-10 19:40:45 +02:00
forust 0093e5ac52 feat(k8s): termix statefulset 2026-06-10 19:38:16 +02:00
forust bb821e138a chore(k8): adjusted system resources requests and limits based on manual monitoring 2026-06-10 19:37:16 +02:00
forust 1ea669220b chore(k8): adjusted system resources requests and limits based on manual monitoring 2026-06-10 19:37:16 +02:00
forust f068a3e6a0 feat: convertx k8s deployment 2026-06-09 14:41:56 +02:00
forust b7854447af lint: yaml spaces and tabs 2026-06-09 12:59:36 +02:00
forust 7a3708f70c lint: yaml spaces and tabs 2026-06-09 12:59:36 +02:00
forust 01ae2dd5cf Merge branch 'feat/convertx' into k8s/full-transfer 2026-06-09 12:19:13 +02:00
forust 82595804bf fix: naio port fix 2026-06-09 12:17:43 +02:00
forust 31b3c5bc31 add kener redis service (clusterip none) 2026-06-09 09:04:02 +02:00
forust 1cdbfb2d7b chore: secrets example for gitea 2026-06-09 09:03:39 +02:00
forust 6232b77026 fix: traefik restart policy to fix port issue 2026-06-09 09:02:30 +02:00
forust 22ffd779cc fix: switched headscale to external-service (managed by docker, routed by k8s)
- Changed ports for headscale to prefix "1"
2026-06-09 09:01:44 +02:00
forust d85b3f02f5 k8s: fixes and formating
- Add checkmk secrets example
- space-tab fixes (linter)
- switcher kener redis to statefulset
- disabled netronome voulme mount (not needed). postgres statefulset instead
-  switched naio to 8888 port (traefik port conflict)
- changed nextcloud ips and ports to prod
2026-06-08 17:39:32 +02:00
forust 17b2dfdc2e fix: gitea ssh tcp router, dns over tls adguard router 2026-06-08 14:20:28 +02:00
forust b641e3bd94 fix: adguard cers mount name 2026-06-08 12:40:23 +02:00
forust e19660fdf4 feat(k8s): standardize IngressRoutes — LE prod certs, prod→local→dev order, Traefik values fix 2026-06-08 12:27:55 +02:00
forust 36922a177c feat(k8s): add K8s manifests for all homelab services
traefik, gitea, adguard, nextcloud, errorpages, homepages,
uptime-kuma, kener, checkmk, headscale, dockmon, metube,
downtify, portainer, netronome, userbot

Includes Helm values, deployments, services, ingress routes,
configmaps, secrets (placeholders), postgres statefulsets,
kustomize overlays, and Traefik dynamic configuration.
2026-06-08 10:54:03 +02:00
forust f3d04e935c k8s: cloudflare-ddns deployment via kubernetes
Deploy to Server / deploy (push) Has been cancelled
2026-06-07 21:04:03 +02:00
forust e5797e60b7 upd: traefik v3.7.2
Deploy to Server / deploy (push) Has been cancelled
2026-06-07 20:36:51 +02:00
forust 444bb97f8e feat: add userbot k8s deployment method
- Kustomize: base + overlays/dev + overlays/prod
2026-06-07 19:41:28 +02:00
forust 0c5cf29f83 feat: add userbot k8s deployment method
Deploy to Server / deploy (push) Has been cancelled
- Kustomize: base + overlays/dev + overlays/prod
2026-06-07 19:41:28 +02:00
forust 4f01cdac31 fix: removed git checkout (was destructive)
- Remove git init/fetch/checkout from utils/misc.py
- Hardcode userbot_version to 2.5.0
2026-06-07 19:40:52 +02:00
forust 43c38767a1 fix: removed git checkout (was destructive)
- Remove git init/fetch/checkout from utils/misc.py
- Hardcode userbot_version to 2.5.0
2026-06-07 19:40:52 +02:00
forust cb40b10ecf chore: add gitea container registry compose support for localy builded apps 2026-06-07 14:37:57 +02:00
forust a68c01a68f chore: add gitea container registry compose support for localy builded apps
Deploy to Server / deploy (push) Has been cancelled
2026-06-07 14:37:57 +02:00
forust bdcdb1cb3d feat: router for gitea container registry (unproxied) 2026-06-07 14:37:53 +02:00
forust fe2b80b51f fix: errorpages intercepted gitea container registry endpoint
gitea registry introduced
2026-06-07 13:39:54 +02:00
forust b8d5bc747d hack: commented out local certs until i figure out how to route certs for local routers 2026-06-07 13:38:14 +02:00
forust 6f77b7d845 switch to embedded dockmon login page
Deploy to Server / deploy (push) Has been cancelled
2026-05-27 20:11:48 +02:00
forust ea286e117d feat: add diary (/diary) command with calendar parsing via Playwright
Deploy to Server / deploy (push) Has been cancelled
- Parse school diary calendar HTML table for daily/weekly/monthly views
- Cache diary data with 5-minute TTL
- Inline keyboard for today/tomorrow/week/month selection
- Ukrainian month/weekday names and formatting
2026-05-27 19:39:45 +02:00
forust 6a050669e8 chore: ignore all generated searxng core-config files 2026-05-27 19:39:41 +02:00
forust b9c11b8eab Merge branch 'main' into optimize/userbot
Deploy to Server / deploy (push) Has been cancelled
2026-05-25 01:48:31 +02:00
forust 6dac841b2c updated traefik to v3.7,
Deploy to Server / deploy (push) Has been cancelled
resolved maxResponseBodySize not set
2026-05-25 01:47:59 +02:00
forust bed58c84ef refactor: update .dockerignore
pull_policy never to use local images
2026-05-25 01:43:46 +02:00
forust 526a2b617a refactor: update .dockerignore
pull_policy never to use local images
2026-05-25 01:43:46 +02:00
forust 56e5d79e3e refactor: improved error handling, timeouts and use temp files 2026-05-21 22:14:29 +02:00
forust 1e08391e0e refactor: improved error handling, timeouts and use temp files 2026-05-21 22:14:29 +02:00
forust cd0ce06246 refactor: imporved layer caching for dockerfile
using existing built image for account 2
2026-05-21 22:12:33 +02:00
forust 0a31601b77 refactor: imporved layer caching for dockerfile
using existing built image for account 2
2026-05-21 22:12:33 +02:00
forust e9a9271d2f WIP: feat: convertx compose layout 2026-05-21 22:00:39 +02:00
forust 25eb89c902 feat: add searxng service (metasearch engine) 2026-05-21 21:34:40 +02:00
forust d988b0f7db refactor: change Cloudlfare DDNS config to env-based
Deploy to Server / deploy (push) Failing after 14m7s
2026-05-08 17:38:41 +02:00
forust e873f37159 refactor: nextcloud now requires mounting /dev/dri inside the container.
Deploy to Server / deploy (push) Failing after 0s
Changed network name for nextcloud
2026-05-08 16:39:09 +02:00
forust 8362f45bdc upd: updated image tags for services:
- gitea 1.25.1 --> 1.26
- portainer-ce latest --> 2.41.0
- traefik latest --> 3.6.15
2026-05-08 16:37:26 +02:00
forust fd5ea6cadd upd: kener v4.0.23
Deploy to Server / deploy (push) Failing after 0s
2026-04-10 02:29:02 +02:00
forust aa3598ee3d Merge pull request 'upd: updated kener to v4' (#14) from upd/kener-v4 into main
Deploy to Server / deploy (push) Failing after 1s
Reviewed-on: #14
2026-02-27 13:06:36 +01:00
forust c0205fa49b upd: updated kener to v4 2026-02-27 13:05:56 +01:00
forust a22707770f feat: update certificates logic:
Deploy to Server / deploy (push) Failing after 1s
- Switched xdfnx's homepage certificates to traefik-managed mode
- Renamed local fallback certs
2026-02-25 21:12:44 +01:00
forust 646f988a86 chore: Pinned stable kener version
Deploy to Server / deploy (push) Failing after 5s
2026-02-24 12:09:06 +01:00
forust 414d17d839 deleted test gitea workflow
Deploy to Server / deploy (push) Successful in 1s
2026-02-22 03:53:51 +01:00
forust 812e4eb87a ci: add copy of the gitea workflow for the github
Deploy to Server / deploy (push) Successful in 1s
2026-02-22 03:37:45 +01:00
forust 70b3b203fb fix: update email address
Deploy to Server / deploy (push) Has been cancelled
2026-02-22 00:28:19 +01:00
forust d857f3838d revert testing message
Deploy to Server / deploy (push) Successful in 1s
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
2026-02-20 20:41:01 +01:00
forust f8620349a9 fix deploy pipeline to prevent logspam
Deploy to Server / deploy (push) Successful in 16s
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
2026-02-20 20:38:39 +01:00
forust b1acff5c85 ci: add run-name to deployment workflow
Deploy to Server / deploy (push) Successful in 19s
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
2026-02-20 20:25:17 +01:00
forust beb6dca6a2 CI test change 2026-02-20 20:23:11 +01:00
forust aed6ff31f7 fix: add .runner (act runner settings) to .gitignore 2026-02-20 20:22:10 +01:00
forust 73684af21b ci: deploy workflow with local act_runner
Deploy to Server / deploy (push) Successful in 1s
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
2026-02-20 20:09:26 +01:00
forust cd5c90adcc Merge branch 'main' into ci/gitea-actions 2026-02-20 20:08:48 +01:00
forust 578a1ca544 revert: naio needs insecure transport
2593b54402 chore: add backquotes for downtify traefik labels removed insecureTransport
2026-02-20 01:25:23 +01:00
forust 12ed20a306 fix: support for external dynamic traefik configs 2026-02-20 01:16:54 +01:00
forust 400e7b6595 Starting learning cicd from scratch
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 8s
Demo gitea action
2026-02-19 00:36:02 +01:00
forust f58e96a25d add support for external dynamic traefik configs 2026-02-07 01:56:10 +01:00
forust a3e5f5a78b fix: let traefik handle https to portainer 2026-02-05 02:42:39 +01:00
forust 1a09a62a4c chore: deploy and lint workflows 2026-02-05 02:17:47 +01:00
forust 2593b54402 chore: add backquotes for downtify traefik labels
removed insecureTransport
2026-02-05 01:33:04 +01:00
forust 42826a037c refactor: store n8n data in named volumes 2026-02-04 22:26:43 +01:00
forust d7a68237e5 chore: remove redundant or unnececary traefik labels
- traefik.docker.network= (defined by traefik cli)
- traefik.http.routers.<routername>.middlewares=security-headers@file" (applied globally by traefik cli)
2026-02-04 22:26:27 +01:00
forust bbb8bdf0a7 feat: traefik routers for netronome, exported netronome env to .env 2026-02-04 11:00:44 +01:00
forust f4d3bd9c6d feat: basic netronome service 2026-02-03 14:25:53 +01:00
forust 6b919df7c6 chore: restart downtify service unless stopped 2026-02-01 17:24:50 +01:00
forust 3ee0b96ed5 feat: add acl policies to the headscale 2026-02-01 01:14:58 +01:00
forust d25d213d9b feat: add headscale admin web admin (along with headplane accessible via port) 2026-01-31 13:38:33 +01:00
forust a3b7c4c889 commit team avatars (and love) 2026-01-29 21:19:34 +01:00
forust 3dbb50c924 json logging 2026-01-29 17:30:09 +01:00
forust 54da82432b fix: remove headplane healtcheck (always false negative) 2026-01-29 17:22:47 +01:00
forust e5eb234c41 chore: gitignore for adguard 2026-01-29 17:22:40 +01:00
forust 721348e67f refactor: switch adguard data to named volume 2026-01-29 17:22:33 +01:00
forust d58ae2a5e7 chore: new statuspage for errorpages 2026-01-25 23:41:42 +01:00
forust aa516c3445 Merge pull request 'fix: errorpage styles fixes #11' (#12) from fix/errorpage-styles into main
Reviewed-on: #12
2026-01-25 23:39:10 +01:00
forust b5cc7d9a0d fix: errorpage styles fixes #11 2026-01-25 23:38:34 +01:00
forust 5dfa9c879b chore: comment-out errorpage ports 2026-01-24 01:24:18 +01:00
forust 3c5702a0fb Merge pull request 'feat/kener' (#9) from feat/kener into main
Reviewed-on: #9
2026-01-24 01:19:52 +01:00
forust dd0ca27f4f fix: add TZ env for checkmk 2026-01-24 01:16:24 +01:00
forust 7f7d0de090 fix: comment out kener ports 2026-01-24 00:58:09 +01:00
forust bee3f16cb2 fix: set restart policy of kener to Unelss stopped 2026-01-24 00:55:43 +01:00
forust 303d23968d fix: set restart policy of errorpages to Unelss stopped 2026-01-24 00:53:41 +01:00
forust b7ecf88052 fix: correct metube local router rule and fix TLS configuration comments 2026-01-23 17:54:00 +01:00
forust 5068591b8b fix: xdfnx's certificate 2026-01-23 17:48:00 +01:00
forust 8e57f21e44 feat: add traefik routers for kener (status subdomain) 2026-01-23 11:26:55 +01:00
forust 073d9ff569 feat: add initial kener instance (uptime monitoring) 2026-01-23 11:12:36 +01:00
forust c6d00e525b fix: comment-out CF Origin CA. (google trust service) 2026-01-22 00:51:15 +01:00
forust 539994a145 Merge branch 'feat/error-page' 2026-01-21 20:14:26 +01:00
forust 95f0afbbee feat: add traefik integration for error-handler
TODO: fix css
2026-01-21 20:14:02 +01:00
forust 9f86bd1142 feat: add errorpage-handler service (403,404, 500, 502-504) 2026-01-21 17:39:14 +01:00
forust af9668e8e6 md: archive overlayfs incident 2026-01-21 17:33:18 +01:00
forust 53b71a33ad fix: correct adguard traefik devrule 2026-01-21 11:41:37 +01:00
forust bf72007b14 feat: headplane (ui for headscale) 2026-01-21 10:09:31 +01:00
forust 0bad0a817e fix: bad request
Revert "chore: compose cleanup:"

This reverts nextcloud service's changes from commit 45ce789f58.
2026-01-20 15:11:06 +01:00
forust 525fed3b92 Merge pull request 'chore/compose-cleanup' (#8) from chore/compose-cleanup into main
Reviewed-on: #8
2026-01-20 00:17:42 +01:00
forust fe5e5c5e35 fix: aborted connection to portainer 2026-01-20 00:08:18 +01:00
forust 72aa022048 fix: "http plaintext sent to https" 2026-01-20 00:05:10 +01:00
forust f18d4d9be4 chore: use volumes to store data of
- portainer
- headscale
- termix
- uptime-kuma
- dockmon
2026-01-19 23:58:26 +01:00
forust 45ce789f58 chore: compose cleanup:
- Remove TZ envs
- +- unified compose structure
- Minify where possible
- Remove <service>.internal routers
- Remove service specifications where possible
Affected services:
- adguardhome
- authentik
- cfddns
- checkmk
- dockmon
- downtify
- gitea
- glance
- headscale
- homepages
- metube
- nextcloud
- penpot
- portainer
- termix
- traefik
- uptime-kuma

TODO: Move data from directory to volumes
2026-01-19 23:33:50 +01:00
forust d7c05fd058 fix: use webinar links to detect duplicates, supress logspam from telegram bot 2026-01-19 17:01:23 +01:00
forust c13056fba1 Merge pull request 'feat/checkmk' (#7) from feat/checkmk into main
Reviewed-on: #7
2026-01-19 15:59:21 +01:00
forust 5fe8af82d5 Revert "fix: clean up traefik configuration and add redirect middleware"
This reverts commit dc7fe64fbc.
2026-01-19 11:18:36 +01:00
forust 6d97246997 fix: change checkmk container restart policy to 'unless-stopped' 2026-01-19 00:08:16 +01:00
forust 6970279311 fix: remove checkmk port for docker cmk agent, using external agent 2026-01-18 23:09:41 +01:00
forust 02f4e0ab42 chore: switch to named volumes for site storing 2026-01-18 22:39:48 +01:00
forust 4a25622552 Merge branch 'main' of ssh://gitssh.forust.xyz:2221/forust/homelab into feat/checkmk 2026-01-18 19:28:12 +01:00
forust 0138fbd276 fix: update middleware for dev router in glance compose file 2026-01-18 19:24:41 +01:00
forust 94b5f39207 make glance dashboard public again 2026-01-18 00:19:39 +01:00
forust 403e88d548 fix: update NEXTCLOUD_DATADIR path to match new hardware 2026-01-17 17:44:11 +01:00
forust d03a4844fb chore: remove unused tailscale setup 2026-01-17 15:38:27 +01:00
forust 48ff08529e Merge pull request 'feat: add group support for webinar notifier' (#5) from feat/edu-group into main
Reviewed-on: #5
2026-01-14 16:46:37 +01:00
forust 81592b6142 feat: add group support for webinar notifier 2026-01-13 00:24:04 +01:00
forust 9480576966 fix: update bots' code to match .env keys 2026-01-12 15:33:42 +01:00
forust 9b43a9bef4 feat: add traefik configuration for external fileservers 2026-01-09 14:48:42 +01:00
forust 2b03335af5 chore: shorten aio subdomain 2026-01-06 19:04:23 +01:00
forust ea738ec14c Merge pull request 'add pgp pubkey to landing page, minor info changes' (#4) from chore/pgp into main
Reviewed-on: #4
2026-01-03 02:26:04 +01:00
forust e4e9d96a0b add pgp pubkey to landing page, minor info changes 2026-01-03 02:25:12 +01:00
forust 89576032b9 fix: update DNS settings for headscale 2026-01-03 01:39:05 +01:00
forust a9edfc0a25 fix: traefik middlewares for local and dev envs 2026-01-02 13:06:49 +01:00
forust acb8009307 Merge pull request 'feat: headscale implementation' (#3) from feat/tailscale into main
Reviewed-on: #3
2026-01-02 03:06:50 +01:00
forust 21f4e46028 chore: container name change for headscale web panel 2026-01-02 03:06:24 +01:00
forust 0234524635 feat: headscale implementation
- Headscale server
- Using goodieshq/headscale-admin as ui
- UI protected by security-chain
- Metrics available at /metrics
2026-01-02 01:29:51 +01:00
forust 26f5fb2fb9 Revert "feat: add gitea-basic-runner"
This reverts commit f0682319a7.
2026-01-02 00:38:04 +01:00
forust 122e6f6986 Revert "chore: gitea runners gitignored"
This reverts commit e0f7ab6561.
2026-01-02 00:37:47 +01:00
forust ce0bc4613a fix: headscale command 2026-01-01 03:19:45 +01:00
forust 8f4f460ff3 feat: add headscale server and UI configuration with Traefik routing 2026-01-01 02:39:28 +01:00
forust c048efd569 Merge branch 'main' of ssh://gitssh.forust.xyz:2221/forust/homelab into feat/tailscale 2026-01-01 01:45:57 +01:00
forust 1f1e13ff39 WIP: fix: update checkmk configuration for correct routing 2026-01-01 00:25:17 +01:00
forust c80a6c5351 Merge branch 'main' of ssh://gitssh.forust.xyz:2221/forust/homelab into feat/checkmk 2025-12-31 21:19:42 +01:00
forust 4fe3d660f1 Merge pull request 'Added DoT support on dns.forust.xyz' (#2) from fix/dns into main
Reviewed-on: #2
2025-12-31 03:44:08 +01:00
forust 9675eac2bf feat: add DoT support on dns.forust.xyz
- Made adguard available on adguard. and dns. subdomain
- DoT and DoH implementation complete
2025-12-31 03:41:38 +01:00
forust dc7fe64fbc fix: clean up traefik configuration and add redirect middleware 2025-12-30 22:44:52 +01:00
forust 502810a12e fix: update traefik router rules for DNS and adjust letsencrypt volume path 2025-12-30 17:20:13 +01:00
forust c047cc291d LE for DoH 2025-12-30 16:54:23 +01:00
forust f2b951673c fix: add traefik route for dns o https 2025-12-30 15:47:39 +01:00
forust 6b7c0df586 Merge pull request 'refactor: update network refs form "traefik-proxy" to "proxy"' (#1) from refactor/traefik-network into main
Reviewed-on: #1
2025-12-30 14:26:09 +01:00
forust fb2f420520 refactor: update network refs form "traefik-proxy" to "proxy"
- To allow testing dev vers of other services
2025-12-30 14:14:24 +01:00
forust 60c7d4ff17 feat: add mailer support and credentials for gitea 2025-12-28 23:35:31 +01:00
forust d20ec696a3 tcp router for gite ssh 2025-12-26 21:02:47 +01:00
forust c030b4cffa Merge branch 'main' into fix/gitea-ssh 2025-12-26 20:58:22 +01:00
forust e0f7ab6561 chore: gitea runners gitignored 2025-12-25 20:00:22 +01:00
forust f0682319a7 feat: add gitea-basic-runner 2025-12-25 19:58:38 +01:00
forust dbd3f36f76 kek 2025-12-25 19:54:32 +01:00
forust 4471da827c kek 2025-12-25 19:41:25 +01:00
forust 163ea867ce feat: downtify service, traefik integration 2025-12-20 02:00:10 +01:00
forust dc75dbaf7c chore: disable logging on pre-dormant period 2025-12-17 03:03:32 +01:00
forust db0f0f7bb6 feat: tailscale server 2025-12-14 02:43:00 +01:00
forust 5e4c60bb30 chore (security): port hardening, commented out non-critical ports 2025-12-14 02:42:46 +01:00
forust a699ceb935 refactor: userbots' compose-files cleanup 2025-12-09 21:45:24 +01:00
forust 17cae71952 refactor: userbots' compose-files cleanup 2025-12-09 21:45:24 +01:00
forust f4c695f95e feat: security-chain (authentik) on no-login services, SSO traefik dashboard 2025-12-09 15:14:42 +01:00
forust 6bdb16ad21 chore: whitespaces 2025-12-09 13:23:59 +01:00
forust 6eb62f41cc fix: authentik-traefik dns lookup 2025-12-09 13:23:48 +01:00
forust 586f0e3f7d feat: authenitk-only access for metube 2025-12-09 03:58:36 +01:00
forust 0e46193f53 chore: replace sec middlewares with sec chain 2025-12-09 03:57:13 +01:00
forust aed28ed15c refactor: sepparate traefik and authentik 2025-12-09 03:52:20 +01:00
forust f3b73bae11 feat: authentik service+envfile for traefik and authentik 2025-12-08 01:29:00 +01:00
forust be049cfa0d chore: gitignore checkmk's files 2025-12-08 00:34:08 +01:00
forust bfb21adff7 using local directory for storing chkmk data 2025-12-07 22:06:30 +01:00
forust 1c75382a6e fix: replace container_name to avoid misunderstandings 2025-12-07 04:14:56 +01:00
forust 9c5e037567 refactor: switch to official checkmk dockercompose 2025-12-07 04:10:32 +01:00
forust 4f9e7ea990 feat: nextcloud beta 2025-12-07 03:51:43 +01:00
forust 9f784d2c31 chore: gitignore checkmk's files 2025-12-07 02:59:18 +01:00
forust a07e27bff6 feat: checkmk service 2025-12-07 02:44:12 +01:00
forust c31369f2b7 add certfiles for xdfnx landing 2025-12-07 02:28:55 +01:00
forust 3bfcd6edf4 feat: add xdfnx's landing site 2025-12-07 02:17:19 +01:00
forust 504cbc81a0 fix: nextcloud traefik router parse error 2025-12-07 02:16:59 +01:00
forust aa7239ee83 refactor: traefik dynamic configuration 2025-12-07 01:39:58 +01:00
forust 6d9427cf2a chore: add traefik dev rule for main nextcloud 2025-12-06 02:34:19 +01:00
forust 70d60ed40a chore: add traefik dev rule for main nextcloud 2025-12-06 02:33:10 +01:00
forust aca6824309 chore: comments cleanup 2025-12-06 01:31:50 +01:00
forust d48a01775d fix: routers for nextcloud frontend 2025-12-06 00:38:16 +01:00
forust 99d50b43fe fix: traefik service 2025-12-05 21:00:55 +01:00
forust 23e955bde7 chore: new auth credentials, revert to info logging 2025-12-05 20:33:14 +01:00
forust 7d7a0e5c8a fix: penpot non- latin symbol 2025-12-05 18:53:19 +01:00
forust a767107277 fix: refering to file-defined middlewares 2025-12-05 14:36:57 +01:00
forust e68e37c285 refactor: use internal tld according to ICAAN 2025-12-05 04:30:51 +01:00
forust fc6a11397b refactor: move traefik configuration to docker-compose files via labels 2025-12-05 04:03:24 +01:00
forust d776124f26 chore: unignored examples, example env for penpot 2025-12-04 20:25:32 +01:00
forust 8a0ed85e7c feat: Penpoint service, traefik integration 2025-12-04 20:04:49 +01:00
forust 15f0f35ce4 chore: exclude prometheus config 2025-12-03 04:23:34 +01:00
forust 1ec145d4cf chore: add xdfnx domain to team links 2025-12-03 00:30:06 +01:00
forust 8b6815f314 chore: gitignored temp folder for references 2025-12-02 13:10:13 +01:00
forust 2d05a1911c feat: cloudflare IP sync (ddns) 2025-11-30 03:28:41 +01:00
forust 6f2f70ad09 chore: ingore streaming services 2025-11-28 12:59:15 +01:00
forust ce66a546f1 chore: update gitignore, add translations 2025-11-27 18:58:17 +01:00
forust 6cb49be951 chore: update gitignore, add translations 2025-11-27 18:58:17 +01:00
forust 3bcabcce4b feat: notify on new webinar, storing N last webinars in redis 2025-11-27 16:33:15 +01:00
forust a54b7b000f fix: add timeout for playwright to load page and debug response saving 2025-11-27 15:50:12 +01:00
forust 5b1fa11b5e chore: switching to latest release of portainer 2025-11-27 02:13:54 +01:00
forust 380288d103 fix: port conflicts 2025-11-26 22:55:16 +01:00
forust 8fae8bf75d feat: add admin commands to manage whitelist, extended webinar reporting 2025-11-26 22:25:47 +01:00
forust d4e0e7f37a feat: telegram webianr checker bot 2025-11-26 22:12:03 +01:00
forust cb92bff0c5 feat: basic webinar checker bot 2025-11-26 21:08:43 +01:00
forust bfbe841154 feat: redis integration 2025-11-26 20:53:53 +01:00
forust 7cbc5bf4f3 feat: edu phpsessid updater bot 2025-11-26 17:00:44 +01:00
forust 258ed04f98 feat: miku 2025-11-26 14:23:56 +01:00
forust 7ee0a1e7a2 feat: repos added to forust homepage 2025-11-26 11:51:09 +01:00
forust 739915c451 feat: team avatars 2025-11-26 01:20:12 +01:00
forust 68b5467a37 chore: exclude team images from project 2025-11-26 01:19:50 +01:00
forust 61e6b2b5c7 chore: reformat discord username to unified style 2025-11-26 01:01:37 +01:00
forust 272666d2fd fix: container url forust-homepage 2025-11-26 00:44:58 +01:00
forust aca485836b fix: service names 2025-11-26 00:42:10 +01:00
forust 02671b9117 feat: traefik dynamic files for forust-homepage 2025-11-26 00:10:35 +01:00
forust 1c1170ca96 refactor: change names for forust-homepage 2025-11-26 00:09:12 +01:00
forust 3eab3b254a Merge branch 'main' into landing 2025-11-26 00:02:37 +01:00
forust 5d9fc18ba5 feat: docker deployment 2025-11-26 00:01:43 +01:00
forust 03d39f8a32 chore: separate docker files from nginx 2025-11-25 23:59:15 +01:00
forust 7b60630d76 feat: new landing page matching xrock style 2025-11-25 23:48:12 +01:00
forust cc20d848f8 chore: vv pass 2025-11-25 19:29:12 +01:00
forust 1040e4fdf7 chore: termix-data to gitignore 2025-11-20 03:44:50 +01:00
forust 7af5af0630 fix: Return glance service 2025-11-18 15:42:51 +01:00
forust 8f9be8a478 fix: Misplaced comment 2025-11-18 15:20:53 +01:00
forust d1adaa54b1 feat: Add routers for local access 2025-11-18 15:19:27 +01:00
forust b3b0d5b553 refactor: sepparate traefik production routers 2025-11-18 15:13:45 +01:00
forust d22ef0cb44 feat: Add routers for local testing 2025-11-18 15:04:06 +01:00
forust 7c29d74c72 refactor: Removed routers from dynamic. Use .bak for references 2025-11-18 14:47:59 +01:00
forust b2ae170ea0 refactor: Sepparate traefik middlewares 2025-11-18 14:45:30 +01:00
forust 2c2474165b refactor: Sepparate traefik tls configuration 2025-11-18 14:43:49 +01:00
forust 155bb8d02b refactor: Sepparate traefik services 2025-11-18 14:41:41 +01:00
forust ed1b41ca1d feat: Add termix ssh manager, traefik routers 2025-11-18 03:34:55 +01:00
forust 9bb5f070e3 chore: gitignore for termix 2025-11-18 03:34:34 +01:00
forust b238e9ccf2 chore: Env example 2025-11-18 03:23:57 +01:00
forust 09a05a5ad5 chore: remove commented docker labels 2025-11-18 02:34:55 +01:00
forust 5b05207985 refactor: Move uptime-kuma traefik labels to dynamic.yaml 2025-11-18 02:30:42 +01:00
forust 319cf0ea1f feat: Add demo landing page 2025-11-18 02:12:48 +01:00
forust 53a0460476 Merge branch 'fix/uptime-kuma' into feat/kuma-uptime 2025-11-17 23:36:44 +01:00
forust 13340b6ddc chore: updated hostame from test 2025-11-17 23:35:31 +01:00
forust ed20fb6e2a fix: Uptime kuma on local traefik 2025-11-17 23:35:31 +01:00
forust 4fe36be5ea fix: update Uptime Kuma host rule in dynamic configuration 2025-11-17 23:35:31 +01:00
forust 6f8b780906 chore: updated hostame from test 2025-11-17 22:17:24 +01:00
forust 1f9a1c2c62 fix: Uptime kuma on local traefik 2025-11-17 22:16:27 +01:00
forust b20c012268 chore: Add uptime-kuma files to .gitignore, fixed gitea-db gitignore 2025-11-17 21:57:32 +01:00
forust b4bce72611 chore: Add uptime-kuma files to .gitignore, fixed gitea-db gitignore 2025-11-17 21:54:24 +01:00
forust e7d21bfe90 fix: update Uptime Kuma host rule in dynamic configuration 2025-11-17 21:50:14 +01:00
forust 0f75fe02c2 refactor: Move uptime kuma to respectful dir
Add container name for uptime-kuma
2025-11-17 21:45:51 +01:00
forust 2d895fe2bd fix: traefik network for uptime-kuma 2025-11-17 21:13:19 +01:00
forust 45b0859ab5 feat: Add uptime-kuma module on uptime subd 2025-11-17 21:04:22 +01:00
forust 45021933a6 feat: add LICENSE file with Mozilla Public License Version 2.0 2025-11-17 19:05:03 +01:00
forust d25570e293 chore: add replacements.txt to .gitignore for data security 2025-11-17 18:42:38 +01:00
forust 6843befac3 fix: temp labels for routing SSH for gitea 2025-11-16 00:01:35 +01:00
forust 0dfb09590d fix: added ssh domain 2025-11-15 23:55:02 +01:00
forust 625eb9561b testing: removed https:// from root url 2025-11-15 23:48:35 +01:00
forust b367b64879 fix: removed ssh port-forwarding for gitea 2025-11-15 23:42:03 +01:00
forust a30bda4940 Renamed userbot's dockercompose for easy access 2025-11-13 04:00:23 +01:00
forust b722467991 Remove unnecessary network and development configurations from userbot Docker Compose file 2025-11-13 03:48:50 +01:00
forust 5f8fd05266 Add userbot Docker Compose configuration for userbot_forust and userbot_anna services 2025-11-13 00:17:26 +01:00
forust 1c7afe5bb3 Move userbot Docker Compose configuration for forust and anna services 2025-11-11 01:57:44 +01:00
forust 4ff80c07b0 deleting that install shit 2025-11-11 01:41:24 +01:00
forust 3a5652daa7 userbot/install.sh, start 2025-11-11 01:39:53 +01:00
forust 4e18cd0eb3 init, .gitignore 2025-11-11 00:02:49 +01:00
332 changed files with 16453 additions and 7620 deletions
+191
View File
@@ -0,0 +1,191 @@
# Инструкция: Анализ хранилища Kubernetes и настройка NFS
## Цель
Проанализировать текущую конфигурацию хранилища Kubernetes и подготовить план внедрения NFS StorageClass для сохранения данных при удалении namespace.
## 1. Собрать информацию о кластере
### 1.1. Версия Kubernetes и тип дистрибутива
```bash
kubectl version --short
# или
kubectl version
```
Определить, используется ли k3s, k8s, microk8s и т.д.:
```bash
# Проверить наличие k3s
which k3s
# Проверить процесс
ps aux | grep -E 'kube|k3s'
```
### 1.2. StorageClass
```bash
kubectl get storageclass -o wide
```
Запомнить:
- `PROVISIONER` — какой драйвер используется
- `RECLAIMPOLICY` — Delete или Retain
- Какой StorageClass помечен как `(default)`
### 1.3. Существующие PV и PVC
```bash
kubectl get pv -o wide
kubectl get pvc --all-namespaces
```
Посмотреть, какие PVC привязаны к каким PV, и какой reclaimPolicy у PV.
### 1.4. Нода и диски
```bash
# Список нод
kubectl get nodes -o wide
# На каждой ноде (через ssh или локально):
lsblk
df -h
cat /etc/fstab
```
Определить:
- Есть ли отдельный раздел/диск для данных
- Куда смонтированы разделы
- Сколько свободного места
- Есть ли монтирование NTFS-разделов (как `/media/forust/Programs`)
### 1.5. Где local-path хранит данные (для k3s)
```bash
ls -la /var/lib/rancher/k3s/storage/ 2>/dev/null
# или для microk8s
ls -la /var/snap/microk8s/common/ 2>/dev/null
```
## 2. Анализ: сохраняются ли данные при удалении namespace?
| Сценарий | Результат |
|---|---|
| `kubectl delete ns <ns>` | Все PVC в namespace удаляются |
| PVC → PV c `reclaimPolicy: Delete` | PV и данные удалены |
| PVC → PV c `reclaimPolicy: Retain` | PV остаётся (статус Released), данные целы |
**Вывод:** Если reclaimPolicy в StorageClass = `Delete`, то данные **пропадут**. Если `Retain` — сохранятся.
## 3. План внедрения NFS
### 3.1. Проверить, установлен ли NFS
```bash
which nfsstat exportfs mount.nfs
systemctl status nfs-server 2>/dev/null || systemctl status nfs-kernel-server 2>/dev/null
```
### 3.2. Выбрать директорию для NFS-экспорта
Варианты (выбрать подходящий):
- `/var/lib/k8s-nfs/` — на корневом разделе
- `<путь к отдельному разделу>/k8s-nfs/` — если есть отдельный диск/раздел
- Не рекомендуется использовать NTFS-раздел (проблемы с правами и производительностью)
Требования:
- Файловая система: ext4 или xfs (не ntfs!)
- Достаточно свободного места
- Права: `755`, владелец root
### 3.3. Установить NFS-сервер
```bash
# Debian/Ubuntu
apt update && apt install -y nfs-kernel-server
# RHEL/Fedora
dnf install -y nfs-utils
```
### 3.4. Настроить экспорт
Создать директорию:
```bash
mkdir -p /var/lib/k8s-nfs
chmod 755 /var/lib/k8s-nfs
```
Добавить в `/etc/exports`:
```
/var/lib/k8s-nfs *(rw,sync,no_subtree_check,no_root_squash)
```
Применить:
```bash
exportfs -rav
```
Проверить:
```bash
showmount -e localhost
```
### 3.5. Выбрать способ интеграции с Kubernetes
#### Вариант A: nfs-subdir-external-provisioner (проще)
```bash
helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--namespace kube-system \
--set nfs.server=127.0.0.1 \
--set nfs.path=/var/lib/k8s-nfs \
--set storageClass.name=nfs \
--set storageClass.defaultClass=false \
--set storageClass.reclaimPolicy=Retain
```
#### Вариант B: NFS CSI Driver
```bash
helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs --namespace kube-system
```
После установки CSI драйвера создать StorageClass:
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs
provisioner: nfs.csi.k8s.io
parameters:
server: 127.0.0.1
share: /var/lib/k8s-nfs
reclaimPolicy: Retain
volumeBindingMode: Immediate
```
### 3.6. Проверить результат
```bash
kubectl get storageclass
kubectl get pods -n kube-system | grep -E 'nfs|provisioner'
```
## 4. Итоговая конфигурация
После внедрения в кластере будет два StorageClass:
| Имя | Provisioner | ReclaimPolicy | Назначение |
|---|---|---|---|
| `local-path` (default) | rancher.io/local-path | Delete | Временные данные, stateless |
| `nfs` | nfs-subdir-external-provisioner или nfs.csi.k8s.io | Retain | Данные, которые нужно сохранять |
**Главное преимущество:** PVC c `storageClassName: nfs` при удалении namespace сохраняют данные на диске, так как NFS-провизор использует `reclaimPolicy: Retain` или файлы физически остаются в NFS-экспорте.
## 5. Ответы на частые вопросы
**В:** Не упадёт ли local-path при установке NFS?
**О:** Нет, они независимы. local-path продолжает работать как обычно.
**В:** Данные NFS и local-path будут на одном диске?
**О:** Да, можно настроить оба на одном разделе, в разных каталогах.
**В:** Что если у меня несколько нод?
**О:** NFS сервер нужно поднять на одной ноде, а с других нод должна быть доступна шари. Для multi-node лучше использовать отдельный сервер или distributed storage (Longhorn, Rook/Ceph).
**В:** Можно ли использовать существующий NTFS-раздел для NFS?
**О:** Не рекомендуется — NTFS не поддерживает права Linux (no_root_squash не сработает корректно), возможны проблемы с блокировками и производительностью.
+24
View File
@@ -0,0 +1,24 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.{yml,yaml}]
indent_size = 2
[*.{json,jsonc}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.py]
indent_size = 4
[{Makefile,makefile}]
indent_style = tab
+70
View File
@@ -0,0 +1,70 @@
#===============================
# Basic auth credentials
#===============================
#
#
#
#===============================
#===============================================
#BEGIN TRAEFIK ENVIRONMENT VARIABLES ===========
#===============================================
#===============================================
# General Traefik Environment Variables
#===============================================
HOST=hostname
EMAIL=your@email.here
CF_DNS_API_TOKEN=API_TOKEN_HERE
CF_EMAIL=your_cloudflare@email.here
TZ=Europe/Berlin
#===============================================
# Dockmon Traefik Configuration File
#===============================================
DOCKMON_APPNAME=dockmon
DOCKMON_SUBDOMEN=dockmon
#===============================================
# Dashboard Traefik Environment Variables
#===============================================
DASHBOARD_APPNAME=traefik
DASHBOARD_SUBDOMEN=traefik
#===============================================
# Watercrawl Traefik Environment Variables
#===============================================
WATERCRAWL_APPNAME=watercrawl
WATERCRAWL_SUBDOMEN=watercrawl
#===============================================
# n8n Traefik Environment Variables
#===============================================
N8N_APPNAME=n8n
N8N_SUBDOMEN=n8n
#===============================================
# Glance Traefik Environment Variables
#===============================================
GLANCE_APPNAME=glance
GLANCE_SUBDOMEN=glance
#===============================================
# AdGuard Traefik Environment Variables
#===============================================
ADGUARD_APPNAME=adguard
ADGUARD_SUBDOMEN=adguard
#===============================================
# Portainer Traefik Environment Variables
#===============================================
PORTAINER_APPNAME=portainer
PORTAINER_SUBDOMEN=portainer
#===============================================
# Nextcloud Traefik Environment Variables
#===============================================
NEXTCLOUD_APPNAME=nextcloud
NEXTCLOUD_SUBDOMEN=nextcloud
#===============================================
# Aio Traefik Environment Variables
#===============================================
NEXTCLOUD_AIO_APPNAME=nextcloud-aio
NEXTCLOUD_AIO_SUBDOMEN=nextcloud-aio
# END OF TRAEFIK ENVIRONMENT VARIABLES
#===============================================
+326
View File
@@ -0,0 +1,326 @@
name: ci
on:
push:
branches:
- "**"
pull_request:
workflow_dispatch:
env:
REGISTRY: gcr.forust.xyz
jobs:
lint-prettier:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check formatting with Prettier
shell: bash
run: |
mapfile -t prettier_files < <(
git ls-files \
| grep -E '\.(md|json|ya?ml|html|css)$' \
| grep -Ev '^(\.docs/|\.zed/|errorpages/html/|homepages/(forust_files|xdfnx_files)/)'
)
if [ "${#prettier_files[@]}" -eq 0 ]; then
echo "No Prettier-managed files found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
node:22-alpine \
sh -lc 'npx --yes prettier@3 --check --ignore-unknown "$@"' sh "${prettier_files[@]}"
lint-ruff:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint Python with Ruff
shell: bash
run: |
docker run --rm \
-v "$PWD:/work" \
-w /work \
ghcr.io/astral-sh/ruff:latest \
check .
lint-yaml:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint YAML syntax
shell: bash
run: |
docker run --rm \
-v "$PWD:/work" \
-w /work \
cytopia/yamllint:latest \
-c .yamllint .
lint-dockerfiles:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint Dockerfiles
shell: bash
run: |
mapfile -t dockerfiles < <(
git ls-files ':(glob)**/Dockerfile' ':(glob)**/Dockerfile.*'
)
if [ "${#dockerfiles[@]}" -eq 0 ]; then
echo "No Dockerfiles found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
--entrypoint hadolint \
hadolint/hadolint:latest-debian \
-c .hadolint.yaml "${dockerfiles[@]}"
validate:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate Kubernetes manifests
shell: bash
run: |
mapfile -t manifests < <(
git ls-files ':(glob)**/k8s/**/*.yaml' ':(glob)**/k8s/**/*.yml' \
| grep -Ev '(^|/)(kustomization\.ya?ml|.*\.example\.ya?ml|.*values\.ya?ml|patch-.*\.ya?ml)$'
)
if [ "${#manifests[@]}" -eq 0 ]; then
echo "No Kubernetes manifests found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
ghcr.io/yannh/kubeconform:latest \
-strict \
-ignore-missing-schemas \
-summary \
"${manifests[@]}"
build:
needs: [lint-prettier, lint-ruff, lint-yaml, lint-dockerfiles, validate]
if: github.event_name != 'pull_request' && (github.ref_name == 'main' || github.ref_name == 'dev')
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changed docker-built services
id: services
shell: bash
run: |
base="${{ github.event.before }}"
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ]; then
base="$(git rev-list --max-parents=0 HEAD)"
fi
mapfile -t changed_files < <(git diff --name-only "$base" "${GITHUB_SHA}")
services=()
add_service() {
local name="$1"
local seen=0
for existing in "${services[@]}"; do
if [ "$existing" = "$name" ]; then
seen=1
break
fi
done
if [ "$seen" -eq 0 ]; then
services+=("$name")
fi
}
for file in "${changed_files[@]}"; do
case "$file" in
dtek_notif/*)
add_service dtek_notif
;;
errorpages/*)
add_service errorpages
;;
userbot/*)
add_service userbot
;;
homepages/*)
add_service homepages
;;
edu_master/phpsessid-bot/*|edu_master/webinar-checker/*|edu_master/compose.yaml)
add_service edu_master
;;
esac
done
if [ "${#services[@]}" -eq 0 ]; then
echo "No docker-built services changed."
echo "services=" >> "$GITHUB_OUTPUT"
exit 0
fi
printf '%s\n' "${services[@]}" | tee /tmp/services.txt
echo "services=$(paste -sd, /tmp/services.txt)" >> "$GITHUB_OUTPUT"
- name: Log in to registry
if: steps.services.outputs.services != ''
shell: bash
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY}" \
-u "${{ secrets.REGISTRY_USERNAME }}" \
--password-stdin
- name: Build and push changed images
if: steps.services.outputs.services != ''
shell: bash
run: |
IFS=, read -r -a services <<< "${{ steps.services.outputs.services }}"
for service in "${services[@]}"; do
case "$service" in
dtek_notif)
image="${REGISTRY}/forust/dtek-notif"
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" dtek_notif
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
;;
errorpages)
image="${REGISTRY}/forust/error-pages"
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" errorpages
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
;;
userbot)
image="${REGISTRY}/forust/userbot"
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" userbot
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
;;
homepages)
for service in forust xdfnx; do
case "$service" in
forust)
image="${REGISTRY}/forust/forust-homepage"
;;
xdfnx)
image="${REGISTRY}/forust/xdfnx-homepage"
;;
esac
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" -f "homepages/Dockerfile.${service}" homepages
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
done
;;
edu_master)
for service in session-keeper webinar-checker; do
case "$service" in
session-keeper)
context="edu_master/phpsessid-bot"
image="${REGISTRY}/forust/session-keeper"
;;
webinar-checker)
context="edu_master/webinar-checker"
image="${REGISTRY}/forust/webinar-checker"
;;
esac
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" "$context"
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
done
;;
esac
done
+326
View File
@@ -0,0 +1,326 @@
name: ci
on:
push:
branches:
- "**"
pull_request:
workflow_dispatch:
env:
REGISTRY: gcr.forust.xyz
jobs:
lint-prettier:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check formatting with Prettier
shell: bash
run: |
mapfile -t prettier_files < <(
git ls-files \
| grep -E '\.(md|json|ya?ml|html|css)$' \
| grep -Ev '^(\.docs/|\.zed/|errorpages/html/|homepages/(forust_files|xdfnx_files)/)'
)
if [ "${#prettier_files[@]}" -eq 0 ]; then
echo "No Prettier-managed files found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
node:22-alpine \
sh -lc 'npx --yes prettier@3 --check --ignore-unknown "$@"' sh "${prettier_files[@]}"
lint-ruff:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint Python with Ruff
shell: bash
run: |
docker run --rm \
-v "$PWD:/work" \
-w /work \
ghcr.io/astral-sh/ruff:latest \
check .
lint-yaml:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint YAML syntax
shell: bash
run: |
docker run --rm \
-v "$PWD:/work" \
-w /work \
cytopia/yamllint:latest \
-c .yamllint .
lint-dockerfiles:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Lint Dockerfiles
shell: bash
run: |
mapfile -t dockerfiles < <(
git ls-files ':(glob)**/Dockerfile' ':(glob)**/Dockerfile.*'
)
if [ "${#dockerfiles[@]}" -eq 0 ]; then
echo "No Dockerfiles found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
--entrypoint hadolint \
hadolint/hadolint:latest-debian \
-c .hadolint.yaml "${dockerfiles[@]}"
validate:
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate Kubernetes manifests
shell: bash
run: |
mapfile -t manifests < <(
git ls-files ':(glob)**/k8s/**/*.yaml' ':(glob)**/k8s/**/*.yml' \
| grep -Ev '(^|/)(kustomization\.ya?ml|.*\.example\.ya?ml|.*values\.ya?ml|patch-.*\.ya?ml)$'
)
if [ "${#manifests[@]}" -eq 0 ]; then
echo "No Kubernetes manifests found."
exit 0
fi
docker run --rm \
-v "$PWD:/work" \
-w /work \
ghcr.io/yannh/kubeconform:latest \
-strict \
-ignore-missing-schemas \
-summary \
"${manifests[@]}"
build:
needs: [lint-prettier, lint-ruff, lint-yaml, lint-dockerfiles, validate]
if: github.event_name != 'pull_request' && (github.ref_name == 'main' || github.ref_name == 'dev')
runs-on: [self-hosted, linux, arch, homelab]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect changed docker-built services
id: services
shell: bash
run: |
base="${{ github.event.before }}"
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ]; then
base="$(git rev-list --max-parents=0 HEAD)"
fi
mapfile -t changed_files < <(git diff --name-only "$base" "${GITHUB_SHA}")
services=()
add_service() {
local name="$1"
local seen=0
for existing in "${services[@]}"; do
if [ "$existing" = "$name" ]; then
seen=1
break
fi
done
if [ "$seen" -eq 0 ]; then
services+=("$name")
fi
}
for file in "${changed_files[@]}"; do
case "$file" in
dtek_notif/*)
add_service dtek_notif
;;
errorpages/*)
add_service errorpages
;;
userbot/*)
add_service userbot
;;
homepages/*)
add_service homepages
;;
edu_master/phpsessid-bot/*|edu_master/webinar-checker/*|edu_master/compose.yaml)
add_service edu_master
;;
esac
done
if [ "${#services[@]}" -eq 0 ]; then
echo "No docker-built services changed."
echo "services=" >> "$GITHUB_OUTPUT"
exit 0
fi
printf '%s\n' "${services[@]}" | tee /tmp/services.txt
echo "services=$(paste -sd, /tmp/services.txt)" >> "$GITHUB_OUTPUT"
- name: Log in to registry
if: steps.services.outputs.services != ''
shell: bash
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${REGISTRY}" \
-u "${{ secrets.REGISTRY_USERNAME }}" \
--password-stdin
- name: Build and push changed images
if: steps.services.outputs.services != ''
shell: bash
run: |
IFS=, read -r -a services <<< "${{ steps.services.outputs.services }}"
for service in "${services[@]}"; do
case "$service" in
dtek_notif)
image="${REGISTRY}/forust/dtek-notif"
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" dtek_notif
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
;;
errorpages)
image="${REGISTRY}/forust/error-pages"
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" errorpages
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
;;
userbot)
image="${REGISTRY}/forust/userbot"
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" userbot
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
;;
homepages)
for service in forust xdfnx; do
case "$service" in
forust)
image="${REGISTRY}/forust/forust-homepage"
;;
xdfnx)
image="${REGISTRY}/forust/xdfnx-homepage"
;;
esac
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" -f "homepages/Dockerfile.${service}" homepages
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
done
;;
edu_master)
for service in session-keeper webinar-checker; do
case "$service" in
session-keeper)
context="edu_master/phpsessid-bot"
image="${REGISTRY}/forust/session-keeper"
;;
webinar-checker)
context="edu_master/webinar-checker"
image="${REGISTRY}/forust/webinar-checker"
;;
esac
tags=("latest")
case "${GITHUB_REF_NAME}" in
main)
tags+=("main" "prod")
;;
dev)
tags+=("dev")
;;
esac
build_args=()
for tag in "${tags[@]}"; do
build_args+=(-t "${image}:${tag}")
done
docker build "${build_args[@]}" "$context"
for tag in "${tags[@]}"; do
docker push "${image}:${tag}"
done
done
;;
esac
done
+99 -19
View File
@@ -2,27 +2,107 @@
sync.ffs_lock
.sync.ffs_db
# Copyparty
*.hist/
# Volumes, configs and data directories
gitea/gitea-db/
gitea/gitea-data/*
n8n/n8n-data/*
n8n/n8n-node-data/*
adguardhome/conf/*
dockmon/data/*
portainer/portainer_data/*
metube/MeTube_downloads
uptime-kuma/data/
termix/termix-data/*
cfddns/config.json
checkmk/checkmk/*
downtify/Downtify_downloads
headscale/config/*
headscale/data/*
searxng/core-config/*
# Steaming services files
streaming/jellyfin/*
streaming/jellyseerr/*
streaming/sonarr/*
streaming/radarr/*
streaming/data/*
streaming/qbittorrent/*
streaming/prowlarr/*
# Homepage
homepages/forust_files/.well-known/*
# Traefik files
traefik/letsencrypt/acme.json
traefik/dynamic/fileservers.yml
traefik/dynamic/*.local.y*ml.*
traefik/dynamic/*.external.y*ml
traefik/k8s/fileservers.y*ml
traefik/logs/*
# SSL Certificates
adguardhome/certs/*
traefik/certs/*
certs/
# Monitoring
monitoring/prometheus.yml
# Python
.python-version
venv/
pyc
unknown_errors.txt
moonlogs.txt
thumb.jpg
antipm_pic.jpg
musicbot/
.trunk/
previous_profiles/
.python-version
/modules/__pycache__/
__pycache__/
*.session
*.session-old
*.db
*.sqlite3
*-journal
/venv/
.venv/
# DataSecurity
replacements.txt
# Vscode
.vscode
# Git
.gitattributes
# Gitea/github Runners
.runner
# Misc
.DS_Store
.idea
# Temp files
edu_master/temp/
temp/*
# Environment
.env
.env.anna
.env.forust
.env.*
!*example
# Volumes and data directories
gitea/gitea-db/*
gitea/gitea-data/*
n8n/n8n-data/*
n8n/n8n-node-data/*
adguardhome/data/*
dockmon/data/*
portainer/portainer_data/*
metube/MeTube_downloads
# Traefik files
traefik/letsencrypt/acme.json
traefik/logs/*
traefik/certs/*
# Python
.python-version
.venv/
venv/
# kubernetes
*/k8s/*secret*
!*/k8s/*secret*.example
traefik/k8s/local-tls.yaml
converters/k8s/config.yaml
convertx/k8s/config.yaml
+10
View File
@@ -0,0 +1,10 @@
ignored:
- DL3008
- DL3042
- DL3018
- DL3059
trustedRegistries:
- docker.io
- ghcr.io
- quay.io
- gcr.forust.xyz
+8
View File
@@ -0,0 +1,8 @@
{
"default": true,
"MD013": false,
"MD024": false,
"MD033": false,
"MD041": false,
"MD046": false
}
+7
View File
@@ -0,0 +1,7 @@
bracketSameLine: true
htmlWhitespaceSensitivity: css
printWidth: 120
tabWidth: 2
trailingComma: all
proseWrap: preserve
endOfLine: lf
+22
View File
@@ -0,0 +1,22 @@
extends: default
rules:
comments:
min-spaces-from-content: 1
comments-indentation: false
document-start: disable
line-length: disable
braces:
min-spaces-inside: 0
max-spaces-inside: 1
brackets:
min-spaces-inside: 0
max-spaces-inside: 1
indentation:
spaces: 2
indent-sequences: consistent
truthy:
allowed-values:
- "true"
- "false"
- "on"
+40
View File
@@ -0,0 +1,40 @@
{
"tab_size": 2,
"soft_wrap": "prefer_line",
"preferred_line_length": 120,
"format_on_save": "on",
"languages": {
"YAML": {
"tab_size": 2,
"hard_tabs": false,
"format_on_save": "on",
"formatter": {
"language_server": { "name": "yaml-language-server" },
},
},
"Python": {
"tab_size": 4,
"format_on_save": "on",
"language_servers": ["pyright", "ruff"],
"formatter": {
"language_server": { "name": "ruff" },
},
},
},
"lsp": {
"yaml-language-server": {
"settings": {
"yaml": {
"schemas": {
"kubernetes": ["**/k8s/*.yaml", "**/k8s/*.yml"],
},
"validate": true,
"completion": true,
"format": {
"enable": true,
},
},
},
},
},
}
+30 -10
View File
@@ -3,26 +3,46 @@ services:
image: adguard/adguardhome:latest
container_name: adguardhome
restart: unless-stopped
environment:
- TZ=${TZ}
ports:
- "53:53/tcp"
- "53:53/udp"
- "853:853/tcp" # DNS over TLS
# - "67:67/udp" # DHCP
# - "68:68/tcp" # DHCP
- "3000:3000/tcp"
# - "3000:3000/tcp"
volumes:
- ./data/work:/opt/adguardhome/work
- ./data/conf:/opt/adguardhome/conf
networks:
- traefik-proxy
- data:/opt/adguardhome/work
- ./conf:/opt/adguardhome/conf
- ./certs:/certs:ro
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-proxy"
- "traefik.http.services.adguard.loadbalancer.server.port=3000"
# Prod Router
- "traefik.http.routers.adguard.rule=Host(`dns.forust.xyz`) || Host(`adguard.forust.xyz`)"
- "traefik.http.routers.adguard.entrypoints=websecure"
- "traefik.http.routers.adguard.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.adguard-local.rule=Host(`adguard.workstation.internal`) || Host(`dns.workstation.internal`)"
- "traefik.http.routers.adguard-local.entrypoints=websecure"
- "traefik.http.routers.adguard-local.tls=true"
# Dev Router
- "traefik.http.routers.adguard-dev.rule=Host(`adguard.gigaforust.internal`) || Host(`dns.gigaforust.internal`)"
- "traefik.http.routers.adguard-dev.entrypoints=websecure"
- "traefik.http.routers.adguard-dev.tls=true"
# DoH Router
- "traefik.http.routers.dns-over-https.rule=(Host(`dns.forust.xyz` || Host(`adguard.forust.xyz`)) && PathPrefix(`/dns-query`))"
- "traefik.http.routers.dns-over-https.entrypoints=websecure"
- "traefik.http.routers.dns-over-https.tls.certresolver=letsencrypt"
# Glance Metadata
- glance.name=adguard
# - glance.icon=si:adguard
- glance.url=https://adguard.forust.xyz/
- glance.description=AdGuard Home is a network-wide software for blocking ads.
networks:
- proxy
volumes:
data:
networks:
traefik-proxy:
proxy:
external: true
+116
View File
@@ -0,0 +1,116 @@
apiVersion: v1
kind: Service
metadata:
name: adguard-lb-service
namespace: adguard
annotations:
metallb.io/loadBalancerIPs: "192.168.80.3"
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app: adguard
ports:
- name: dns-udp
port: 53
targetPort: 53
protocol: UDP
- name: dns-tcp
port: 53
targetPort: 53
protocol: TCP
- name: dot
port: 853
targetPort: 853
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: adguard-service
namespace: adguard
spec:
selector:
app: adguard
ports:
- port: 3000
name: webui
targetPort: 3000
- port: 53
name: dns
targetPort: 53
protocol: UDP
- port: 53
name: dns-tcp
targetPort: 53
protocol: TCP
- port: 853
name: dot
targetPort: 853
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: adguard-deployment
namespace: adguard
spec:
replicas: 1
selector:
matchLabels:
app: adguard
template:
metadata:
labels:
app: adguard
spec:
containers:
- name: adguard
image: adguard/adguardhome:latest
resources:
limits:
memory: "1.5Gi"
cpu: "300m"
requests:
memory: "500Mi"
cpu: "50m"
ports:
- containerPort: 3000
name: webui
- containerPort: 53
name: dns
- containerPort: 853
name: dot
volumeMounts:
- name: adguard-data
mountPath: /opt/adguardhome/work
subPath: work
- name: adguard-data
mountPath: /opt/adguardhome/conf
subPath: conf
- name: adguard-certs
mountPath: /certs
readOnly: true
volumes:
- name: adguard-data
persistentVolumeClaim:
claimName: adguard-pvc
- name: adguard-certs
secret:
secretName: adguard-certs
items:
- key: tls.crt
path: fullchain.pem
- key: tls.key
path: privkey.pem
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: adguard-pvc
namespace: adguard
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
+43
View File
@@ -0,0 +1,43 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: adguard-prod
namespace: adguard
spec:
entryPoints:
- websecure
routes:
- match: Host(`adguard.forust.xyz`) || Host(`dns.forust.xyz`)
kind: Rule
middlewares:
services:
- name: adguard-service
port: 3000
- match: (Host(`adguard.forust.xyz`) || Host(`dns.forust.xyz`)) && PathPrefix(`/dns-query`)
kind: Rule
middlewares:
services:
- name: adguard-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: adguard-local
namespace: adguard
spec:
entryPoints:
- websecure
routes:
- match: Host(`adguard.workstation.internal`) || Host(`dns.workstation.internal`) || Host(`adguard.gigaforust.internal`) || Host(`dns.gigaforust.internal`)
kind: Rule
services:
- name: adguard-service
port: 3000
- match: (Host(`adguard.workstation.internal`) || Host(`dns.workstation.internal`) || Host(`adguard.gigaforust.internal`) || Host(`dns.gigaforust.internal`)) && PathPrefix(`/dns-query`)
kind: Rule
services:
- name: adguard-service
port: 3000
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: adguard
+10
View File
@@ -0,0 +1,10 @@
kubectl apply -f k8s/namespace.yaml && \
kubectl create secret tls adguard-certs -n adguard \
--cert=certs/fullchain.pem \
--key=certs/privkey.pem --dry-run=client -o yaml > \
k8s/secrets.yaml
# OR WITH NO FILE CREATION:
kubectl create secret tls adguard-certs -n adguard \
--cert=certs/fullchain.pem --key=certs/privkey.pem \
--save-config
+20
View File
@@ -0,0 +1,20 @@
# ===================================
# Authentification app (authentik)
# PostgresQL conf
PG_PASS=change_this_cuz_its_ur_db_pass
PG_USER=authentik # it's okay
# Image Settings
AUTHENTIK_IMAGE=ghcr.io/goauthentik/server
AUTHENTIK_TAG=2025.10.2
# Networking
PORT_HTTP=9000
PORT_HTTPS=9443 # btw likely already used by portainer
AUTHENTIK_SECRET_KEY=super_secret_super_scary_authenik_key
AUTHENTIK_BOOTSTRAP_PASSWORD=pls_change_this
AUTHENTIK_ERROR_REPORTING__ENABLED=true # Or false to turn off
+95
View File
@@ -0,0 +1,95 @@
services:
postgresql:
image: docker.io/library/postgres:15-alpine
restart: unless-stopped
env_file:
- .env
environment:
POSTGRES_DB: ${PG_DB:-authentik}
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
healthcheck:
interval: 30s
retries: 5
start_period: 20s
test:
- CMD-SHELL
- pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}
timeout: 5s
volumes:
- database:/var/lib/postgresql/data
networks:
- authentik
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.2}
command: server
container_name: authentik-server
restart: unless-stopped
# ports:
# - ${PORT_HTTP:-9000}:9000
# - ${PORT_HTTPS:-9443}:9443
env_file:
- .env
environment:
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
volumes:
- ./media:/media
- ./custom-templates:/templates
labels:
- "traefik.enable=true"
- "traefik.http.services.authentik-server.loadbalancer.server.port=9000"
# Prod Router
- "traefik.http.routers.authentik-server.rule=Host(`auth.forust.xyz`)"
- "traefik.http.routers.authentik-server.entrypoints=websecure"
- "traefik.http.routers.authentik-server.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.authentik-server-local.rule=Host(`auth.workstation.internal`)"
- "traefik.http.routers.authentik-server-local.entrypoints=websecure"
- "traefik.http.routers.authentik-server-local.tls=true"
# Dev Router
- "traefik.http.routers.authentik-server-dev.rule=Host(`auth.gigaforust.internal`)"
- "traefik.http.routers.authentik-server-dev.entrypoints=websecure"
- "traefik.http.routers.authentik-server-dev.middlewares=security-headers@file"
- "traefik.http.routers.authentik-server-dev.tls=true"
networks:
- proxy
- authentik
depends_on:
postgresql:
condition: service_healthy
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.10.2}
restart: unless-stopped
user: root
command: worker
env_file:
- .env
environment:
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./media:/media
- ./certs:/certs
- ./custom-templates:/templates
networks:
- authentik
depends_on:
postgresql:
condition: service_healthy
volumes:
database:
driver: local
networks:
authentik:
proxy:
external: true
+93
View File
@@ -0,0 +1,93 @@
apiVersion: v1
kind: Service
metadata:
name: authentik-server-service
namespace: authentik
spec:
type: ClusterIP
selector:
app: authentik-server
ports:
- port: 9000
targetPort: 9000
---
apiVersion: v1
kind: Service
metadata:
name: authentik-worker-service
namespace: authentik
spec:
type: ClusterIP
selector:
app: authentik-worker
ports:
- port: 9000
targetPort: 9000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: authentik-server-deployment
namespace: authentik
spec:
replicas: 1
selector:
matchLabels:
app: authentik-server
template:
metadata:
labels:
app: authentik-server
spec:
containers:
- name: authentik-server
image: ghcr.io/goauthentik/server:2025.10.2
args: ["server"]
envFrom:
- configMapRef:
name: authentik-config
- secretRef:
name: authentik-secrets
ports:
- containerPort: 9000
resources:
requests:
memory: "700Mi"
cpu: "300m"
limits:
memory: "1.5Gi"
cpu: "1000m"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: authentik-worker-deployment
namespace: authentik
spec:
replicas: 1
selector:
matchLabels:
app: authentik-worker
template:
metadata:
labels:
app: authentik-worker
spec:
containers:
- name: authentik-worker
image: ghcr.io/goauthentik/server:2025.10.2
args: ["worker"]
securityContext:
runAsUser: 0
envFrom:
- configMapRef:
name: authentik-config
- secretRef:
name: authentik-secrets
resources:
requests:
memory: "512Mi"
cpu: "300m"
limits:
memory: "1Gi"
cpu: "700m"
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: authentik-config
namespace: authentik
data:
AUTHENTIK_IMAGE: ghcr.io/goauthentik/server
AUTHENTIK_TAG: "2025.10.2"
AUTHENTIK_POSTGRESQL__HOST: authentik-postgres-service
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_ERROR_REPORTING__ENABLED: "true"
+32
View File
@@ -0,0 +1,32 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: authentik-prod
namespace: authentik
spec:
entryPoints:
- websecure
routes:
- match: Host(`auth.forust.xyz`)
kind: Rule
middlewares:
services:
- name: authentik-server-service
port: 9000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: authentik-local
namespace: authentik
spec:
entryPoints:
- websecure
routes:
- match: Host(`auth.workstation.internal`) || Host(`auth.gigaforust.internal`)
kind: Rule
services:
- name: authentik-server-service
port: 9000
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: authentik
+66
View File
@@ -0,0 +1,66 @@
apiVersion: v1
kind: Service
metadata:
name: authentik-postgres-service
namespace: authentik
spec:
clusterIP: None
selector:
app: authentik-postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: authentik-postgres-statefulset
namespace: authentik
spec:
selector:
matchLabels:
app: authentik-postgres
serviceName: authentik-postgres-service
replicas: 1
template:
metadata:
labels:
app: authentik-postgres
spec:
containers:
- name: postgres
image: docker.io/library/postgres:15-alpine
env:
- name: POSTGRES_DB
value: authentik
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: authentik-secrets
key: AUTHENTIK_POSTGRESQL__USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: authentik-secrets
key: AUTHENTIK_POSTGRESQL__PASSWORD
ports:
- containerPort: 5432
name: postgres
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "500m"
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: authentik-secrets
namespace: authentik
type: Opaque
stringData:
AUTHENTIK_SECRET_KEY: ""
AUTHENTIK_POSTGRESQL__PASSWORD: ""
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_BOOTSTRAP_PASSWORD: authentik
+15
View File
@@ -0,0 +1,15 @@
CLOUDFLARE_API_TOKEN=YOUR_CLOUDFLARE_API_TOKEN
DOMAINS=example.com,dns.example.com,mc.example.com,auth.example.com,ssh.example.com
IP4_DOMAINS=
IP6_DOMAINS=
IP4_PROVIDER=cloudflare.trace
IP6_PROVIDER=none # change if you want to update AAAA
UPDATE_CRON=@every 5m
UPDATE_ON_START=true
DELETE_ON_STOP=false
DELETE_ON_FAILURE=true
TTL=1
PROXIED=!is(dns.example.com) && !is(mc.example.com) && !is(ssh.example.com)
EMOJI=true
UPTIMEKUMA=https://uptime-kuma.example.com/api/push/AsaSDFGFkfklaFALSKffkfFKfkfkfkFK?status=up&msg=OK&ping=
REJECT_CLOUDFLARE_IPS=true
+30
View File
@@ -0,0 +1,30 @@
services:
cloudflare-ddns:
image: timothyjmiller/cloudflare-ddns:latest
container_name: cloudflare-ddns
restart: unless-stopped
security_opt:
- no-new-privileges:true
network_mode: "host"
# https://github.com/timothymiller/cloudflare-ddns#-quick-start
environment:
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:?Cloudflare API token is required}
- DOMAINS=${DOMAINS:-}
- IP4_DOMAINS=${IP4_DOMAINS:-}
- IP6_DOMAINS=${IP6_DOMAINS:-}
- IP4_PROVIDER=${IP4_PROVIDER:-cloudflare.trace}
- IP6_PROVIDER=${IP6_PROVIDER:-none}
- UPDATE_CRON=${UPDATE_CRON:-@every 5m}
- UPDATE_ON_START=${UPDATE_ON_START:-true}
- DELETE_ON_STOP=${DELETE_ON_STOP:-false}
- DELETE_ON_FAILURE=${DELETE_ON_FAILURE:-true}
- TTL=${TTL:-1} # 1=auto
# to proxy only "dns.example.com" and "wfs.example.com" use "!is(dns.domain.com) && !is (wfs.domain.com)"
- PROXIED=${PROXIED:-true}
- EMOJI=${EMOJI:-true}
- UPTIMEKUMA=${UPTIMEKUMA:-}
- HEALTHCHECKS=${HEALTHCHECKS:-}
- REJECT_CLOUDFLARE_IPS=${REJECT_CLOUDFLARE_IPS:-true}
# volumes:
# Prefer using environment variables for configuration, config.json legacy support
# - ./config.json:/config.json
+22
View File
@@ -0,0 +1,22 @@
{
"cloudflare": [
{
"authentication": {
"api_token": "API_TOKEN"
},
"api_key": {
"api_key": "api_key_here",
"account_email": "your_email_here"
},
"zone_id": "your_zone-id",
"subdomains": [
{ "name": "", "proxied": true },
{ "name": "www", "proxied": true }
]
}
],
"a": true,
"aaaa": false,
"purgeUnknownRecords": false,
"ttl": 300
}
+1
View File
@@ -0,0 +1 @@
secret.yaml
+32
View File
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: cfddns
labels:
app: cfddns
spec:
replicas: 1
selector:
matchLabels:
app: cfddns
template:
metadata:
labels:
app: cfddns
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
containers:
- name: cloudflare-ddns
image: timothyjmiller/cloudflare-ddns:latest
imagePullPolicy: Always
resources:
requests:
memory: "20Mi"
cpu: "30m"
limits:
memory: "64Mi"
cpu: "50m"
envFrom:
- secretRef:
name: cfddns-secrets
+18
View File
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Secret
metadata:
name: cfddns-secrets
type: Opaque
stringData:
CLOUDFLARE_API_TOKEN: your_token
DOMAINS: "example.com,www.example.com"
IP4_PROVIDER: cloudflare.trace
IP6_PROVIDER: none
UPDATE_CRON: "@every 5m"
UPDATE_ON_START: "true"
DELETE_ON_STOP: "false"
DELETE_ON_FAILURE: "true"
TTL: "1"
PROXIED: "true"
EMOJI: "true"
REJECT_CLOUDFLARE_IPS: "true"
+2
View File
@@ -0,0 +1,2 @@
CMK_PASSWORD=password
TZ=Europe/Berlin
+39
View File
@@ -0,0 +1,39 @@
services:
checkmk:
image: "checkmk/check-mk-raw:2.4.0-latest"
container_name: "checkmk"
restart: unless-stopped
# ports:
# - 5000:5000
# - 6776:8000
volumes:
- sites:/omd/sites
tmpfs:
- /opt/omd/sites/cmk/tmp:uid=1000,gid=1000
environment:
- CMK_PASSWORD=${CMK_PASSWORD:-password}
- CMK_SITE_ID=cmk
- TZ=${TZ:-Etc/UTC}
labels:
- "traefik.enable=true"
- "traefik.http.services.checkmk.loadbalancer.server.port=5000"
# Prod Router
- "traefik.http.routers.checkmk.rule=Host(`cmk.forust.xyz`)"
- "traefik.http.routers.checkmk.entrypoints=websecure"
- "traefik.http.routers.checkmk.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.checkmk-local.rule=Host(`cmk.workstation.internal`)"
- "traefik.http.routers.checkmk-local.entrypoints=websecure"
- "traefik.http.routers.checkmk-local.tls=true"
# Dev Router
- "traefik.http.routers.checkmk-dev.rule=Host(`cmk.gigaforust.internal`)"
- "traefik.http.routers.checkmk-dev.entrypoints=websecure"
- "traefik.http.routers.checkmk-dev.tls=true"
networks:
- proxy
networks:
proxy:
external: true
volumes:
sites:
+68
View File
@@ -0,0 +1,68 @@
apiVersion: v1
kind: Service
metadata:
name: checkmk-service
namespace: checkmk
spec:
selector:
app: checkmk
ports:
- port: 5000
targetPort: 5000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkmk-deployment
namespace: checkmk
spec:
replicas: 1
selector:
matchLabels:
app: checkmk
template:
metadata:
labels:
app: checkmk
spec:
containers:
- name: checkmk
image: checkmk/check-mk-raw:2.4.0-latest
envFrom:
- secretRef:
name: checkmk-secrets
- configMapRef:
name: checkmk-config
ports:
- containerPort: 5000
volumeMounts:
- name: sites
mountPath: /omd/sites
- name: tmp
mountPath: /opt/omd/sites/cmk/tmp
resources:
requests:
memory: "2Gi"
cpu: "600m"
limits:
memory: "5Gi"
cpu: "4"
volumes:
- name: sites
persistentVolumeClaim:
claimName: checkmk-sites-pvc
- name: tmp
emptyDir:
medium: Memory
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: checkmk-sites-pvc
namespace: checkmk
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: checkmk-config
namespace: checkmk
data:
TZ: Europe/Bratislava
CMK_SITE_ID: cmk
+32
View File
@@ -0,0 +1,32 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: checkmk-prod
namespace: checkmk
spec:
entryPoints:
- websecure
routes:
- match: Host(`cmk.forust.xyz`)
kind: Rule
middlewares:
services:
- name: checkmk-service
port: 5000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: checkmk-local
namespace: checkmk
spec:
entryPoints:
- websecure
routes:
- match: Host(`cmk.workstation.internal`) || Host(`cmk.gigaforust.internal`)
kind: Rule
services:
- name: checkmk-service
port: 5000
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: checkmk
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: checkmk-secrets
namespace: checkmk
type: Opaque
stringData:
CMK_PASSWORD: "password"
+9
View File
@@ -0,0 +1,9 @@
ACCOUNT_REGISTRATION=false
HTTP_ALLOWED=false
ALLOW_UNAUTHENTICAED=false
AUTO_DELETE_EVERY_N_HOURS=24
WEBROOT=/convert
HIDE_HISTORY=false
LANGUAGE=en
UNAUTHED_USER_SHARING=false
MAX_CONVERT_PROCESS=0
+70
View File
@@ -0,0 +1,70 @@
services:
convertx:
container_name: convertx
image: ghcr.io/c4illin/convertx:latest
restart: unless-stopped
ports:
- "9992:3000"
# https://github.com/C4illin/ConvertX#environment-variables
environment:
- JWT_SECRET=$(JWT_SECRET)
- ACCOUNT_REGISTRATION=$(ACCOUNT_REGISTRATION:-false)
- HTTP_ALLOWED=$(HTTP_ALLOWED:-false)
- ALLOW_UNAUTHENTICATED=$(ALLOW_UNAUTHENTICATED:-false)
- AUTO_DELETE_EVERY_N_HOURS=$(AUTO_DELETE_EVERY_N_HOURS:-24)
- WEBROOT=$(WEBROOT)
- HIDE_HISTORY=$(HIDE_HISTORY:-false)
- LANGUAGE=$(LANGUAGE:-en)
- UNAUTHENTICATED_USER_SHARING=$(UNAUTHENTICATED_USER_SHARING:-false)
- MAX_CONVERT_PROCESS=$(MAX_CONVERT_PROCESS:-0)
labels:
- "traefik.enable=true"
- "traefik.http.services.convertx.loadbalancer.server.port=3000"
# Prod Router
- "traefik.http.routers.convertx.rule=(Host(`forust.xyz`) || Host(`www.forust.xyz`)) && PathPrefix(`/convert`)"
- "traefik.http.routers.convertx.entrypoints=websecure"
- "traefik.http.routers.convertx.priority=50"
- "traefik.http.routers.convertx.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.convertx-local.rule=Host(`workstation.internal`) && PathPrefix(`/convert`)"
- "traefik.http.routers.convertx-local.entrypoints=websecure"
- "traefik.http.routers.convertx-local.priority=50"
- "traefik.http.routers.convertx-local.tls=true"
# Dev Router
- "traefik.http.routers.convertx-dev.rule=Host(`gigaforust.internal`) && PathPrefix(`/convert`)"
- "traefik.http.routers.convertx-dev.entrypoints=websecure"
- "traefik.http.routers.convertx-dev.priority=50"
- "traefik.http.routers.convertx-dev.tls=true"
networks:
- proxy
volumes:
- data:/app/data
bentopdf:
container_name: bentopdf
image: bentopdf/bentopdf:latest
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.services.bentopdf.loadbalancer.server.port=8080"
# Prod router
- "traefik.http.routers.bentopdf.rule=Host(`pdf.forust.xyz`)"
- "traefik.http.routers.bentopdf.entrypoints=websecure"
- "traefik.http.routers.bentopdf.tls.certresolver=letsencrypt"
- "traefik.http.routers.bentopdf.tls=true"
# Local router
- "traefik.http.routers.bentopdf-local.rule=Host(`pdf.wokstation.internal`)"
- "traefik.http.routers.bentopdf-local.entrypoints=websecure"
- "traefik.http.routers.bentopdf-local.tls=true"
# Dev router
- "traefik.http.routers.bentopdf-dev.rule=Host(`pdf.gigaforust.internal`)"
- "traefik.http.routers.bentopdf-dev.entrypoints=websecure"
- "traefik.http.routers.bentopdf-dev.tls=true"
networks:
- proxy
networks:
proxy:
external: true
volumes:
data:
+42
View File
@@ -0,0 +1,42 @@
kind: Service
apiVersion: v1
metadata:
name: bentopdf-service
namespace: converters
spec:
selector:
app: bentopdf
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bentopdf-deployment
namespace: converters
spec:
replicas: 2
selector:
matchLabels:
app: bentopdf
template:
metadata:
labels:
app: bentopdf
spec:
containers:
- image: bentopdf/bentopdf:latest
imagePullPolicy: Always
name: bentopdf
ports:
- containerPort: 8080
resources:
requests:
memory: "50Mi"
cpu: "50m"
ephemeral-storage: "100Mi"
limits:
memory: "700Mi"
cpu: "700m"
ephemeral-storage: "5Gi"
+16
View File
@@ -0,0 +1,16 @@
# test manifest with docker and k8s config keys mismatch
apiVersion: v1
kind: ConfigMap
metadata:
name: convertx-config
namespace: converters
data:
ACCOUNT_REGISTRATION: "false"
HTTP_ALLOWED: "false"
ALLOW_UNAUTHENTICAED: "false"
AUTO_DELETE_EVERY_N_HOURS: "24"
WEBROOT: "/convert"
HIDE_HISTORY: "false"
LANGUAGE: "en"
UNAUTHED_USER_SHARING: "false"
MAX_CONVERT_PROCESS: "0"
+63
View File
@@ -0,0 +1,63 @@
apiVersion: v1
kind: Service
metadata:
name: convertx-service
namespace: converters
spec:
selector:
app: convertx
ports:
- port: 3000
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: convertx-deployment
namespace: converters
spec:
replicas: 1
selector:
matchLabels:
app: convertx
template:
metadata:
labels:
app: convertx
spec:
containers:
- image: ghcr.io/c4illin/convertx:latest
name: convertx
envFrom:
- configMapRef:
name: convertx-config
- secretRef:
name: convertx-secrets
ports:
- containerPort: 3000
volumeMounts:
- mountPath: /data
name: data
resources:
requests:
memory: "250Mi"
cpu: "100m"
limits:
cpu: "1500m"
memory: "1.5Gi"
volumes:
- name: data
persistentVolumeClaim:
claimName: convertx-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: convertx-pvc
namespace: converters
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
+65
View File
@@ -0,0 +1,65 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: convertx-prod
namespace: converters
spec:
entryPoints:
- websecure
routes:
- match: (Host(`forust.xyz`) || Host(`www.forust.xyz`)) && PathPrefix(`/convert`)
kind: Rule
priority: 50
services:
- name: convertx-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: convertx-local
namespace: converters
spec:
entryPoints:
- websecure
routes:
- match: (Host(`workstation.internal`) || Host(`gigaforust.internal`)) && PathPrefix(`/convert`)
kind: Rule
priority: 50
services:
- name: convertx-service
port: 3000
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: bentopdf-prod
namespace: converters
spec:
entryPoints:
- websecure
routes:
- match: Host(`pdf.forust.xyz`)
kind: Rule
services:
- name: bentopdf-service
port: 8080
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: bentopdf-local
namespace: converters
spec:
entryPoints:
- websecure
routes:
- match: Host(`pdf.workstation.internal`) || Host(`pdf.gigaforust.internal`)
kind: Rule
services:
- name: bentopdf-service
port: 8080
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: converters
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
kind: Secret
metadata:
name: convertx-secrets
namespace: converters
type: Opaque
stringData:
jwt-secret: ""
+27 -11
View File
@@ -3,28 +3,44 @@ services:
image: darthnorse/dockmon:latest
container_name: dockmon
restart: unless-stopped
ports:
- 8000:443
environment:
- TZ=Europe/Bratislava
# ports:
# - 8000:443
volumes:
- ./data:/app/data
- data:/app/data
- /var/run/docker.sock:/var/run/docker.sock
healthcheck:
test: ["CMD", "curl", "-k", "-f", "https://localhost:443/health"]
interval: 30s
timeout: 10s
retries: 3
networks:
- traefik-proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-proxy"
- "traefik.http.services.dockmon.loadbalancer.server.port=443"
- "traefik.http.services.dockmon.loadbalancer.server.scheme=https"
- "traefik.http.services.dockmon.loadbalancer.serverstransport=insecureTransport@file"
# Prod Router
- "traefik.http.routers.dockmon.rule=Host(`dockmon.forust.xyz`)"
- "traefik.http.routers.dockmon.entrypoints=websecure"
- "traefik.http.routers.dockmon.middlewares=security-headers@file"
- "traefik.http.routers.dockmon.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.dockmon-local.rule=Host(`dockmon.workstation.internal`)"
- "traefik.http.routers.dockmon-local.entrypoints=websecure"
- "traefik.http.routers.dockmon-local.tls=true"
# Dev Router
- "traefik.http.routers.dockmon-dev.rule=Host(`dockmon.gigaforust.internal`)"
- "traefik.http.routers.dockmon-dev.entrypoints=websecure"
- "traefik.http.routers.dockmon-dev.tls=true"
# Glance Metadata
- glance.name=dockmon
# - glance.icon=sh:dockmon
- glance.url=https://dockmon.forust.xyz/
- glance.description=Dockmon is a lightweight Docker container monitoring and management tool with a user-friendly web interface.
networks:
- proxy
volumes:
data:
networks:
traefik-proxy:
proxy:
external: true
+68
View File
@@ -0,0 +1,68 @@
apiVersion: v1
kind: Service
metadata:
name: dockmon-service
namespace: dockmon
spec:
clusterIP: None
selector:
app: dockmon
ports:
- port: 443
targetPort: 443
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: dockmon-statefulset
namespace: dockmon
spec:
serviceName: dockmon-service
replicas: 1
selector:
matchLabels:
app: dockmon
template:
metadata:
labels:
app: dockmon
spec:
containers:
- name: dockmon
image: darthnorse/dockmon:latest
ports:
- containerPort: 443
volumeMounts:
- name: data
mountPath: /app/data
- name: docker-sock
mountPath: /var/run/docker.sock
livenessProbe:
httpGet:
path: /health
port: 443
scheme: HTTPS
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 3
resources:
requests:
memory: "512Mi"
cpu: "200m"
limits:
memory: "1.5Gi"
cpu: "700m "
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
type: Socket
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
+43
View File
@@ -0,0 +1,43 @@
apiVersion: traefik.io/v1alpha1
kind: ServersTransport
metadata:
name: dockmon-transport
namespace: dockmon
spec:
insecureSkipVerify: true
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: dockmon-prod
namespace: dockmon
spec:
entryPoints:
- websecure
routes:
- match: Host(`dockmon.forust.xyz`)
kind: Rule
middlewares:
- name: security-headers@file
services:
- name: dockmon-service
port: 443
serversTransport: dockmon-transport
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: dockmon-local
namespace: dockmon
spec:
entryPoints:
- websecure
routes:
- match: Host(`dockmon.workstation.internal`) || Host(`dockmon.gigaforust.internal`)
kind: Rule
services:
- name: dockmon-service
port: 443
serversTransport: dockmon-transport
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: dockmon
+31
View File
@@ -0,0 +1,31 @@
services:
downtify:
container_name: downtify
image: ghcr.io/henriquesebastiao/downtify:latest
restart: unless-stopped
# ports:
# - '7077:8000'
volumes:
- ./Downtify_downloads:/downloads
labels:
- "traefik.enable=true"
- "traefik.http.services.downtify.loadbalancer.server.port=8000"
# Prod Router
- "traefik.http.routers.downtify.rule=Host(`downtify.forust.xyz`)"
- "traefik.http.routers.downtify.entrypoints=websecure"
- "traefik.http.routers.downtify.middlewares=security-chain@file"
- "traefik.http.routers.downtify.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.downtify-local.rule=Host(`downtify.workstation.internal`)"
- "traefik.http.routers.downtify-local.entrypoints=websecure"
- "traefik.http.routers.downtify-local.tls=true"
# Dev Router
- "traefik.http.routers.downtify-dev.rule=Host(`downtify.gigaforust.internal`)"
- "traefik.http.routers.downtify-dev.entrypoints=websecure"
- "traefik.http.routers.downtify-dev.tls=true"
networks:
- proxy
networks:
proxy:
external: true
+58
View File
@@ -0,0 +1,58 @@
apiVersion: v1
kind: Service
metadata:
name: downtify-service
namespace: downtify
spec:
selector:
app: downtify
ports:
- port: 8000
targetPort: 8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: downtify-deployment
namespace: downtify
spec:
replicas: 1
selector:
matchLabels:
app: downtify
template:
metadata:
labels:
app: downtify
spec:
containers:
- name: downtify
image: ghcr.io/henriquesebastiao/downtify:latest
ports:
- containerPort: 8000
volumeMounts:
- name: downloads
mountPath: /downloads
resources:
requests:
memory: "128Mi"
cpu: "200m"
limits:
memory: "1Gi"
cpu: "1"
volumes:
- name: downloads
persistentVolumeClaim:
claimName: downtify-downloads-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: downtify-downloads-pvc
namespace: downtify
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
+33
View File
@@ -0,0 +1,33 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: downtify-prod
namespace: downtify
spec:
entryPoints:
- websecure
routes:
- match: Host(`downtify.forust.xyz`)
kind: Rule
middlewares:
- name: security-chain@file
services:
- name: downtify-service
port: 8000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: downtify-local
namespace: downtify
spec:
entryPoints:
- websecure
routes:
- match: Host(`downtify.workstation.internal`) || Host(`downtify.gigaforust.internal`)
kind: Rule
services:
- name: downtify-service
port: 8000
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: downtify
+373
View File
@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
+2 -1
View File
@@ -3,10 +3,11 @@ services:
build:
context: .
dockerfile: Dockerfile
image: gcr.forust.xyz/forust/dtek-notif:latest
pull_policy: build
restart: unless-stopped
environment:
- TZ=Europe/Kyiv
dns:
- 1.1.1.1
- 8.8.8.8
+284 -314
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
EDU_LOGIN=your_edu_login_here
EDU_PASSWORD=your_edu_password_here
EDU_URL_LOGIN=https://edu.edu.vn.ua/user/login
EDU_URL_VERIFY=https://edu.edu.vn.ua/course/userlist
PHPSESSID_INTERVAL=10
USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"
WEBINAR_URL=https://edu.edu.vn.ua/webinar/useractive
WEBINAR_CHECK_INTERVAL=60
REDIS_HOST=redis
REDIS_PORT=6379
PLAYWRIGHT_WS=ws://playwright-service:3000/ws
WEBINAR_TELEGRAM_TOKEN=your_telegram_bot_token_here
WEBINAR_ADMIN_ID=123456789
-15
View File
@@ -1,15 +0,0 @@
ARG VERSION
# Use the official WaterCrawl image as the base image
FROM watercrawl/watercrawl:${VERSION:-v0.10.2}
# Set working directory
WORKDIR /var/www
# Copy the extra requirements file
COPY extra_requirements.txt /var/www/extra_requirements.txt
# Install any additional packages
RUN poetry run pip install -r /var/www/extra_requirements.txt
# The rest of the configuration is inherited from the base image
# The entrypoint and command should be defined in docker-compose.yml
@@ -1 +0,0 @@
# Add your additional Python packages here, one per line
+37 -250
View File
@@ -1,262 +1,49 @@
x-app: &app
build:
context: ./backend/
dockerfile: Dockerfile
args:
- VERSION=${VERSION:-v0.10.2}
depends_on:
db:
condition: service_healthy
dns:
- 8.8.8.8
- 1.1.1.1
environment:
- SECRET_KEY=${SECRET_KEY:-django-insecure-el4wo4a4--=f0+ag#omp@^w4eq^8v4(scda&1a(td_y2@=sh6&}
- API_ENCRYPTION_KEY=${API_ENCRYPTION_KEY:-8zSd6JIuC7ovfZ4AoxG_XmhubW6CPnQWW7Qe_4TD1TQ=}
- DEBUG=${DEBUG:-True}
- ALLOWED_HOSTS=${ALLOWED_HOSTS:-*}
- LANGUAGE_CODE=${LANGUAGE_CODE:-en-us}
- TIME_ZONE=${TIME_ZONE:-UTC}
- USE_I18N=${USE_I18N:-True}
- USE_TZ=${USE_TZ:-True}
- STATIC_ROOT=${STATIC_ROOT:-storage/static/}
- MEDIA_ROOT=${MEDIA_ROOT:-storage/media/}
- LOG_LEVEL=${LOG_LEVEL:-INFO}
- REDIS_URL=${REDIS_URL:-redis://redis:6379/1}
- DATABASE_URL=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@${POSTGRES_HOST:-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
- CELERY_BROKER_URL=${CELERY_BROKER_URL:-redis://redis:6379/0}
- CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND:-django-db}
- REDIS_LOCKER_URL=${REDIS_LOCKER_URL:-redis://redis:6379/3}
- MINIO_ENDPOINT=minio:9000
- MINIO_EXTERNAL_ENDPOINT=nginx
- MINIO_REGION=us-east-1
- MINIO_ACCESS_KEY=minio
- MINIO_SECRET_KEY=minio123
- MINIO_USE_HTTPS=False
- MINIO_EXTERNAL_ENDPOINT_USE_HTTPS=False
- MINIO_URL_EXPIRY_HOURS=7
- MINIO_PRIVATE_BUCKET=private
- MINIO_PUBLIC_BUCKET=public
- CSRF_TRUSTED_ORIGINS=${CSRF_TRUSTED_ORIGINS:-}
- CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-}
- CORS_ALLOWED_ORIGIN_REGEXES=${CORS_ALLOWED_ORIGIN_REGEXES:-}
- CORS_ALLOW_ALL_ORIGINS=${CORS_ALLOW_ALL_ORIGINS:-False}
- FRONTEND_URL=${FRONTEND_URL:-http://localhost}
- IS_LOGIN_ACTIVE=${IS_LOGIN_ACTIVE:-True}
- IS_SIGNUP_ACTIVE=${IS_SIGNUP_ACTIVE:-True}
- IS_GITHUB_LOGIN_ACTIVE=${IS_GITHUB_LOGIN_ACTIVE:-True}
- IS_GOOGLE_LOGIN_ACTIVE=${IS_GOOGLE_LOGIN_ACTIVE:-True}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
- ACCESS_TOKEN_LIFETIME_MINUTES=${ACCESS_TOKEN_LIFETIME_MINUTES:-5}
- REFRESH_TOKEN_LIFETIME_DAYS=${REFRESH_TOKEN_LIFETIME_DAYS:-30}
- EMAIL_BACKEND=${EMAIL_BACKEND:-django.core.mail.backends.smtp.EmailBackend}
- EMAIL_HOST=${EMAIL_HOST:-}
- EMAIL_PORT=${EMAIL_PORT:-587}
- EMAIL_USE_TLS=${EMAIL_USE_TLS:-True}
- EMAIL_HOST_USER=${EMAIL_HOST_USER:-}
- EMAIL_HOST_PASSWORD=${EMAIL_HOST_PASSWORD:-}
- DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL:-}
- SCRAPY_USER_AGENT=${SCRAPY_USER_AGENT:-WaterCrawl/0.1 (+https://github.com/watercrawl/watercrawl)}
- SCRAPY_ROBOTSTXT_OBEY=${SCRAPY_ROBOTSTXT_OBEY:-True}
- SCRAPY_CONCURRENT_REQUESTS=${SCRAPY_CONCURRENT_REQUESTS:-16}
- SCRAPY_DOWNLOAD_DELAY=${SCRAPY_DOWNLOAD_DELAY:-0}
- SCRAPY_CONCURRENT_REQUESTS_PER_DOMAIN=${SCRAPY_CONCURRENT_REQUESTS_PER_DOMAIN:-4}
- SCRAPY_CONCURRENT_REQUESTS_PER_IP=${SCRAPY_CONCURRENT_REQUESTS_PER_IP:-4}
- SCRAPY_COOKIES_ENABLED=${SCRAPY_COOKIES_ENABLED:-False}
- SCRAPY_HTTPCACHE_ENABLED=${SCRAPY_HTTPCACHE_ENABLED:-True}
- SCRAPY_HTTPCACHE_EXPIRATION_SECS=${SCRAPY_HTTPCACHE_EXPIRATION_SECS:-3600}
- SCRAPY_HTTPCACHE_DIR=${SCRAPY_HTTPCACHE_DIR:-httpcache}
- SCRAPY_LOG_LEVEL=${SCRAPY_LOG_LEVEL:-ERROR}
- SCRAPY_GOOGLE_API_KEY=${SCRAPY_GOOGLE_API_KEY:-}
- SCRAPY_GOOGLE_CSE_ID=${SCRAPY_GOOGLE_CSE_ID:-}
- SCRAPY_MAX_NUMBER_OF_SITEMAP_URLS=${SCRAPY_MAX_NUMBER_OF_SITEMAP_URLS:-20000}
- SCRAPY_SITEMAP_CRAWL_PAGE_LIMIT=${SCRAPY_SITEMAP_CRAWL_PAGE_LIMIT:-100}
- PLAYWRIGHT_SERVER=${PLAYWRIGHT_SERVER:-http://playwright:8000}
- PLAYWRIGHT_API_KEY=${PLAYWRIGHT_API_KEY:-your-secret-api-key}
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY:-}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET:-}
- GOOGLE_ANALYTICS_ID=${GOOGLE_ANALYTICS_ID:-}
- IS_ENTERPRISE_MODE_ACTIVE=${IS_ENTERPRISE_MODE_ACTIVE:-False}
- MAX_CRAWL_DEPTH=${MAX_CRAWL_DEPTH:--1}
- CAPTURE_USAGE_HISTORY=${CAPTURE_USAGE_HISTORY:-True}
- MCP_SERVER=${MCP_SERVER:-http://localhost/sse}
networks:
- traefik-proxy
- default
- n8n
x-frontend: &frontend
image: watercrawl/frontend:${VERSION:-v0.10.2}
environment:
- VITE_API_BASE_URL=${API_BASE_URL:-http://localhost/api}
depends_on:
- app
services:
nginx:
image: nginx:alpine
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf.template
- ./nginx/entrypoint.sh:/entrypoint.sh
environment:
- MINIO_PRIVATE_BUCKET=${MINIO_PRIVATE_BUCKET:-private}
- MINIO_PUBLIC_BUCKET=${MINIO_PUBLIC_BUCKET:-public}
command: ["/bin/sh", "/entrypoint.sh"]
depends_on:
- app
- frontend
- minio
restart: unless-stopped
networks:
- traefik-proxy
- n8n
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-proxy"
app:
<<: *app
command: [ "gunicorn", "-b", "0.0.0.0:9000", "-w", "2", "watercrawl.wsgi:application", "--access-logfile", "-", "--error-logfile", "-", "--timeout", "60" ]
celery:
<<: *app
command: [ "celery", "-A", "watercrawl", "worker", "-l", "info", "-S", "django" ]
dns:
- 1.1.1.1
- 8.8.8.8
celery-beat:
<<: *app
command: [ "celery", "-A", "watercrawl", "beat", "-l", "info", "-S", "django" ]
frontend:
<<: *frontend
command: [ "npm", "run", "serve" ]
minio:
image: minio/minio:RELEASE.2024-11-07T00-52-20Z
redis:
image: redis:alpine
restart: unless-stopped
volumes:
- ./volumes/minio-data:/data
command: server /data --console-address ":9001"
environment:
- MINIO_BROWSER_REDIRECT_URL=${MINIO_BROWSER_REDIRECT_URL:-http://localhost/minio-console/}
- MINIO_SERVER_URL=${MINIO_SERVER_URL:-http://localhost/}
- MINIO_ROOT_USER=${MINIO_ACCESS_KEY:-minio}
- MINIO_ROOT_PASSWORD=${MINIO_SECRET_KEY:-minio123}
playwright:
image: watercrawl/playwright:1.1
restart: unless-stopped
user: root
environment:
- AUTH_API_KEY=${PLAYWRIGHT_API_KEY:-your-secret-api-key}
- PORT=${PLAYWRIGHT_PORT:-8000}
- HOST=${PLAYWRIGHT_HOST:-0.0.0.0}
dns:
- 8.8.8.8
- 1.1.1.1
networks:
- traefik-proxy
- n8n
db:
image: postgres:17.2-alpine3.21
restart: unless-stopped
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_DB=${POSTGRES_DB:-postgres}
volumes:
- ./volumes/postgres-db:/var/lib/postgresql/data
- redis-data:/data
healthcheck:
test: [ "CMD-SHELL", "pg_isready" ]
interval: 10s
timeout: 5s
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
mcp:
image: watercrawl/mcp:v1.2.0
playwright-service:
image: mcr.microsoft.com/playwright:v1.56.0-jammy
restart: unless-stopped
command: [ "sse", "--base-url", "http://app:9000", '--port', '3000', '--endpoint', '/sse' ]
networks:
- n8n
command: npx -y playwright@1.56.0 run-server --port 3000 --path /ws
session-keeper:
build: ./phpsessid-bot
image: gcr.forust.xyz/forust/session-keeper:latest
pull_policy: build
env_file: .env
restart: unless-stopped
depends_on:
redis:
image: redis:latest
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "redis-cli -h redis EXISTS EDU_PHPSESSID | grep -q 1"]
interval: 30s
timeout: 5s
retries: 10
start_period: 60s
webinar-checker:
build: ./webinar-checker
image: gcr.forust.xyz/forust/webinar-checker:latest
pull_policy: build
env_file: .env
restart: unless-stopped
llm:
image: ollama/ollama:latest
restart: unless-stopped
volumes:
- ./volumes/ollama-models:/root/.ollama
environment:
- OLLAMA_DISABLE_TELEMETRY=true
- OLLAMA_KEEP_ALIVE=5m
- OLLAMA_HOST=0.0.0.0:11434
- OLLAMA_NUM_PARALLEL=1
- OLLAMA_MAX_LOADED_MODELS=1
dns:
- 1.1.1.1
- 8.8.8.8
networks:
- n8n
# docker exec -it edu_master-llm-1 ollama pull neural-chat:7b-q4
# docker exec -it edu_master-llm-1 ollama pull mistral:7b-q4
# lessons-bot:
# build:
# context: edu_master/lessons_bot/
# dockerfile: Dockerfile
# restart: unless-stopped
# environment:
# - LESSONS_BOT_TOKEN=${LESSONS_BOT_TOKEN}
# - N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://n8n:5678/webhook-test/get-lessons}
# - N8N_SECRET=${N8N_SECRET:-your-secret-token-here}
# - WATERCRAWL_API_URL=${WATERCRAWL_API_URL:-http://app:9000/api}
# - PHPSESSID_BOT_URL=${PHPSESSID_BOT_URL:-http://phpsessid-bot:5000}
# - EDU_HOST=${EDU_HOST:-edu.edu.vn.ua}
# depends_on:
# - n8n
# - app
# - phpsessid-bot
# dns:
# - 1.1.1.1
# - 8.8.8.8
# networks:
# - default
phpsessid-bot:
build:
context: ./phpsessid_bot/
dockerfile: Dockerfile
restart: unless-stopped
environment:
- EDU_HOST=${EDU_HOST:-edu.edu.vn.ua}
- EDU_LOGIN=${EDU_LOGIN}
- EDU_PASSWORD=${EDU_PASSWORD}
- BOT_PORT=${PHPSESSID_BOT_PORT:-5000}
- BOT_HOST=${PHPSESSID_BOT_HOST:-0.0.0.0}
dns:
- 1.1.1.1
- 8.8.8.8
networks:
- default
depends_on:
redis:
condition: service_healthy
session-keeper:
condition: service_healthy
playwright-service:
condition: service_started
volumes:
n8n_data:
postgres-db:
minio-data:
ollama-models:
lmstudio_data:
networks:
traefik-proxy:
external: true
redis-data:
-16
View File
@@ -1,16 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
# Установка зависимостей
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копирование кода
COPY config.py .
COPY utils.py .
COPY handlers.py .
COPY main.py .
# Запуск бота
CMD ["python", "-u", "main.py"]
-26
View File
@@ -1,26 +0,0 @@
import os
from dotenv import load_dotenv
load_dotenv()
# Telegram
BOT_TOKEN = os.getenv('LESSONS_BOT_TOKEN')
# n8n
N8N_WEBHOOK_URL = os.getenv('N8N_WEBHOOK_URL', 'http://n8n:5678/webhook/homework-check')
N8N_SECRET = os.getenv('N8N_SECRET', 'your-secret-token-here')
# WaterCrawl API
WATERCRAWL_API_URL = os.getenv('WATERCRAWL_API_URL', 'http://app:9000/api')
# PHPSESSID Bot
PHPSESSID_BOT_URL = os.getenv('PHPSESSID_BOT_URL', 'http://phpsessid-bot:5000')
# EDU site
EDU_HOST = os.getenv('EDU_HOST', 'edu.edu.vn.ua')
EDU_WEBINAR_URL = f'https://{EDU_HOST}/webinar/useractive'
# Playwright
PLAYWRIGHT_SERVER = os.getenv('PLAYWRIGHT_SERVER', 'http://playwright:8000')
PLAYWRIGHT_API_KEY = os.getenv('PLAYWRIGHT_API_KEY', 'your-secret-api-key')
WEBINAR_WAIT_TIME = int(os.getenv('WEBINAR_WAIT_TIME', '3')) # Секунды ожидания загрузки
-131
View File
@@ -1,131 +0,0 @@
import logging
import requests
from telegram import Update
from telegram.ext import ContextTypes
import config
from utils import fetch_webinars, format_webinar_message
logger = logging.getLogger(__name__)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Команда /start"""
welcome_message = """
Привет! Я бот для проверки домашних заданий и вебинаров.
<b>Команды:</b>
/check - Проверить несделанные уроки
/webinar - Проверить активные онлайн уроки
/help - Помощь
"""
await update.message.reply_text(welcome_message, parse_mode='HTML')
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Команда /help"""
help_text = """
<b>Как пользоваться ботом:</b>
<b>/check</b> - Проверка домашних заданий
- Поиск несделанных уроков
⏱ Проверка занимает 10-30 секунд
<b>/webinar</b> - Активные онлайн уроки
- Проверка активных вебинаровв
⏱ Проверка занимает 3-5 секунд
"""
await update.message.reply_text(help_text, parse_mode='HTML')
async def check_homework(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Команда /check - запускает проверку уроков"""
chat_id = update.effective_chat.id
user_id = update.effective_user.id
username = update.effective_user.username or "unknown"
# Отправляем уведомление что начали работу
status_message = await update.message.reply_text("Запускаю проверку уроков...")
# Формируем данные для n8n
payload = {
"chat_id": chat_id,
"user_id": user_id,
"username": username,
"timestamp": update.message.date.isoformat()
}
headers = {
"Authorization": f"Bearer {config.N8N_SECRET}",
"Content-Type": "application/json"
}
try:
logger.info(f"Sending request to n8n for user {user_id}")
# Отправляем запрос в n8n
response = requests.post(
config.N8N_WEBHOOK_URL,
json=payload,
headers=headers,
timeout=5 # Короткий таймаут т.к. это асинхронный запрос
)
if response.status_code == 200:
await status_message.edit_text(
"✅ Запрос принят!\n"
"🔄 Парсинг сайта и анализ данных...\n"
"⏱ Это займет 10-30 секунд"
)
logger.info(f"Request accepted for user {user_id}")
else:
await status_message.edit_text(
f"Ошибка при отправке запроса. Функция в разработке\n"
f"Код: {response.status_code}"
)
logger.error(f"n8n returned status {response.status_code}")
except requests.Timeout:
await status_message.edit_text("⏱ Запрос обрабатывается (таймаут соединения)")
logger.warning(f"Timeout for user {user_id}")
except Exception as e:
await status_message.edit_text(f"❌ Ошибка: {str(e)}")
logger.error(f"Error for user {user_id}: {e}", exc_info=True)
async def check_webinar(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Команда /webinar - проверяет активные онлайн уроки"""
user_id = update.effective_user.id
# Отправляем уведомление что начали работу
status_message = await update.message.reply_text("Проверяю активные вебинары...")
try:
logger.info(f"Checking webinars for user {user_id}")
# Получаем список вебинаров
webinars = fetch_webinars()
if webinars is None:
await status_message.edit_text(
"❌ Не удалось получить информацию о вебинарах\n"
"Попробуйте позже или обратитесь к администратору\n"
"|@MrForust|mr.forust| Либо же прямо сюда."
)
logger.error(f"Failed to fetch webinars for user {user_id}")
return
# Форматируем и отправляем результат
message = format_webinar_message(webinars)
await status_message.edit_text(message, parse_mode='HTML', disable_web_page_preview=True)
logger.info(f"Webinar check completed for user {user_id}: found {len(webinars)} webinars")
except Exception as e:
await status_message.edit_text(f"❌ Ошибка: {str(e)}")
logger.error(f"Error checking webinars for user {user_id}: {e}", exc_info=True)
async def error_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик ошибок"""
logger.error(f"Update {update} caused error {context.error}", exc_info=context.error)
-42
View File
@@ -1,42 +0,0 @@
import logging
from telegram import Update
from telegram.ext import Application, CommandHandler
import config
from handlers import start, help_command, check_homework, check_webinar, error_handler
# Настройка логирования
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
def main():
"""Запуск бота"""
if not config.BOT_TOKEN:
logger.error("LESSONS_BOT_TOKEN not set!")
return
# Создаем приложение
application = Application.builder().token(config.BOT_TOKEN).build()
# Регистрируем обработчики команд
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("check", check_homework))
application.add_handler(CommandHandler("webinar", check_webinar))
# Регистрируем обработчик ошибок
application.add_error_handler(error_handler)
# Запускаем бота
logger.info("Lessons Bot started!")
logger.info(f"PHPSESSID Bot URL: {config.PHPSESSID_BOT_URL}")
logger.info(f"n8n Webhook URL: {config.N8N_WEBHOOK_URL}")
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == '__main__':
main()
-5
View File
@@ -1,5 +0,0 @@
python-telegram-bot==20.7
requests==2.31.0
beautifulsoup4==4.12.2
python-dotenv==1.0.0
lxml==4.9.3
-258
View File
@@ -1,258 +0,0 @@
import logging
import requests
from bs4 import BeautifulSoup
from typing import Optional, Dict, List
import config
logger = logging.getLogger(__name__)
def get_phpsessid() -> Optional[str]:
"""
Получает валидный PHPSESSID через phpsessid-bot
Returns:
str: PHPSESSID или None в случае ошибки
"""
try:
url = f"{config.PHPSESSID_BOT_URL}/get-session"
logger.info(f"Requesting PHPSESSID from {url}")
response = requests.post(url, timeout=10)
if response.status_code == 200:
data = response.json()
if data.get('success'):
phpsessid = data.get('phpsessid')
logger.info(f"Got PHPSESSID: {phpsessid[:10]}...")
return phpsessid
else:
logger.error(f"Failed to get PHPSESSID: {data.get('error')}")
return None
else:
logger.error(f"PHPSESSID bot returned status {response.status_code}")
return None
except Exception as e:
logger.error(f"Error getting PHPSESSID: {e}", exc_info=True)
return None
def parse_webinar_table(html_content: str) -> List[Dict[str, str]]:
"""
Парсит таблицу с вебинарами
Args:
html_content: HTML контент страницы
Returns:
List[Dict]: Список вебинаров или пустой список
"""
try:
soup = BeautifulSoup(html_content, 'html.parser')
# Находим таблицу с вебинарами
meetings_div = soup.find('div', {'id': 'meetings'})
if not meetings_div:
logger.warning("meetings div not found")
return []
table = meetings_div.find('table', {'class': 'table table-zebra'})
if not table:
logger.warning("table not found")
return []
tbody = table.find('tbody')
if not tbody:
logger.warning("tbody not found")
return []
rows = tbody.find_all('tr')
if not rows:
return []
# Проверяем на сообщение "Жодного онлайн уроку зараз"
first_row = rows[0]
td = first_row.find('td')
if td and 'Жодного онлайн уроку зараз' in td.get_text(strip=True):
logger.info("No webinars available")
return []
# Парсим активные вебинары
webinars = []
for row in rows:
tds = row.find_all('td')
if len(tds) >= 4:
webinar = {
'topic': tds[0].get_text(strip=True),
'course': tds[1].get_text(strip=True),
'teacher': tds[2].get_text(strip=True),
'join_link': tds[3].find('a')['href'] if tds[3].find('a') else ''
}
webinars.append(webinar)
logger.info(f"Parsed {len(webinars)} webinars")
return webinars
except Exception as e:
logger.error(f"Error parsing webinar table: {e}", exc_info=True)
return []
def fetch_webinars_with_playwright() -> Optional[List[Dict[str, str]]]:
"""
Получает список активных вебинаров используя Playwright для динамического контента
Returns:
List[Dict]: Список вебинаров или None в случае ошибки
"""
# Получаем PHPSESSID
phpsessid = get_phpsessid()
if not phpsessid:
logger.error("Failed to get PHPSESSID")
return None
try:
# Подготавливаем cookies для Playwright
cookies = [
{
'name': 'PHPSESSID',
'value': phpsessid,
'domain': config.EDU_HOST,
'path': '/'
}
]
# Запрос к Playwright серверу
playwright_request = {
'url': config.EDU_WEBINAR_URL,
'cookies': cookies,
'wait_until': 'networkidle', # Ждем пока сеть успокоится
'wait_time': config.WEBINAR_WAIT_TIME * 1000, # Дополнительное ожидание в миллисекундах
'user_agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
}
headers = {
'Authorization': f'Bearer {config.PLAYWRIGHT_API_KEY}',
'Content-Type': 'application/json'
}
logger.info(f"Fetching webinars via Playwright from {config.EDU_WEBINAR_URL}")
logger.info(f"Will wait {config.WEBINAR_WAIT_TIME} seconds for dynamic content")
response = requests.post(
f"{config.PLAYWRIGHT_SERVER}/render",
json=playwright_request,
headers=headers,
timeout=30
)
if response.status_code != 200:
logger.error(f"Playwright server returned status {response.status_code}")
logger.error(f"Response: {response.text}")
return None
result = response.json()
html_content = result.get('html', '')
if not html_content:
logger.error("No HTML content in Playwright response")
return None
# Парсим таблицу
webinars = parse_webinar_table(html_content)
return webinars
except Exception as e:
logger.error(f"Error fetching webinars via Playwright: {e}", exc_info=True)
return None
def fetch_webinars() -> Optional[List[Dict[str, str]]]:
"""
Получает список активных вебинаров
Сначала пробует через Playwright (для динамического контента),
при неудаче - через обычный requests
Returns:
List[Dict]: Список вебинаров или None в случае ошибки
"""
# Пробуем через Playwright
logger.info("Attempting to fetch via Playwright for dynamic content")
webinars = fetch_webinars_with_playwright()
if webinars is not None:
return webinars
# Fallback на обычный requests
logger.warning("Playwright fetch failed, falling back to simple requests")
# Получаем PHPSESSID
phpsessid = get_phpsessid()
if not phpsessid:
logger.error("Failed to get PHPSESSID")
return None
# Запрашиваем страницу с вебинарами
try:
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'ru-RU,ru;q=0.9,uk;q=0.8',
'Referer': f'https://{config.EDU_HOST}/'
}
cookies = {
'PHPSESSID': phpsessid
}
logger.info(f"Fetching webinars from {config.EDU_WEBINAR_URL}")
response = requests.get(
config.EDU_WEBINAR_URL,
headers=headers,
cookies=cookies,
timeout=15
)
if response.status_code != 200:
logger.error(f"Failed to fetch webinars page: {response.status_code}")
return None
# Парсим таблицу
webinars = parse_webinar_table(response.text)
return webinars
except Exception as e:
logger.error(f"Error fetching webinars: {e}", exc_info=True)
return None
def format_webinar_message(webinars: List[Dict[str, str]]) -> str:
"""
Форматирует список вебинаров для отправки в Telegram
Args:
webinars: Список вебинаров
Returns:
str: Отформатированное сообщение
"""
if not webinars:
return "📭 Жодного онлайн уроку зараз"
message = "🎓 <b>Активні онлайн уроки:</b>\n\n"
for i, webinar in enumerate(webinars, 1):
message += f"<b>{i}. {webinar['topic']}</b>\n"
message += f"📚 Курс: {webinar['course']}\n"
message += f"👨‍🏫 Вчитель: {webinar['teacher']}\n"
if webinar['join_link']:
full_link = webinar['join_link']
if not full_link.startswith('http'):
full_link = f"https://{config.EDU_HOST}{webinar['join_link']}"
message += f"🔗 <a href='{full_link}'>Увійти до уроку</a>\n"
message += "\n"
return message
-8
View File
@@ -1,8 +0,0 @@
#!/bin/sh
set -e
# Replace environment variables in the Nginx configuration template
envsubst '${MINIO_PRIVATE_BUCKET} ${MINIO_PUBLIC_BUCKET}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
# Start Nginx
exec nginx -g 'daemon off;'
-87
View File
@@ -1,87 +0,0 @@
server {
listen 80;
server_name localhost;
client_max_body_size 100M;
# Frontend
location / {
proxy_pass http://frontend:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API
location /api/ {
proxy_pass http://app:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# MCP
location ~ ^/(sse|messages) {
proxy_pass http://mcp:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Important SSE settings
proxy_http_version 1.1;
proxy_set_header Connection "";
# Disable buffering so events are sent immediately
proxy_buffering off;
proxy_cache off;
# Increase timeouts so connection stays open
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
# MinIO private bucket
location /${MINIO_PRIVATE_BUCKET}/ {
proxy_pass http://minio:9000/${MINIO_PRIVATE_BUCKET}/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
# MinIO public bucket
location /${MINIO_PUBLIC_BUCKET}/ {
proxy_pass http://minio:9000/${MINIO_PUBLIC_BUCKET}/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
}
# MinIO API - for direct S3 operations
# location /minio/api/ {
# proxy_pass http://minio:9000/;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_buffering off;
# }
# MinIO Console
location /minio-console/ {
proxy_pass http://minio:9001/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Rewrite location headers
proxy_redirect / /minio-console/;
}
}
+15
View File
@@ -0,0 +1,15 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends redis-tools && rm -rf /var/lib/apt/lists/*
# Install dependencies
RUN pip install --no-cache-dir requests==2.32.3 redis==5.2.1
# Copy application code
COPY . .
# Run the bot
CMD ["python", "bot.py"]
+132
View File
@@ -0,0 +1,132 @@
import logging
import os
import time
from datetime import datetime
import redis
import requests
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Load configuration (adapted to .env keys)
def _env(key, default=None):
v = os.getenv(key, default)
if isinstance(v, str) and len(v) >= 2 and ((v[0] == '"' and v[-1] == '"') or (v[0] == "'" and v[-1] == "'")):
return v[1:-1]
return v
LOGIN = _env('KEEPER_LOGIN')
PASSWORD = _env('KEEPER_PASSWORD')
EDU_BASE = _env('EDU_URL_BASE', 'https://edu.edu.vn.ua')
EDU_LOGIN_PATH = _env('EDU_URL_LOGIN', '/user/login')
EDU_COURSES_PATH = _env('EDU_URL_COURSES', '/course/userlist')
URL_LOGIN = f'{EDU_BASE.rstrip("/")}/{EDU_LOGIN_PATH.lstrip("/")}'
URL_VERIFY = f'{EDU_BASE.rstrip("/")}/{EDU_COURSES_PATH.lstrip("/")}'
INTERVAL = int(_env('KEEPER_INTERVAL', 10))
USER_AGENT = _env(
'USER_AGENT',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36',
)
REDIS_HOST = _env('REDIS_HOST', 'redis')
REDIS_PORT = int(_env('REDIS_PORT', 6379))
SUCCESS_FILE = '/tmp/last_success' # noqa: S108
def touch_success_file():
"""Updates the timestamp of the success file for healthchecks."""
try:
with open(SUCCESS_FILE, 'w') as f:
f.write(str(datetime.now().timestamp()))
except Exception as e:
logger.error(f'Failed to touch success file: {e}')
def main():
logger.info('Starting Session Keeper Bot')
# Connect to Redis
try:
redis_client = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
redis_client.ping()
logger.info(f'Connected to Redis at {REDIS_HOST}:{REDIS_PORT}')
except Exception as e:
logger.error(f'Failed to connect to Redis: {e}')
return
session = requests.Session()
# Set headers
headers = {
'User-Agent': USER_AGENT,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language': 'en-US,en;q=0.9',
'Cache-Control': 'max-age=0',
'Upgrade-Insecure-Requests': '1',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?1',
'Sec-Fetch-Dest': 'document',
'Sec-Ch-Ua': '"Not_A Brand";v="99", "Chromium";v="142"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Linux"',
'Accept-Encoding': 'gzip, deflate, br',
'Priority': 'u=0, i',
}
session.headers.update(headers)
while True:
try:
logger.info('Attempting login...')
# Login payload
payload = {'login': LOGIN, 'password': PASSWORD}
# Perform Login
# Note: The user request shows a POST to /user/login with form data
# We need to make sure we handle the PHPSESSID correctly.
# If we already have a PHPSESSID, requests will send it.
login_response = session.post(URL_LOGIN, data=payload, allow_redirects=True)
logger.info(f'Login Response Status: {login_response.status_code}')
logger.info(f'Cookies after login: {session.cookies.get_dict()}')
# Verify Session
logger.info('Verifying session...')
verify_response = session.get(URL_VERIFY, allow_redirects=False)
logger.info(f'Verify Response Status: {verify_response.status_code}')
if verify_response.status_code == 200:
logger.info('Session verification SUCCESS (200 OK).')
touch_success_file()
# Save PHPSESSID to Redis
phpsessid = session.cookies.get('PHPSESSID')
if phpsessid:
try:
redis_client.set('EDU_PHPSESSID', phpsessid)
logger.info(f'Saved PHPSESSID to Redis: {phpsessid}')
except Exception as e:
logger.error(f'Failed to save PHPSESSID to Redis: {e}')
elif verify_response.status_code == 302:
logger.warning('Session verification FAILED (302 Redirect). Session might be invalid.')
else:
logger.warning(f'Session verification returned unexpected status: {verify_response.status_code}')
except Exception as e:
logger.error(f'An error occurred: {e}')
logger.info(f'Sleeping for {INTERVAL} minutes...')
time.sleep(INTERVAL * 60)
if __name__ == '__main__':
main()
-15
View File
@@ -1,15 +0,0 @@
FROM python:3.11-slim
WORKDIR /app
# Устанавливаем зависимости
RUN pip install --no-cache-dir flask requests
# Копируем код бота
COPY main.py .
# Открываем порт
EXPOSE 5000
# Запускаем бот
CMD ["python", "-u", "main.py"]
-216
View File
@@ -1,216 +0,0 @@
import os
import logging
from flask import Flask, request, jsonify
import requests
from datetime import datetime
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
# Конфигурация из переменных окружения
EDU_HOST = os.getenv('EDU_HOST', 'edu.edu.vn.ua')
EDU_LOGIN = os.getenv('EDU_LOGIN', '')
EDU_PASSWORD = os.getenv('EDU_PASSWORD', '')
BOT_PORT = int(os.getenv('BOT_PORT', '5000'))
BOT_HOST = os.getenv('BOT_HOST', '0.0.0.0')
# Кэш для хранения актуальной сессии
session_cache = {
'phpsessid': None,
'expires_at': None
}
def login_and_get_session():
"""
Выполняет логин и возвращает новый PHPSESSID
"""
url = f"https://{EDU_HOST}/user/login"
headers = {
'Cache-Control': 'max-age=0',
'Sec-Ch-Ua': '"Chromium";v="141", "Not?A_Brand";v="8"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Linux"',
'Accept-Language': 'ru-RU,ru;q=0.9',
'Origin': f'https://{EDU_HOST}',
'Content-Type': 'application/x-www-form-urlencoded',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-User': '?1',
'Sec-Fetch-Dest': 'document',
'Referer': f'https://{EDU_HOST}/',
'Accept-Encoding': 'gzip, deflate, br',
'Priority': 'u=0, i'
}
data = {
'login': EDU_LOGIN,
'password': EDU_PASSWORD
}
try:
logger.info(f"Attempting login to {url}")
response = requests.post(
url,
data=data,
headers=headers,
allow_redirects=False,
timeout=10
)
# Получаем PHPSESSID из cookies
phpsessid = response.cookies.get('PHPSESSID')
if phpsessid:
logger.info(f"Login successful, got PHPSESSID: {phpsessid[:10]}...")
return {
'success': True,
'phpsessid': phpsessid,
'status_code': response.status_code
}
else:
logger.warning(f"Login failed: no PHPSESSID in response. Status: {response.status_code}")
return {
'success': False,
'error': 'No PHPSESSID in response',
'status_code': response.status_code
}
except requests.exceptions.RequestException as e:
logger.error(f"Login request failed: {str(e)}")
return {
'success': False,
'error': str(e)
}
def validate_phpsessid(phpsessid):
"""
Проверяет валидность существующего PHPSESSID
"""
url = f"https://{EDU_HOST}/"
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
}
cookies = {
'PHPSESSID': phpsessid
}
try:
response = requests.get(url, headers=headers, cookies=cookies, timeout=10)
# Проверяем, не редиректит ли на страницу логина
is_valid = response.status_code == 200 and '/user/login' not in response.url
return {
'valid': is_valid,
'status_code': response.status_code,
'url': response.url
}
except requests.exceptions.RequestException as e:
logger.error(f"Validation request failed: {str(e)}")
return {
'valid': False,
'error': str(e)
}
@app.route('/health', methods=['GET'])
def health():
"""Health check endpoint"""
return jsonify({'status': 'ok', 'timestamp': datetime.now().isoformat()})
@app.route('/get-session', methods=['POST', 'GET'])
def get_session():
"""
Основной endpoint для получения валидного PHPSESSID
Возвращает кэшированную сессию или создает новую
"""
result = login_and_get_session()
if result['success']:
session_cache['phpsessid'] = result['phpsessid']
session_cache['last_updated'] = datetime.now().isoformat()
return jsonify({
'success': True,
'phpsessid': result['phpsessid'],
'timestamp': datetime.now().isoformat()
})
else:
return jsonify({
'success': False,
'error': result.get('error', 'Login failed'),
'timestamp': datetime.now().isoformat()
}), 400
@app.route('/validate-session', methods=['POST'])
def validate_session():
"""
Проверяет валидность переданного PHPSESSID
"""
data = request.get_json() or {}
phpsessid = data.get('phpsessid') or request.args.get('phpsessid')
if not phpsessid:
return jsonify({
'success': False,
'error': 'PHPSESSID not provided'
}), 400
validation_result = validate_phpsessid(phpsessid)
return jsonify({
'success': True,
'valid': validation_result.get('valid', False),
'details': validation_result,
'timestamp': datetime.now().isoformat()
})
@app.route('/refresh-session', methods=['POST', 'GET'])
def refresh_session():
"""
Принудительно обновляет сессию
"""
result = login_and_get_session()
if result['success']:
return jsonify({
'success': True,
'phpsessid': result['phpsessid'],
'message': 'Session refreshed successfully',
'timestamp': datetime.now().isoformat()
})
else:
return jsonify({
'success': False,
'error': result.get('error', 'Failed to refresh session'),
'timestamp': datetime.now().isoformat()
}), 400
if __name__ == '__main__':
if not EDU_LOGIN or not EDU_PASSWORD:
logger.error("EDU_LOGIN and EDU_PASSWORD must be set!")
exit(1)
logger.info(f"Starting PHPSESSID validator bot on {BOT_HOST}:{BOT_PORT}")
logger.info(f"Target host: {EDU_HOST}")
app.run(host=BOT_HOST, port=BOT_PORT, debug=False)
@@ -1,3 +0,0 @@
flask==3.0.0
requests==2.31.0
Werkzeug==3.0.1
+10
View File
@@ -0,0 +1,10 @@
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
RUN pip install --no-cache-dir pip==25.0.1 && pip install --no-cache-dir playwright==1.56.0 redis==5.2.1 requests==2.32.3 "python-telegram-bot[job-queue]==21.10"
COPY checker.py .
CMD ["python", "checker.py"]
+965
View File
@@ -0,0 +1,965 @@
import contextlib
import json
import logging
import os
import re
import time
from datetime import datetime, timedelta
import redis
from playwright.async_api import async_playwright
from telegram import ChatMember, InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ChatType
from telegram.ext import Application, CallbackQueryHandler, CommandHandler, ContextTypes
# Logger
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Suppress HTTP request logs
logging.getLogger('urllib3').setLevel(logging.WARNING)
logging.getLogger('httpx').setLevel(logging.WARNING)
logging.getLogger('telegram.ext._application').setLevel(logging.WARNING)
# Load environment variables
def _env(key, default=None):
v = os.getenv(key, default)
if isinstance(v, str) and len(v) >= 2 and ((v[0] == '"' and v[-1] == '"') or (v[0] == "'" and v[-1] == "'")):
return v[1:-1]
return v
EDU_BASE = _env('EDU_URL_BASE', 'https://edu.edu.vn.ua')
EDU_WEBINAR_PATH = _env('EDU_URL_WEBINAR', '/webinar/useractive')
WEBINAR_URL = f'{EDU_BASE.rstrip("/")}/{EDU_WEBINAR_PATH.lstrip("/")}'
DIARY_URL = f'{EDU_BASE.rstrip("/")}/user/diary'
WEBINAR_CHECK_INTERVAL = int(_env('WEBINAR_CHECK_INTERVAL', 60))
REDIS_HOST = _env('REDIS_HOST', 'redis')
REDIS_PORT = int(_env('REDIS_PORT', 6379))
PLAYWRIGHT_WS = _env('PLAYWRIGHT_WS', 'ws://playwright-service:3000/ws')
USER_AGENT = _env(
'USER_AGENT',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36',
)
WEBINAR_TELEGRAM_TOKEN = _env('WEBINAR_TELEGRAM_TOKEN')
ADMIN_ID = int(_env('WEBINAR_ADMIN_ID', '0'))
# Redis Keys
KEY_WHITELIST = 'bot:whitelist'
KEY_WHITELIST_ENABLED = 'bot:whitelist_enabled'
KEY_SUBSCRIBERS = 'bot:subscribers'
KEY_PHPSESSID = 'EDU_PHPSESSID'
KEY_WEBINAR_HISTORY = 'bot:webinar_history' # Stores last 3 webinars
# Initialize Redis
try:
redis_client = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
redis_client.ping()
logger.info(f'Connected to Redis at {REDIS_HOST}:{REDIS_PORT}')
except Exception as e:
logger.error(f'Failed to connect to Redis: {e}')
exit(1)
# --- Translations ---
TRANSLATIONS = {
'ru': {
'welcome': '👋 Привет, {name}!\n\nЯ бот-уведомитель о вебинарах. Я буду сообщать вам, когда появится новый вебинар.\nВы подписаны на уведомления.',
'welcome_admin': '\n\n👑 <b>Режим администратора активен</b>',
'access_denied': '⛔ Доступ запрещен. Вас нет в белом списке.',
'help_title': '🤖 <b>Помощь по боту</b>\n\n',
'help_commands': '/start - Подписаться на уведомления\n/stop - Отписаться от уведомлений\n/help - Показать это сообщение\n/language - Сменить язык',
'help_admin': '\n<b>Команды администратора:</b>\n/adduser [user_id] - Добавить пользователя в белый список\n/removeuser [user_id] - Удалить пользователя из белого списка\nИли используйте панель ниже для управления настройками.',
'admin_only': '⛔ Только для администратора!',
'user_added': '✅ Пользователь {user_id} добавлен в белый список',
'user_removed': '✅ Пользователь {user_id} удален из белого списка',
'user_not_in_whitelist': '⚠️ Пользователь {user_id} не был в белом списке',
'cannot_remove_admin': '❌ Невозможно удалить администратора из белого списка',
'invalid_user_id': '❌ Неверный ID пользователя. Должно быть число.',
'usage_adduser': 'Использование: /adduser [user_id]',
'usage_removeuser': 'Использование: /removeuser [user_id]',
'whitelist_enabled': '✅ Белый список включен',
'whitelist_disabled': '✅ Белый список отключен',
'whitelist_title': '📋 <b>Белый список:</b>\n',
'subscribers_title': '👥 <b>Подписчики:</b>\n',
'empty': 'Пусто',
'force_check_running': '🔄 Запускаю проверку...',
'check_failed': '❌ Проверка не удалась. Смотрите логи.',
'check_completed_none': '✅ Проверка завершена. Вебинаров не найдено.',
'check_completed': '✅ Проверка завершена. Найдено {count} вебинар(ов)!',
'toggle_whitelist_disable': '🔒 Отключить белый список',
'toggle_whitelist_enable': '🔓 Включить белый список',
'view_whitelist': '📋 Посмотреть белый список',
'view_subscribers': '👥 Посмотреть подписчиков',
'force_check': '🔄 Принудительная проверка',
'webinar_found': '🎓 <b>Новый вебинар!</b>\n\n',
'webinar_item': '📌 <b>{name}</b>\n🔗 https://edu.edu.vn.ua{url}',
'select_language': '🌐 <b>Выберите язык / Оберіть мову / Select language:</b>',
'language_changed': '✅ Язык изменен на русский',
'flag_ru': '🇷🇺 Русский',
'flag_uk': '🇺🇦 Українська',
'flag_en': '🇬🇧 English',
'history_cleared': '✅ История вебинаров очищена',
'history_clear_failed': '❌ Ошибка при очистке истории',
},
'uk': {
'welcome': "👋 Привіт, {name}!\n\nЯ бот-сповіщувач про вебінари. Я повідомлятиму вас, коли з'явиться новий вебінар.\nВи підписані на сповіщення.",
'welcome_admin': '\n\n👑 <b>Режим адміністратора активний</b>',
'access_denied': '⛔ Доступ заборонено. Вас немає в білому списку.',
'help_title': '🤖 <b>Довідка по боту</b>\n\n',
'help_commands': '/start - Підписатися на сповіщення\n/stop - Відписатися від сповіщень\n/help - Показати це повідомлення\n/language - Змінити мову',
'help_admin': '\n<b>Команди адміністратора:</b>\n/adduser [user_id] - Додати користувача до білого списку\n/removeuser [user_id] - Видалити користувача з білого списку\nАбо використовуйте панель нижче для керування налаштуваннями.',
'admin_only': '⛔ Тільки для адміністратора!',
'user_added': '✅ Користувач {user_id} доданий до білого списку',
'user_removed': '✅ Користувач {user_id} видалений з білого списку',
'user_not_in_whitelist': '⚠️ Користувач {user_id} не був у білому списку',
'cannot_remove_admin': '❌ Неможливо видалити адміністратора з білого списку',
'invalid_user_id': '❌ Невірний ID користувача. Має бути число.',
'usage_adduser': 'Використання: /adduser [user_id]',
'usage_removeuser': 'Використання: /removeuser [user_id]',
'whitelist_enabled': '✅ Білий список увімкнено',
'whitelist_disabled': '✅ Білий список вимкнено',
'whitelist_title': '📋 <b>Білий список:</b>\n',
'subscribers_title': '👥 <b>Підписники:</b>\n',
'empty': 'Порожньо',
'force_check_running': '🔄 Запускаю перевірку...',
'check_failed': '❌ Перевірка не вдалася. Дивіться логи.',
'check_completed_none': '✅ Перевірка завершена. Вебінарів не знайдено.',
'check_completed': '✅ Перевірка завершена. Знайдено {count} вебінар(ів)!',
'toggle_whitelist_disable': '🔒 Вимкнути білий список',
'toggle_whitelist_enable': '🔓 Увімкнути білий список',
'view_whitelist': '📋 Переглянути білий список',
'view_subscribers': '👥 Переглянути підписників',
'force_check': '🔄 Примусова перевірка',
'webinar_found': '🎓 <b>Новий вебінар!</b>\n\n',
'webinar_item': '📌 <b>{name}</b>\n🔗 https://edu.edu.vn.ua{url}',
'select_language': '🌐 <b>Виберіть мову / Выберите язык / Select language:</b>',
'language_changed': '✅ Мову змінено на українську',
'flag_ru': '🇷🇺 Русский',
'flag_uk': '🇺🇦 Українська',
'flag_en': '🇬🇧 English',
'history_cleared': '✅ Історія вебінарів очищена',
'history_clear_failed': '❌ Помилка при очищенні історії',
},
'en': {
'welcome': '👋 Hello, {name}!\n\nI am the Webinar Checker Bot. I will notify you when a new webinar appears.\nYou have been subscribed to notifications.',
'welcome_admin': '\n\n👑 <b>Admin Mode Active</b>',
'access_denied': '⛔ Access denied. You are not on the whitelist.',
'help_title': '🤖 <b>Bot Help</b>\n\n',
'help_commands': '/start - Subscribe to notifications\n/stop - Unsubscribe from notifications\n/help - Show this message\n/language - Change language',
'help_admin': '\n<b>Admin Commands:</b>\n/adduser [user_id] - Add user to whitelist\n/removeuser [user_id] - Remove user from whitelist\nOr use the panel below to manage settings.',
'admin_only': '⛔ Admin only!',
'user_added': '✅ User {user_id} added to whitelist',
'user_removed': '✅ User {user_id} removed from whitelist',
'user_not_in_whitelist': '⚠️ User {user_id} was not in whitelist',
'cannot_remove_admin': '❌ Cannot remove admin from whitelist',
'invalid_user_id': '❌ Invalid user ID. Must be a number.',
'usage_adduser': 'Usage: /adduser [user_id]',
'usage_removeuser': 'Usage: /removeuser [user_id]',
'whitelist_enabled': '✅ Whitelist Enabled',
'whitelist_disabled': '✅ Whitelist Disabled',
'whitelist_title': '📋 <b>Whitelist:</b>\n',
'subscribers_title': '👥 <b>Subscribers:</b>\n',
'empty': 'Empty',
'force_check_running': '🔄 Running immediate check...',
'check_failed': '❌ Check failed. See logs for details.',
'check_completed_none': '✅ Check completed. No webinars found.',
'check_completed': '✅ Check completed. Found {count} webinar(s)!',
'toggle_whitelist_disable': '🔒 Disable Whitelist',
'toggle_whitelist_enable': '🔓 Enable Whitelist',
'view_whitelist': '📋 View Whitelist',
'view_subscribers': '👥 View Subscribers',
'force_check': '🔄 Force Check',
'webinar_found': '🎓 <b>New webinar found!</b>\n\n',
'webinar_item': '📌 <b>{name}</b>\n🔗 https://edu.edu.vn.ua{url}',
'select_language': '🌐 <b>Select language / Виберіть мову / Выберите язык:</b>',
'language_changed': '✅ Language changed to English',
'flag_ru': '🇷🇺 Русский',
'flag_uk': '🇺🇦 Українська',
'flag_en': '🇬🇧 English',
'history_cleared': '✅ Webinar history cleared',
'history_clear_failed': '❌ Error clearing history',
},
}
# --- Language Helper Functions ---
def get_user_language(user_id: int) -> str:
"""Get user's preferred language from Redis. Default: Ukrainian."""
lang = redis_client.get(f'user:{user_id}:language')
return lang if lang in ['ru', 'uk', 'en'] else 'uk'
def set_user_language(user_id: int, lang: str):
"""Save user's language preference to Redis."""
if lang in ['ru', 'uk', 'en']:
redis_client.set(f'user:{user_id}:language', lang)
logger.info(f'User {user_id} language set to {lang}')
def t(user_id: int, key: str, **kwargs) -> str:
"""Translate message for user with optional formatting."""
lang = get_user_language(user_id)
message = TRANSLATIONS.get(lang, TRANSLATIONS['uk']).get(key, key)
if kwargs:
return message.format(**kwargs)
return message
def get_language_keyboard():
"""Generate language selection keyboard."""
keyboard = [
[
InlineKeyboardButton('🇷🇺 Русский', callback_data='lang_ru'),
InlineKeyboardButton('🇺🇦 Українська', callback_data='lang_uk'),
],
[
InlineKeyboardButton('🇬🇧 English', callback_data='lang_en'),
],
]
return InlineKeyboardMarkup(keyboard)
# --- Helper Functions ---
def is_whitelisted(user_id: int) -> bool:
"""Check if user is allowed to use the bot."""
if user_id == ADMIN_ID:
return True
enabled = redis_client.get(KEY_WHITELIST_ENABLED)
if enabled == '0': # Whitelist disabled
return True
return redis_client.sismember(KEY_WHITELIST, str(user_id))
async def is_group_admin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
"""Check if the user is an administrator in the group."""
user = update.effective_user
chat = update.effective_chat
if chat.type in [ChatType.PRIVATE, 'private']:
return True
try:
member = await context.bot.get_chat_member(chat.id, user.id)
return member.status in [ChatMember.OWNER, ChatMember.ADMINISTRATOR]
except Exception as e:
logger.error(f'Failed to check admin status: {e}')
return False
def get_admin_keyboard(user_id: int):
"""Generate admin panel keyboard."""
whitelist_enabled = redis_client.get(KEY_WHITELIST_ENABLED) != '0'
toggle_text = t(user_id, 'toggle_whitelist_disable') if whitelist_enabled else t(user_id, 'toggle_whitelist_enable')
keyboard = [
[InlineKeyboardButton(toggle_text, callback_data='toggle_whitelist')],
[InlineKeyboardButton(t(user_id, 'view_whitelist'), callback_data='view_whitelist')],
[InlineKeyboardButton(t(user_id, 'view_subscribers'), callback_data='view_subscribers')],
[InlineKeyboardButton(t(user_id, 'force_check'), callback_data='force_check')],
]
return InlineKeyboardMarkup(keyboard)
# --- Diary Functions ---
DIARY_MONTH_NAMES = [
'',
'Січня',
'Лютого',
'Березня',
'Квітня',
'Травня',
'Червня',
'Липня',
'Серпня',
'Вересня',
'Жовтня',
'Листопада',
'Грудня',
]
DIARY_WEEKDAYS_SHORT = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Нд']
def get_diary_keyboard():
today = datetime.now()
keyboard = [
[
InlineKeyboardButton(f'📌 Сьогодні ({today.day}.{today.month:02d})', callback_data='diary_today'),
InlineKeyboardButton('📌 Завтра', callback_data='diary_tomorrow'),
],
[
InlineKeyboardButton('📅 Цей тиждень', callback_data='diary_week'),
InlineKeyboardButton('📅 Весь місяць', callback_data='diary_month'),
],
]
return InlineKeyboardMarkup(keyboard)
def _parse_calendar_html(table_html: str) -> tuple:
"""Parse calendar HTML table into (month_text, {day_num: {weekday, events}})."""
days = {}
weekdays = []
rows = re.findall(r'<tr[^>]*>(.*?)</tr>', table_html, re.DOTALL)
month_text = ''
for r_idx, row in enumerate(rows):
cells = re.findall(r'<t[dh][^>]*>(.*?)</t[dh]>', row, re.DOTALL)
if r_idx == 0:
# Month navigation row: extract "Травень 2026" from nav text
raw = re.sub(r'<[^>]+>', ' ', row).strip()
raw = re.sub(r'\s+', ' ', raw)
m = re.search(r'([А-Яа-яіїєґ\']+\s*:?\s*\d{4})', raw)
month_text = m.group(1).replace(' : ', ' ').strip() if m else raw
elif r_idx == 1:
# Day names row
for cell in cells:
name = re.sub(r'<[^>]+>', '', cell).strip()
if name:
weekdays.append(name)
else:
# Data rows: each cell = a day
for col_idx, cell in enumerate(cells):
# Extract day number — first number in the cell text
text = re.sub(r'<[^>]+>', ' ', cell).strip()
text = re.sub(r'\s+', ' ', text)
dm = re.match(r'(\d+)', text)
if not dm:
continue
day_num = dm.group(1)
# Extract events: title attribute (full name) of ALL <a> tags inside the cell
events = []
for a_match in re.finditer(r'<a[^>]*>(.*?)</a>', cell, re.DOTALL):
a_tag = a_match.group(0)
# Prefer the title attribute (contains full name, not truncated)
title_m = re.search(r'title\s*=\s*"([^"]*)"', a_tag)
et = title_m.group(1).strip() if title_m else re.sub(r'<[^>]+>', '', a_match.group(1)).strip()
if et:
events.append(et)
weekday = weekdays[col_idx] if col_idx < len(weekdays) else ''
days[day_num] = {'weekday': weekday, 'events': events}
return month_text, days
async def fetch_diary_data(phpsessid: str) -> dict | None:
logger.info('Fetching diary data via Playwright...')
try:
async with async_playwright() as p:
browser = await p.chromium.connect(PLAYWRIGHT_WS)
try:
context_browser = await browser.new_context(user_agent=USER_AGENT)
await context_browser.add_cookies(
[{'name': 'PHPSESSID', 'value': phpsessid, 'domain': 'edu.edu.vn.ua', 'path': '/'}]
)
page = await context_browser.new_page()
try:
await page.goto(DIARY_URL, wait_until='domcontentloaded')
await page.wait_for_selector('table.calendar', timeout=10000)
await page.wait_for_timeout(1500)
table_html = await page.evaluate("""
() => {
const t = document.querySelector('table.calendar');
return t ? t.outerHTML : null;
}
""")
if not table_html:
logger.error('table.calendar not found in DOM')
return None
# Debug: save HTML for troubleshooting
with contextlib.suppress(Exception), open('/tmp/diary_debug.html', 'w', encoding='utf-8') as f: # noqa: S108
f.write(table_html)
month_text, days = _parse_calendar_html(table_html)
logger.info(
f'Diary parsed: month={month_text!r}, days_with_events={sum(1 for d in days.values() if d["events"])}/{len(days)}'
)
return {'monthFullText': month_text, 'days': days}
except Exception as e:
logger.error(f'Error parsing diary: {e}')
return None
finally:
await page.close()
await context_browser.close()
finally:
await browser.close()
except Exception as e:
logger.error(f'Playwright error in diary fetch: {e}')
return None
def _parse_diary_month(text: str) -> str:
match = re.search(r'([А-Яа-яіїєґ\']+\s*:\s*\d{4})', text)
if match:
return match.group(1).replace(' : ', ' ').strip()
return text.strip()
def format_diary_day(data: dict, day_num: int) -> str:
days = data.get('days', {})
month_str = _parse_diary_month(data.get('monthFullText', ''))
day_data = days.get(str(day_num))
lines = [f'📅 <b>{day_num} {month_str}</b>', '' * 18]
if not day_data or not day_data.get('events'):
lines.append('Немає подій')
else:
for e in day_data['events']:
lines.append(f'📌 {e}')
lines.append(f'\n🔗 {DIARY_URL}')
return '\n'.join(lines)
def format_diary_week(data: dict, today: datetime) -> str:
days = data.get('days', {})
_parse_diary_month(data.get('monthFullText', ''))
monday = today - timedelta(days=today.weekday())
sunday = monday + timedelta(days=6)
lines = [f'📅 <b>Тиждень {monday.day}.{monday.month} {sunday.day}.{sunday.month}</b>\n']
for i in range(7):
d = monday + timedelta(days=i)
day_data = days.get(str(d.day))
lines.append(f'─ <b>{DIARY_WEEKDAYS_SHORT[i]} {d.day}.{d.month}</b> ─')
if not day_data or not day_data.get('events'):
lines.append('Немає подій\n')
else:
for e in day_data['events']:
lines.append(f'📌 {e}')
lines.append('')
lines.append(f'🔗 {DIARY_URL}')
return '\n'.join(lines)
def format_diary_month(data: dict) -> str:
days = data.get('days', {})
month_str = _parse_diary_month(data.get('monthFullText', ''))
lines = [f'📅 <b>{month_str}</b>\n']
for day_num in sorted(days.keys(), key=int):
day_data = days[day_num]
events = day_data.get('events', [])
weekday = day_data.get('weekday', '')
lines.append(f'─ <b>{weekday} {day_num}</b> ─')
if not events:
lines.append('Немає подій\n')
else:
for e in events:
lines.append(f'📌 {e}')
lines.append('')
lines.append(f'🔗 {DIARY_URL}')
return '\n'.join(lines)
async def _get_diary_data(context: ContextTypes.DEFAULT_TYPE) -> dict | None:
cached = context.user_data.get('diary_cache')
now_ts = time.time()
if cached and (now_ts - cached.get('timestamp', 0)) < 300:
return cached['data']
phpsessid = redis_client.get(KEY_PHPSESSID)
if not phpsessid:
return None
data = await fetch_diary_data(phpsessid)
if data:
context.user_data['diary_cache'] = {'data': data, 'timestamp': now_ts}
return data
# --- Command Handlers ---
async def start(update: Update, _context: ContextTypes.DEFAULT_TYPE):
"""Handle /start command."""
user = update.effective_user
chat = update.effective_chat
logger.info(f'User {user.id} ({user.username}) started the bot in chat {chat.id} ({chat.type}).')
# Check whitelist - MUST be the user executing the command
if not is_whitelisted(user.id):
await update.message.reply_text(t(user.id, 'access_denied'))
return
# Add to subscribers (Chat ID!)
redis_client.sadd(KEY_SUBSCRIBERS, chat.id)
msg = t(chat.id, 'welcome', name=user.first_name)
if user.id == ADMIN_ID and chat.type == 'private':
msg += t(chat.id, 'welcome_admin')
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=get_admin_keyboard(user.id))
else:
await update.message.reply_text(msg, parse_mode='HTML')
async def help_command(update: Update, _context: ContextTypes.DEFAULT_TYPE):
"""Handle /help command."""
user_id = update.effective_user.id
chat_id = update.effective_chat.id
msg = t(chat_id, 'help_title') + t(chat_id, 'help_commands')
if user_id == ADMIN_ID and update.effective_chat.type == 'private':
msg += t(chat_id, 'help_admin')
await update.message.reply_text(msg, parse_mode='HTML', reply_markup=get_admin_keyboard(user_id))
else:
await update.message.reply_text(msg, parse_mode='HTML')
async def stop_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /stop command (unsubscribe)."""
user = update.effective_user
chat = update.effective_chat
# Permission check: Whitelisted user OR Group Admin
if not (is_whitelisted(user.id) or await is_group_admin(update, context)):
await update.message.reply_text(t(chat.id, 'access_denied')) # Or specific "admin only" message
return
redis_client.srem(KEY_SUBSCRIBERS, chat.id)
await update.message.reply_text(
t(chat.id, 'whitelist_disabled')
.replace(' whitelist', ' notifications')
.replace('Білий список', 'Сповіщення')
.replace('Белый список', 'Уведомления')
if chat.id
else 'Unsubscribed'
)
async def language_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /language command."""
user = update.effective_user
chat = update.effective_chat
# Permission check for groups
if not (is_whitelisted(user.id) or await is_group_admin(update, context)):
return
await update.message.reply_text(
t(chat.id, 'select_language'), parse_mode='HTML', reply_markup=get_language_keyboard()
)
async def add_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Add user to whitelist (admin only)."""
admin_id = update.effective_user.id
if admin_id != ADMIN_ID:
await update.message.reply_text(t(admin_id, 'admin_only'))
return
if not context.args:
await update.message.reply_text(t(admin_id, 'usage_adduser'))
return
try:
user_id = int(context.args[0])
redis_client.sadd(KEY_WHITELIST, str(user_id))
await update.message.reply_text(t(admin_id, 'user_added', user_id=user_id))
logger.info(f'Admin added user {user_id} to whitelist')
except ValueError:
await update.message.reply_text(t(admin_id, 'invalid_user_id'))
async def remove_user(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Remove user from whitelist (admin only)."""
admin_id = update.effective_user.id
if admin_id != ADMIN_ID:
await update.message.reply_text(t(admin_id, 'admin_only'))
return
if not context.args:
await update.message.reply_text(t(admin_id, 'usage_removeuser'))
return
try:
user_id = int(context.args[0])
if str(user_id) == str(ADMIN_ID):
await update.message.reply_text(t(admin_id, 'cannot_remove_admin'))
return
removed = redis_client.srem(KEY_WHITELIST, str(user_id))
if removed:
await update.message.reply_text(t(admin_id, 'user_removed', user_id=user_id))
logger.info(f'Admin removed user {user_id} from whitelist')
else:
await update.message.reply_text(t(admin_id, 'user_not_in_whitelist', user_id=user_id))
except ValueError:
await update.message.reply_text(t(admin_id, 'invalid_user_id'))
async def clear_history(update: Update, _context: ContextTypes.DEFAULT_TYPE):
"""Clear webinar history (admin only)."""
admin_id = update.effective_user.id
if admin_id != ADMIN_ID:
await update.message.reply_text(t(admin_id, 'admin_only'))
return
try:
redis_client.delete(KEY_WEBINAR_HISTORY)
await update.message.reply_text(t(admin_id, 'history_cleared'))
logger.info('Admin cleared webinar history')
except Exception as e:
logger.error(f'Failed to clear history: {e}')
await update.message.reply_text(t(admin_id, 'history_clear_failed'))
async def diary_command(update: Update, _context: ContextTypes.DEFAULT_TYPE):
user = update.effective_user
if not is_whitelisted(user.id):
await update.message.reply_text(t(user.id, 'access_denied'))
return
await update.message.reply_text(
'📅 <b>Щоденник</b> — виберіть період:', parse_mode='HTML', reply_markup=get_diary_keyboard()
)
async def diary_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
query = update.callback_query
user_id = query.from_user.id
await query.answer()
if user_id != ADMIN_ID and not is_whitelisted(user_id):
await query.edit_message_text('⛔ Доступ заборонено.')
return
data = query.data
if data == 'diary_refresh':
context.user_data.pop('diary_cache', None)
await query.edit_message_text('🔄 Завантажую щоденник...')
diary_data = await _get_diary_data(context)
if not diary_data:
await query.edit_message_text('❌ Не вдалося завантажити щоденник. Немає сесії або помилка.')
return
await query.edit_message_text(
'📅 <b>Щоденник</b> — виберіть період:', parse_mode='HTML', reply_markup=get_diary_keyboard()
)
return
await query.edit_message_text('🔄 Завантажую щоденник...')
diary_data = await _get_diary_data(context)
if not diary_data:
await query.edit_message_text('❌ Не вдалося завантажити щоденник.')
return
today = datetime.now()
if data == 'diary_today':
text = format_diary_day(diary_data, today.day)
elif data == 'diary_tomorrow':
tomorrow = today + timedelta(days=1)
if tomorrow.day < today.day:
text = '❌ Дані за наступний місяць недоступні. Перейдіть на сайт.'
else:
text = format_diary_day(diary_data, tomorrow.day)
elif data == 'diary_week':
text = format_diary_week(diary_data, today)
elif data == 'diary_month':
text = format_diary_month(diary_data)
else:
return
if len(text) > 4096:
text = text[:4090] + '\n\n✂️ ...(обрізано)'
await query.edit_message_text(text, parse_mode='HTML', reply_markup=get_diary_keyboard())
# --- Admin Callbacks ---
async def admin_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle admin panel button clicks."""
query = update.callback_query
user_id = query.from_user.id
if user_id != ADMIN_ID:
await query.answer(t(user_id, 'admin_only'), show_alert=True)
return
await query.answer()
data = query.data
if data == 'toggle_whitelist':
current = redis_client.get(KEY_WHITELIST_ENABLED)
new_state = '0' if current != '0' else '1'
redis_client.set(KEY_WHITELIST_ENABLED, new_state)
state_text = t(user_id, 'whitelist_disabled') if new_state == '0' else t(user_id, 'whitelist_enabled')
await query.edit_message_reply_markup(reply_markup=get_admin_keyboard(user_id))
await query.message.reply_text(state_text)
elif data == 'view_whitelist':
members = redis_client.smembers(KEY_WHITELIST)
msg = t(user_id, 'whitelist_title') + ('\n'.join(members) if members else t(user_id, 'empty'))
await query.message.reply_text(msg, parse_mode='HTML')
elif data == 'view_subscribers':
subs = redis_client.smembers(KEY_SUBSCRIBERS)
msg = t(user_id, 'subscribers_title') + ('\n'.join(subs) if subs else t(user_id, 'empty'))
await query.message.reply_text(msg, parse_mode='HTML')
elif data == 'force_check':
await query.message.reply_text(t(user_id, 'force_check_running'))
result = await check_webinars_job(context)
if result is None:
await query.message.reply_text(t(user_id, 'check_failed'))
elif result == 0:
await query.message.reply_text(t(user_id, 'check_completed_none'))
else:
await query.message.reply_text(t(user_id, 'check_completed', count=result))
async def language_callback(update: Update, _context: ContextTypes.DEFAULT_TYPE):
"""Handle language selection button clicks."""
query = update.callback_query
user_id = query.from_user.id
data = query.data
if data.startswith('lang_'):
lang = data.split('_')[1]
set_user_language(user_id, lang)
await query.answer()
await query.edit_message_text(t(user_id, 'language_changed'), parse_mode='HTML')
# --- Webinar Checking Job ---
def get_webinar_key(url: str) -> str:
"""Generate unique key for a webinar based on URL."""
return url
def get_stored_webinars() -> list:
"""Get list of stored webinar keys from Redis."""
data = redis_client.get(KEY_WEBINAR_HISTORY)
if data:
try:
return json.loads(data)
except Exception as e:
logger.error(f'Failed to parse webinar history: {e}')
return []
def store_webinars(webinar_keys: list):
"""Store up to 3 most recent webinar keys in Redis."""
# Keep only last 3
webinar_keys = webinar_keys[-3:]
try:
redis_client.set(KEY_WEBINAR_HISTORY, json.dumps(webinar_keys))
logger.info(f'Stored {len(webinar_keys)} webinar(s) in history')
except Exception as e:
logger.error(f'Failed to store webinar history: {e}')
async def check_webinars_job(context: ContextTypes.DEFAULT_TYPE):
"""Background job to check for webinars using Async Playwright.
Returns:
int: Number of webinars found, or None if check failed
"""
logger.info('Running webinar check...')
phpsessid = redis_client.get(KEY_PHPSESSID)
if not phpsessid:
logger.warning('PHPSESSID missing. Skipping check.')
# --- DEBUG LOGGING ---
try:
with open('phpsessid_missing.log', 'a') as f:
f.write(f'[{os.getcwd()}] PHPSESSID missing at {context.job.last_run: %Y-%m-%d %H:%M:%S}\n')
except Exception as e:
logger.error(f'Failed to write PHPSESSID debug log: {e}')
# ---------------------
return None
current_webinars = [] # List of dicts with name, url, and formatted text
content = ''
try:
async with async_playwright() as p:
# Connect to remote Playwright service
browser = await p.chromium.connect(PLAYWRIGHT_WS)
try:
# Create browser context with user agent
context_browser = await browser.new_context(user_agent=USER_AGENT)
# Add PHPSESSID cookie
await context_browser.add_cookies(
[{'name': 'PHPSESSID', 'value': phpsessid, 'domain': 'edu.edu.vn.ua', 'path': '/'}]
)
# Create new page
page = await context_browser.new_page()
try:
# Navigate to webinar page
await page.goto(WEBINAR_URL, wait_until='domcontentloaded')
# Wait for the table to load
await page.wait_for_selector('#meetings table', timeout=10000)
await page.wait_for_timeout(2000)
# Get page content
content = await page.content()
# Check if "no webinar" message is present
if 'Жодного онлайн уроку зараз' not in content:
logger.info('!!! WEBINAR FOUND !!!')
# Extract webinar details from table rows
rows = page.locator('#meetings table tbody tr')
count = await rows.count()
for i in range(count):
row = rows.nth(i)
text = await row.inner_text()
if 'Жодного онлайн уроку зараз' not in text:
# Extract name (topic) from first column
name_elem = row.locator('td').nth(0)
name = await name_elem.inner_text()
name = name.strip()
# Extract join URL from fourth column
url_elem = row.locator('td').nth(3).locator('a[href*="/webinar/join/"]').first
url = await url_elem.get_attribute('href')
if name and url:
current_webinars.append({'name': name, 'url': url, 'text': text.strip()})
logger.info(f'Found webinar: {name} -> {url}')
else:
logger.info('No webinars found (expected message present)')
except Exception as e:
logger.error(f'Error checking page: {e}. Saving content for debug.')
# If page content is available, save it on error
with contextlib.suppress(Exception):
if page and not content:
content = await page.content()
return None
finally:
await page.close()
await context_browser.close()
finally:
await browser.close()
except Exception as e:
logger.error(f'Playwright error: {e}')
return None
# --- DEBUG LOGGING (Saving last response content) ---
if not current_webinars and content: # if no webinars found, save the page content
try:
with open('response.html', 'w', encoding='utf-8') as f:
f.write(content)
logger.info('Saved page content to response.html for debug.')
except Exception as e:
logger.error(f'Failed to write debug HTML: {e}')
# -----------------------------------------------------
# Check for NEW webinars and notify
if current_webinars:
# Get stored webinar history
stored_keys = get_stored_webinars()
logger.info(f'Stored webinar keys: {stored_keys}')
# Find new webinars (not in history)
new_webinars = []
current_keys = []
for webinar in current_webinars:
key = get_webinar_key(webinar['url'])
current_keys.append(key)
if key not in stored_keys:
new_webinars.append(webinar)
logger.info(f'NEW webinar detected: {webinar["name"]}')
# Update stored history with current webinars
# Merge old and new, keeping only last 5
updated_keys = stored_keys + [k for k in current_keys if k not in stored_keys]
store_webinars(updated_keys)
# Notify subscribers ONLY about NEW webinars
if new_webinars:
subscribers = redis_client.smembers(KEY_SUBSCRIBERS)
logger.info(
f'Sending notification about {len(new_webinars)} new webinar(s) to {len(subscribers)} subscriber(s)'
)
for sub_id in subscribers:
try:
# Build message in user's language
# sub_id comes from redis set as string, convert to int for translation lookup
webinar_items = '\n\n'.join(
[t(int(sub_id), 'webinar_item', name=w['name'], url=w['url']) for w in new_webinars]
)
message = t(int(sub_id), 'webinar_found') + webinar_items
await context.bot.send_message(chat_id=sub_id, text=message, parse_mode='HTML')
logger.info(f'Notification sent to {sub_id}')
except Exception as e:
logger.error(f'Failed to send to {sub_id}: {e}')
else:
logger.info(f'Found {len(current_webinars)} webinar(s), but all are already known')
return len(current_webinars)
# --- Main ---
def main():
if not WEBINAR_TELEGRAM_TOKEN:
logger.error('WEBINAR_TELEGRAM_TOKEN is missing!')
return
# Set default whitelist state if not set
if not redis_client.exists(KEY_WHITELIST_ENABLED):
redis_client.set(KEY_WHITELIST_ENABLED, '1') # Enabled by default
# Add admin to whitelist
if ADMIN_ID:
redis_client.sadd(KEY_WHITELIST, str(ADMIN_ID))
app = Application.builder().token(WEBINAR_TELEGRAM_TOKEN).build()
# Handlers
app.add_handler(CommandHandler('start', start))
app.add_handler(CommandHandler('stop', stop_command))
app.add_handler(CommandHandler('help', help_command))
app.add_handler(CommandHandler('language', language_command))
app.add_handler(CommandHandler('adduser', add_user))
app.add_handler(CommandHandler('removeuser', remove_user))
app.add_handler(CommandHandler('clearhistory', clear_history))
app.add_handler(CommandHandler('diary', diary_command))
# Callback handlers - diary first, then language selection, then admin panel
app.add_handler(CallbackQueryHandler(diary_callback, pattern='^diary_'))
app.add_handler(CallbackQueryHandler(language_callback, pattern='^lang_'))
app.add_handler(CallbackQueryHandler(admin_callback))
# Job Queue
job_queue = app.job_queue
job_queue.run_repeating(check_webinars_job, interval=WEBINAR_CHECK_INTERVAL, first=10)
logger.info('Bot started polling...')
app.run_polling()
if __name__ == '__main__':
main()
+5
View File
@@ -0,0 +1,5 @@
FROM nginx:alpine
RUN rm -rf /usr/share/nginx/html/*
COPY html /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+21
View File
@@ -0,0 +1,21 @@
services:
errorpage:
build: .
image: gcr.forust.xyz/forust/error-pages:latest
pull_policy: build
container_name: error-pages
restart: unless-stopped
# ports:
# - 1234:80
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.services.error-pages.loadbalancer.server.port=80"
# Error handler middleware
- "traefik.http.middlewares.error-pages.errors.status=400,402-599"
- "traefik.http.middlewares.error-pages.errors.service=error-pages"
- "traefik.http.middlewares.error-pages.errors.query=/{status}.html"
networks:
proxy:
external: true
+208
View File
@@ -0,0 +1,208 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>403 // Forbidden</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="403">403</h1>
<p class="subtitle">> Forbidden / Access Denied.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>You do not have permission to access this resource.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> if you believe this is an
error.</p>
</section>
<footer>
<p>root@error:~$ sudo access_resource</p>
<p>User is not in the sudoers file. This incident will be reported.</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
+207
View File
@@ -0,0 +1,207 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 // Not Found</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="404">404</h1>
<p class="subtitle">> Page Not Found / Lost in the Void.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>The page you are looking for does not exist or has been moved.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> if you believe this is an error.</p>
</section>
<footer>
<p>root@error:~$ ping target</p>
<p>Destination Host Unreachable</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
+207
View File
@@ -0,0 +1,207 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>500 // Server Error</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="500">500</h1>
<p class="subtitle">> Internal Server Error / System Failure.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>Something went wrong on our end. We are working to fix it.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> for more information.</p>
</section>
<footer>
<p>root@error:~$ systemctl status service</p>
<p>Active: failed (Result: core-dump)</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
+207
View File
@@ -0,0 +1,207 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>502 // Bad Gateway</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="502">502</h1>
<p class="subtitle">> Bad Gateway / System Failure.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>The server received an invalid response from the upstream server.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> for more information.</p>
</section>
<footer>
<p>root@error:~$ curl -I upstream_host</p>
<p>HTTP/1.1 502 Bad Gateway</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
+207
View File
@@ -0,0 +1,207 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>503 // Service Unavailable</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="503">503</h1>
<p class="subtitle">> Service Unavailable / System Failure.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>The server is currently unable to handle the request due to maintenance or overload.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> for more information.</p>
</section>
<footer>
<p>root@error:~$ systemctl start service</p>
<p>Job for service failed because the control process exited with error code.</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
+207
View File
@@ -0,0 +1,207 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>504 // Gateway Timeout</title>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* hidden in a plain sight? */
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: var(--font-mono);
line-height: 1.6;
font-size: 16px;
padding: 2rem;
}
a {
color: var(--text-color);
text-decoration: none;
border-bottom: 1px solid var(--dim);
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color);
color: var(--bg-color);
border-color: var(--text-color);
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* TEXT */
h1 {
font-size: 2.5rem;
text-transform: uppercase;
letter-spacing: -2px;
margin-bottom: 0.5rem;
}
h2 {
font-size: 1.2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--dim);
display: inline-block;
padding-right: 20px;
}
.subtitle {
color: var(--dim);
margin-bottom: 2rem;
}
hr {
border: 0;
border-top: 1px dashed var(--dim);
margin: 2rem 0;
}
.comment {
color: var(--dim);
font-size: 0.9rem;
margin-left: 10px;
}
/* SECTIONS */
section {
margin-bottom: 3rem;
}
/* LISTS */
ul {
list-style: none;
}
.link-list li {
margin-bottom: 0.8rem;
display: flex;
align-items: center;
gap: 15px;
}
/* STACK GRID */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.skill-item {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.level {
font-weight: bold;
}
.special .level {
color: var(--text-color);
text-shadow: 1px 0 0 red, -1px 0 0 blue;
}
/* my dudes */
.team-grid {
display: flex;
gap: 2rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.member {
text-align: center;
width: 100px;
}
.avatar {
width: 80px;
height: 80px;
background-color: #222;
border: 2px solid var(--text-color);
margin: 0 auto 10px auto;
background-size: cover;
}
/* if no avatar added: */
.placeholder::before {
content: "?";
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 2rem;
color: var(--dim);
}
/* REPOS */
.repo-list li {
margin-bottom: 1rem;
}
/* FOOTER */
footer {
text-align: center;
color: var(--dim);
font-size: 0.8rem;
/* flag{why-are-you-here?} */
margin-top: 4rem;
}
/* SMTH RESPONSIVE */
@media (max-width: 600px) {
.grid-2 {
grid-template-columns: 1fr;
gap: 0;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1 class="glitch" data-text="504">504</h1>
<p class="subtitle">> Gateway Timeout / System Failure.</p>
</header>
<hr>
<section id="message">
<h2>./error_message</h2>
<p>The server did not receive a timely response from the upstream server.</p>
<br>
<p>Check the <a href="https://status.forust.xyz">System Status</a> for more information.</p>
</section>
<footer>
<p>root@error:~$ timeout 30s curl upstream</p>
<p>curl: (28) Operation timed out after 30001 milliseconds with 0 bytes received</p>
<p>&copy; XRock - Just Signal.</p>
</footer>
</div>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
apiVersion: v1
kind: Service
metadata:
name: error-pages-service
namespace: error-pages
spec:
selector:
app: error-pages
ports:
- port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: error-pages-deployment
namespace: error-pages
spec:
replicas: 1
selector:
matchLabels:
app: error-pages
template:
metadata:
labels:
app: error-pages
spec:
containers:
- name: error-pages
image: gcr.forust.xyz/forust/error-pages:latest
ports:
- containerPort: 80
---
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: error-pages
+3
View File
@@ -1,3 +1,6 @@
GITEA_POSTGRES_USER=
GITEA_POSTGRES_PASSWORD=
GITEA_POSTGRES_DB=gitea
GITEA_SMTP_PASS=
MAILER_ADDR=
SERVICE_EMAIL=email.used.by.services@domain.tld
+41 -12
View File
@@ -1,7 +1,8 @@
services:
server:
image: docker.gitea.com/gitea:1.25.1
image: docker.gitea.com/gitea:1.26
container_name: gitea
restart: always
environment:
- USER_UID=1000
- USER_GID=1000
@@ -11,25 +12,54 @@ services:
- GITEA__database__USER=gitea
- GITEA__database__PASSWD=gitea
- GITEA__database__NAME=gitea
#Server
# Server
- GITEA__server__ROOT_URL=https://gitea.forust.xyz
- GITEA__server__SSH_DOMAIN=gitssh.forust.xyz
- GITEA__server__SSH_PORT=2221
restart: always
networks:
- gitea-db
- traefik-proxy
# Mailer
- GITEA__mailer__ENABLED=true
- GITEA__mailer__FROM=${SERVICE_EMAIL}
- GITEA__mailer__SMTP_ADDR=${MAILER_ADDR}:465
- GITEA__mailer__USER=${SERVICE_EMAIL}
- GITEA__mailer__PASSWD=${GITEA_SMTP_PASS}
- GITEA__mailer__PROTOCOL=SMTP
- GITEA__service__REGISTER_EMAIL_CONFIRM=true
- GITEA__service__ENABLE_NOTIFY_MAIL=true
volumes:
- ./gitea-data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-proxy"
- "traefik.http.services.gitea.loadbalancer.server.port=3000"
# Prod Router
- "traefik.http.routers.gitea.rule=Host(`gitea.forust.xyz`)"
- "traefik.http.routers.gitea.entrypoints=websecure"
- "traefik.http.routers.gitea.tls.certresolver"
# Local Router
- "traefik.http.routers.gitea-local.rule=Host(`gitea.workstation.internal`)"
- "traefik.http.routers.gitea-local.entrypoints=websecure"
- "traefik.http.routers.gitea-local.tls=true"
# Dev Router
- "traefik.http.routers.gitea-dev.rule=Host(`gitea.gigaforust.internal`)"
- "traefik.http.routers.gitea-dev.entrypoints=websecure"
- "traefik.http.routers.gitea-dev.tls=true"
# SSH Router
- "traefik.tcp.services.gitea.loadbalancer.server.port=22"
- "traefik.tcp.routers.gitea.entrypoints=ssh"
- "traefik.tcp.routers.gitea.rule=HostSNI(`*`)"
# Gitea container registry Router
- "traefik.http.routers.gitea-registry.rule=Host(`gcr.forust.xyz`) && PathPrefix(`/v2`)"
- "traefik.http.routers.gitea-registry.entrypoints=websecure"
- "traefik.http.routers.gitea-registry.tls.certresolver=letsencrypt"
ports:
- "2221:22"
networks:
- gitea-db
- proxy
depends_on:
- db
db:
image: docker.io/library/postgres:14
restart: always
@@ -37,13 +67,12 @@ services:
- POSTGRES_USER=gitea
- POSTGRES_PASSWORD=gitea
- POSTGRES_DB=gitea
networks:
- gitea-db
volumes:
- ./gitea-db/:/var/lib/postgresql/data
networks:
- gitea-db
networks:
gitea-db:
external: false
traefik-proxy:
proxy:
external: true
+22
View File
@@ -0,0 +1,22 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: gitea-config
namespace: gitea
data:
GITEA__server__DOMAIN: "gitea.forust.xyz"
GITEA__server__ROOT_URL: "https://gitea.forust.xyz"
GITEA__server__SSH_DOMAIN: "gitssh.forust.xyz"
GITEA__server__SSH_PORT: "2221"
GITEA__database__DB_TYPE: "postgres"
GITEA__database__HOST: "gitea-postgres-service:5432"
GITEA__database__NAME: "gitea"
GITEA__security__REVERSE_PROXY_LIMIT: "1"
GITEA__security__REVERSE_PROXY_TRUSTED_PROXIES: "*"
GITEA__mailer__ENABLED: "false"
GITEA__log__logger__access__MODE: "console, file"
USER_UID: "1000"
USER_GID: "1000"
+71
View File
@@ -0,0 +1,71 @@
apiVersion: v1
kind: Service
metadata:
name: gitea-service
namespace: gitea
spec:
selector:
app: gitea
ports:
- port: 3000
name: http
targetPort: 3000
- port: 2221
name: ssh
targetPort: 22
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea-deployment
namespace: gitea
spec:
replicas: 1
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
containers:
- name: gitea
image: docker.gitea.com/gitea:1.26
envFrom:
- configMapRef:
name: gitea-config
- secretRef:
name: gitea-secrets
ports:
- containerPort: 3000
name: http
- containerPort: 2221
name: ssh
volumeMounts:
- name: gitea-data
mountPath: /data
resources:
requests:
memory: "512Mi"
cpu: "300m"
limits:
memory: "1.5Gi"
cpu: "1300m"
volumes:
- name: gitea-data
persistentVolumeClaim:
claimName: gitea-pvc
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-pvc
namespace: gitea
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
+57
View File
@@ -0,0 +1,57 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: gitea-prod
namespace: gitea
spec:
entryPoints:
- websecure
routes:
- match: Host(`gitea.forust.xyz`)
kind: Rule
middlewares:
services:
- name: gitea-service
port: 3000
- match: Host(`gcr.forust.xyz`) && PathPrefix(`/v2`)
kind: Rule
middlewares:
services:
- name: gitea-service
port: 3000
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: gitea-local
namespace: gitea
spec:
entryPoints:
- websecure
routes:
- match: Host(`gitea.workstation.internal`) || Host(`gitea.gigaforust.internal`)
kind: Rule
services:
- name: gitea-service
port: 3000
- match: (Host(`gcr.workstation.internal`) || Host(`gcr.gigaforust.internal`)) && PathPrefix(`/v2`)
kind: Rule
services:
- name: gitea-service
port: 3000
---
apiVersion: traefik.io/v1alpha1
kind: IngressRouteTCP
metadata:
name: gitea-ssh
namespace: gitea
spec:
entryPoints:
- ssh
routes:
- match: HostSNI(`*`)
services:
- name: gitea-service
port: 2221
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: gitea
+62
View File
@@ -0,0 +1,62 @@
apiVersion: v1
kind: Service
metadata:
name: gitea-postgres-service
namespace: gitea
spec:
clusterIP: None
selector:
app: gitea-postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: gitea-postgres-statefulset
namespace: gitea
spec:
selector:
matchLabels:
app: gitea-postgres
serviceName: gitea-postgres-service
replicas: 1
template:
metadata:
labels:
app: gitea-postgres
spec:
containers:
- name: gitea-postgres
image: postgres:14
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: gitea-secrets
key: GITEA__database__USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: gitea-secrets
key: GITEA__database__PASSWD
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: gitea-secrets
key: GITEA__database__USER
ports:
- containerPort: 5432
name: postgres
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
+9
View File
@@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: gitea-secrets
namespace: gitea
type: Opaque
stringData:
GITEA__database__USER: "gitea"
GITEA__database__PASSWD: "gitea"
+20 -6
View File
@@ -11,12 +11,26 @@ services:
env_file: .env
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-proxy"
- "traefik.docker.network=proxy"
- "traefik.services.glance.loadbalancer.server.port=8080"
# Prod Router
- "traefik.http.routers.glance.rule=(Host(`forust.xyz`) || Host(`www.forust.xyz`)) && PathPrefix(`/glance`)"
- "traefik.http.routers.glance.entrypoints=websecure"
- "traefik.http.routers.glance.priority=50"
- "traefik.http.routers.glance.tls.certresolver=letsencrypt"
# Local Router
- "traefik.http.routers.glance-local.rule=Host(`workstation.internal`) && PathPrefix(`/glance`)"
- "traefik.http.routers.glance-local.entrypoints=websecure"
- "traefik.http.routers.glance-local.priority=50"
- "traefik.http.routers.glance-local.tls=true"
# Dev Router
- "traefik.http.routers.glance-dev.rule=Host(`gigaforust.internal`) && PathPrefix(`/glance`)"
- "traefik.http.routers.glance-dev.entrypoints=websecure"
- "traefik.http.routers.glance-dev.priority=50"
- "traefik.http.routers.glance-dev.tls=true"
networks:
- traefik-proxy
dns:
- 1.1.1.1
- 8.8.8.8
- proxy
networks:
traefik-proxy:
proxy:
external: true
-1
View File
@@ -65,7 +65,6 @@
- symbol: MSFT
name: Microsoft
- type: releases
cache: 1d
# Without authentication the Github API allows for up to 60 requests per hour. You can create a
-2
View File
@@ -11,5 +11,3 @@
widgets:
- type: docker-containers
hide-by-default: false
+211
View File
@@ -0,0 +1,211 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: glance-assets
namespace: glance
data:
# Инжектируем твой брутализм напрямую в ассеты
user.css: |
:root {
--bg-color: #050505;
--text-color: #e0e0e0;
--accent: #ffffff;
--dim: #666666;
--font-mono: 'Courier New', Courier, monospace;
}
/* Принудительно ставим моноширинный шрифт для всего дашборда */
body, id, main, div, span, p, a, h1, h2, h3 {
font-family: var(--font-mono) !important;
letter-spacing: -0.5px;
}
/* Ломаем закругления Glance и делаем жесткие рамки */
.widget, .card, main div, [class*="widget"], [class*="card"] {
border-radius: 0px !important;
border: 1px solid var(--dim) !important;
box-shadow: none !important;
background-color: var(--bg-color) !important;
}
/* Стилизация ссылок под ховер-эффект из твоего style.css */
a {
color: var(--text-color) !important;
text-decoration: none !important;
border-bottom: 1px solid var(--dim) !important;
transition: all 0.2s;
}
a:hover {
background-color: var(--text-color) !important;
color: var(--bg-color) !important;
border-color: var(--text-color) !important;
}
/* Кастомизация заголовков внутри модулей */
h2, .widget-title, [class*="title"] {
text-transform: uppercase;
font-weight: bold;
}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: glance-config
namespace: glance
data:
glance.yml: |
server:
assets-path: /app/assets
proxied: true
base-url: /glance
theme:
# Перевели #050505 и #e0e0e0 в формат HSL для Glance
background-color: 0 0 2 # Истинно черный фон
primary-color: 0 0 88 # Светло-серый текст
contrast-multiplier: 1.4
positive-color: 140 50 50 # Зеленый для UP-сервисов (не вырвиглазный)
negative-color: 0 70 50 # Красный для упавших сайтов
custom-css-file: /assets/user.css
pages:
- $include: home.yml
- $include: docker.yml
- $include: monitor.yml
home.yml: |
- name: Home
columns:
- size: small
widgets:
- type: clock
hour-format: 24h
timezones:
- timezone: Europe/Bratislava
label: Bratislava
- timezone: Europe/Kyiv
label: Kyiv
- timezone: Europe/Moscow
label: St. Petersburg
- type: calendar
first-day-of-week: monday
- type: rss
limit: 10
collapse-after: 3
cache: 12h
feeds:
- url: https://selfh.st/rss/
title: selfh.st
- size: full
widgets:
- type: group
widgets:
- type: hacker-news
- type: lobsters
- type: videos
channels:
- UCXuqSBlHAE6Xw-yeJA0Tunw
- UCR-DXc1voovS8nhAvccRZhg
- UCsBjURrPoezykLs9EqgamOA
- UCBJycsmduvYEL83R_U4JriQ
- UCHnyfMqiRRG1u-2MsSQLbXA
- type: group
widgets:
- type: reddit
subreddit: technology
show-thumbnails: true
- type: reddit
subreddit: selfhosted
show-thumbnails: true
- size: small
widgets:
- type: weather
location: London, United Kingdom
units: metric
hour-format: 12h
hide-location: true
- type: markets
markets:
- symbol: SPY
name: S&P 500
- symbol: BTC-USD
name: Bitcoin
- symbol: NVDA
name: NVIDIA
- symbol: AAPL
name: Apple
- symbol: MSFT
name: Microsoft
- type: releases
cache: 1d
repositories:
- glanceapp/glance
- go-gitea/gitea
- nextcloud/all-in-one
docker.yml: |
- name: Docker
columns:
- size: full
widgets:
- type: docker-containers
hide-by-default: false
monitor.yml: |
- name: Monitoring
columns:
- size: small
widgets:
- type: dns-stats
service: adguard
url: http://adguard-service.adguard.svc.cluster.local:3000
username: forust
password: ${ADGUARD_PASSWORD}
- size: full
widgets:
- type: monitor
title: Services Status
cache: 1m
sites:
- title: forust.xyz
url: https://forust.xyz
- title: dns.forust.xyz
url: https://dns.forust.xyz
- title: www.lk-tour.com.ua
url: https://www.lk-tour.com.ua
- title: lk-tour.com.ua
url: https://lk-tour.com.ua
# - title: gitssh.forust.xyz
# url: https://gitssh.forust.xyz
# - title: gcr.forust.xyz
# url: https://gcr.forust.xyz/v2/
- title: gitea.forust.xyz
url: https://gitea.forust.xyz
- title: nextcloud.forust.xyz
url: https://nextcloud.forust.xyz
- title: mc.forust.xyz
url: https://mc.forust.xyz/map
- title: auth.forust.xyz
url: https://auth.forust.xyz
- title: metube.forust.xyz
url: https://metube.forust.xyz
- title: dockmon.forust.xyz
url: https://dockmon.forust.xyz
- title: portainer.forust.xyz
url: https://portainer.forust.xyz
- title: termix.forust.xyz
url: https://termix.forust.xyz
- title: uptime.forust.xyz
url: https://uptime.forust.xyz
- title: cmk.forust.xyz
url: https://cmk.forust.xyz
- title: search.forust.xyz
url: https://search.forust.xyz
- title: status.forust.xyz
url: https://status.forust.xyz
- title: traefik.forust.xyz
url: https://traefik.forust.xyz
- title: media.forust.xyz
url: https://media.forust.xyz
- title: wfs.forust.xyz
url: https://wfs.forust.xyz
- title: forust.xyz/convert
url: https://forust.xyz/convert
+78
View File
@@ -0,0 +1,78 @@
apiVersion: v1
kind: Service
metadata:
name: glance-service
namespace: glance
spec:
selector:
app: glance
ports:
- port: 8080
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: glance-deployment
namespace: glance
spec:
replicas: 1
selector:
matchLabels:
app: glance
template:
metadata:
labels:
app: glance
spec:
containers:
- name: glance
image: glanceapp/glance
envFrom:
- secretRef:
name: glance-secrets
ports:
- containerPort: 8080
volumeMounts:
- name: glance-config
mountPath: /app/config/glance.yml
subPath: glance.yml
- name: glance-config
mountPath: /app/config/home.yml
subPath: home.yml
- name: glance-config
mountPath: /app/config/docker.yml
subPath: docker.yml
- name: glance-config
mountPath: /app/config/monitor.yml
subPath: monitor.yml
- name: glance-assets
mountPath: /app/assets/user.css
subPath: user.css
- name: docker-socket
mountPath: /var/run/docker.sock
- name: localtime
mountPath: /etc/localtime
readOnly: true
resources:
requests:
memory: "30Mi"
cpu: "20m"
limits:
memory: "100Mi"
cpu: "50m"
volumes:
- name: glance-config
configMap:
name: glance-config
- name: glance-assets
configMap:
name: glance-config
- name: docker-socket
hostPath:
path: /var/run/docker.sock
type: Socket
- name: localtime
hostPath:
path: /etc/localtime
type: File
+47
View File
@@ -0,0 +1,47 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: glance-prod
namespace: glance
spec:
entryPoints:
- websecure
routes:
- match: (Host(`forust.xyz`) || Host(`www.forust.xyz`)) && PathPrefix(`/glance`)
kind: Rule
priority: 50
middlewares:
- name: glance-stripprefix
services:
- name: glance-service
port: 8080
tls:
certResolver: letsencrypt
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: glance-local
namespace: glance
spec:
entryPoints:
- websecure
routes:
- match: (Host(`glance.workstation.internal`) || Host(`glance.gigaforust.internal`)) && PathPrefix(`/glance`)
kind: Rule
middlewares:
- name: glance-stripprefix
priority: 50
services:
- name: glance-service
port: 8080
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: glance-stripprefix
namespace: glance
spec:
stripPrefix:
prefixes:
- /glance
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: glance

Some files were not shown because too many files have changed in this diff Show More