diff --git a/.github/workflows/frontend-cicd.yml b/.github/workflows/frontend-cicd.yml index 0391a39..dd242f6 100644 --- a/.github/workflows/frontend-cicd.yml +++ b/.github/workflows/frontend-cicd.yml @@ -45,6 +45,9 @@ jobs: - name: Install dependencies run: npm ci --prefer-offline --legacy-peer-deps + - name: Run tests + run: npm run test + - name: Configure Next.js for pages uses: actions/configure-pages@v5 with: diff --git a/frontend/src/__tests__/public/validate-json.test.ts b/frontend/src/__tests__/public/validate-json.test.ts index ab73292..562ebe9 100644 --- a/frontend/src/__tests__/public/validate-json.test.ts +++ b/frontend/src/__tests__/public/validate-json.test.ts @@ -4,17 +4,7 @@ import path from "path"; import { ScriptSchema, type Script } from "@/app/json-editor/_schemas/schemas"; import { Metadata } from "@/lib/types"; -const publicJsonPath = path.join(process.cwd(), 'public/json'); -const getJsonDirectory = async () => { - if (!(await fs.stat(publicJsonPath).catch(() => null))) { - throw new Error(`JSON path file not found: ${publicJsonPath}`); - } - const jsonPath = (await fs.readFile(publicJsonPath, "utf-8")).trim(); - return path.resolve(process.cwd(), jsonPath); -}; - - -const jsonDir = await getJsonDirectory(); +const jsonDir = "public/json"; const metadataFileName = "metadata.json"; const encoding = "utf-8"; @@ -25,7 +15,7 @@ describe.each(fileNames)("%s", async (fileName) => { let script: Script; beforeAll(async () => { - const filePath = path.resolve(jsonDir, fileName); + const filePath = path.resolve(jsonDir, fileName); const fileContent = await fs.readFile(filePath, encoding) script = JSON.parse(fileContent); }) @@ -46,7 +36,7 @@ describe(`${metadataFileName}`, async () => { let metadata: Metadata; beforeAll(async () => { - const filePath = path.resolve(jsonDir, metadataFileName); + const filePath = path.resolve(jsonDir, metadataFileName); const fileContent = await fs.readFile(filePath, encoding) metadata = JSON.parse(fileContent); }) @@ -55,9 +45,9 @@ describe(`${metadataFileName}`, async () => { // TODO: create zod schema for metadata. Move zod schemas to /lib/types.ts assert(metadata.categories.length > 0); metadata.categories.forEach((category) => { - assert.isString(category.name) - assert.isNumber(category.id) - assert.isNumber(category.sort_order) + assert.isString(category.name) + assert.isNumber(category.id) + assert.isNumber(category.sort_order) }); }); }) diff --git a/frontend/src/app/api/categories/route.ts b/frontend/src/app/api/categories/route.ts index b603d9c..2d33a3a 100644 --- a/frontend/src/app/api/categories/route.ts +++ b/frontend/src/app/api/categories/route.ts @@ -5,16 +5,7 @@ import path from "path"; export const dynamic = "force-static"; -const publicJsonPath = path.join(process.cwd(), 'public/json'); -const getJsonDirectory = async () => { - if (!(await fs.stat(publicJsonPath).catch(() => null))) { - throw new Error(`JSON path file not found: ${publicJsonPath}`); - } - const jsonPath = (await fs.readFile(publicJsonPath, "utf-8")).trim(); - return path.resolve(process.cwd(), jsonPath); -}; - -const jsonDir = await getJsonDirectory(); +const jsonDir = "public/json"; const metadataFileName = "metadata.json"; const encoding = "utf-8"; diff --git a/frontend/src/app/data/page.tsx b/frontend/src/app/data/page.tsx deleted file mode 100644 index b752464..0000000 --- a/frontend/src/app/data/page.tsx +++ /dev/null @@ -1,198 +0,0 @@ -"use client"; - -import React, { JSX, useEffect, useState } from "react"; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import ApplicationChart from "../../components/ApplicationChart"; - -interface DataModel { - id: number; - ct_type: number; - disk_size: number; - core_count: number; - ram_size: number; - os_type: string; - os_version: string; - disableip6: string; - nsapp: string; - created_at: string; - method: string; - pve_version: string; - status: string; - error: string; - type: string; - [key: string]: any; -} - -interface SummaryData { - total_entries: number; - status_count: Record; - nsapp_count: Record; -} - -const DataFetcher: React.FC = () => { - const [data, setData] = useState([]); - const [summary, setSummary] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [currentPage, setCurrentPage] = useState(1); - const [itemsPerPage, setItemsPerPage] = useState(25); - const [sortConfig, setSortConfig] = useState<{ key: string; direction: 'ascending' | 'descending' } | null>(null); - - useEffect(() => { - const fetchSummary = async () => { - try { - const response = await fetch("https://api.htl-braunau.at/data/summary"); - if (!response.ok) throw new Error(`Failed to fetch summary: ${response.statusText}`); - const result: SummaryData = await response.json(); - setSummary(result); - } catch (err) { - setError((err as Error).message); - } - }; - - fetchSummary(); - }, []); - - useEffect(() => { - const fetchPaginatedData = async () => { - setLoading(true); - try { - const response = await fetch(`https://api.htl-braunau.at/data/paginated?page=${currentPage}&limit=${itemsPerPage === 0 ? '' : itemsPerPage}`); - if (!response.ok) throw new Error(`Failed to fetch data: ${response.statusText}`); - const result: DataModel[] = await response.json(); - setData(result); - } catch (err) { - setError((err as Error).message); - } finally { - setLoading(false); - } - }; - - fetchPaginatedData(); - }, [currentPage, itemsPerPage]); - - const sortedData = React.useMemo(() => { - if (!sortConfig) return data; - const sorted = [...data].sort((a, b) => { - if (a[sortConfig.key] < b[sortConfig.key]) { - return sortConfig.direction === 'ascending' ? -1 : 1; - } - if (a[sortConfig.key] > b[sortConfig.key]) { - return sortConfig.direction === 'ascending' ? 1 : -1; - } - return 0; - }); - return sorted; - }, [data, sortConfig]); - - if (loading) return

Loading...

; - if (error) return

Error: {error}

; - - const requestSort = (key: string) => { - let direction: 'ascending' | 'descending' = 'ascending'; - if (sortConfig && sortConfig.key === key && sortConfig.direction === 'ascending') { - direction = 'descending'; - } - setSortConfig({ key, direction }); - }; - - const formatDate = (dateString: string): string => { - const date = new Date(dateString); - const year = date.getFullYear(); - const month = date.getMonth() + 1; - const day = date.getDate(); - const hours = String(date.getHours()).padStart(2, '0'); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const timezoneOffset = dateString.slice(-6); - return `${day}.${month}.${year} ${hours}:${minutes} ${timezoneOffset} GMT`; - }; - - return ( -
-

Created LXCs

- -

-
-

{summary?.total_entries} results found

-

Status Legend: 🔄 installing {summary?.status_count["installing"] ?? 0} | ✔️ completed {summary?.status_count["done"] ?? 0} | ❌ failed {summary?.status_count["failed"] ?? 0} | ❓ unknown

-
-
-
- - - - - - - - - - - - - - - - - - - {sortedData.map((item, index) => ( - - - - - - - - - - - - - - - ))} - -
requestSort('status')}>Status requestSort('type')}>Type requestSort('nsapp')}>Application requestSort('os_type')}>OS requestSort('os_version')}>OS Version requestSort('disk_size')}>Disk Size requestSort('core_count')}>Core Count requestSort('ram_size')}>RAM Size requestSort('method')}>Method requestSort('pve_version')}>PVE Version requestSort('error')}>Error Message requestSort('created_at')}>Created At
- {item.status === "done" ? ( - "✔️" - ) : item.status === "failed" ? ( - "❌" - ) : item.status === "installing" ? ( - "🔄" - ) : ( - item.status - )} - {item.type === "lxc" ? ( - "📦" - ) : item.type === "vm" ? ( - "🖥️" - ) : ( - item.type - )}{item.nsapp}{item.os_type}{item.os_version}{item.disk_size}{item.core_count}{item.ram_size}{item.method}{item.pve_version}{item.error}{formatDate(item.created_at)}
-
-
-
- - Page {currentPage} - - -
-
- ); -}; - -export default DataFetcher; diff --git a/json/calibre-web-automated.json b/json/calibre-web-automated.json index 360cca3..0fb22ab 100644 --- a/json/calibre-web-automated.json +++ b/json/calibre-web-automated.json @@ -31,13 +31,13 @@ "password": "admin123" }, "notes": [ - { - "text": "This LXC is not interchangeable with the Calibre-Web LXC", - "type": "warning" - }, - { - "text": "Options enabled by default: Kobo sync; Goodreads author info; metadata extaction from epub, fb2, pdf; cover extraction from cbr, cbz, cbt files" - "type": "info" - } + { + "text": "This LXC is not interchangeable with the Calibre-Web LXC", + "type": "warning" + }, + { + "text": "Options enabled by default: Kobo sync; Goodreads author info; metadata extaction from epub, fb2, pdf; cover extraction from cbr, cbz, cbt files", + "type": "info" + } ] } diff --git a/json/done/inventree.json b/json/done/inventree.json deleted file mode 100644 index 016d0e3..0000000 --- a/json/done/inventree.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "InvenTree", - "slug": "inventree", - "categories": [ - 25 - ], - "date_created": "2025-03-05", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 80, - "documentation": "https://docs.inventree.org/en/latest/", - "website": "https://inventree.org", - "logo": "https://docs.inventree.org/en/latest/assets/logo.png", - "description": "InvenTree is an open-source inventory management system which provides intuitive parts management and stock control. It is designed to be lightweight and easy to use for SME or hobbyist applications.", - "install_methods": [ - { - "type": "default", - "script": "ct/inventree.sh", - "resources": { - "cpu": 2, - "ram": 2048, - "hdd": 6, - "os": "debian", - "version": "12" - } - } - ], - "default_credentials": { - "username": "admin", - "password": "`cat /etc/inventree/admin_password.txt`" - }, - "notes": [ - { - "text": "Please read the documentation for your configuration needs.", - "type": "info" - } - ] -} diff --git a/json/done/seafile.json b/json/done/seafile.json deleted file mode 100644 index 7614fa8..0000000 --- a/json/done/seafile.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "Seafile", - "slug": "Seafile", - "categories": [ - 12 - ], - "date_created": "2025-02-25", - "type": "ct", - "updateable": false, - "privileged": false, - "interface_port": 8000, - "documentation": "https://manual.seafile.com/11.0/deploy", - "website": "https://seafile.com", - "logo": "https://manual.seafile.com/11.0/media/seafile-transparent-1024.png", - "description": "Seafile is an open source file sync and share platform, focusing on reliability and performance.", - "install_methods": [ - { - "type": "default", - "script": "ct/seafile.sh", - "resources": { - "cpu": 2, - "ram": 2048, - "hdd": 20, - "os": "debian", - "version": "12" - } - } - ], - "default_credentials": { - "username": null, - "password": null - }, - "notes": [ - { - "text": "Application credentials: `cat ~/seafile.creds`", - "type": "info" - }, - { - "text": "Change STORAGE_DIR value in `external-storage.sh` and run `bash external-storage.sh` to use your defined storage instead of internal.", - "type": "info" - } - ] -} \ No newline at end of file diff --git a/json/revealjs.json b/json/revealjs.json deleted file mode 100644 index 004b2b2..0000000 --- a/json/revealjs.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "RevealJS", - "slug": "revealjs", - "categories": [ - 12 - ], - "date_created": "2025-03-03", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 8000, - "documentation": "https://github.com/hakimel/reveal.js/wiki", - "website": "https://github.com/hakimel/reveal.js", - "logo": "https://static.slid.es/reveal/logo-v1/reveal-white-text.svg", - "description": "reveal.js is an open source HTML presentation framework. It's a tool that enables anyone with a web browser to create fully-featured and beautiful presentations for free.", - "install_methods": [ - { - "type": "default", - "script": "ct/revealjs.sh", - "resources": { - "cpu": 1, - "ram": 1024, - "hdd": 4, - "os": "debian", - "version": "12" - } - } - ], - "default_credentials": { - "username": null, - "password": null - }, - "notes": [ - { - "text": "Config file is at `/opt/revealjs/gulpfile.js`. Check the documentation for more information.", - "type": "info" - }, - { - "text": "LiveReload is on port: 35729", - "type": "info" - } - ] -} diff --git a/misc/switch_from_VED_to_VE.sh b/misc/switch_from_VED_to_VE.sh new file mode 100644 index 0000000..8496358 --- /dev/null +++ b/misc/switch_from_VED_to_VE.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2025 community-scripts +# Author: MickLesk +# License: MIT +# https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE + +set -eEuo pipefail +BL=$(echo "\033[36m") +RD=$(echo "\033[01;31m") +GN=$(echo "\033[1;92m") +CL=$(echo "\033[m") + +function header_info { + clear + cat <<"EOF" + ____ _ ____________ __ ____ _ ________ + / __ \_________ _ ______ ___ ____ _ _| | / / ____/ __ \ / /_____ / __ \_________ _ ______ ___ ____ _ _| | / / ____/ + / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | / / __/ / / / / / __/ __ \ / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | / / __/ + / ____/ / / /_/ /> < | |/ / /___/ /_/ / / /_/ /_/ / / ____/ / / /_/ /> < | |/ / /___ +/_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| |___/_____/_____/ \__/\____/ /_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| |___/_____/ +EOF +} + +function update_container() { + container=$1 + os=$(pct config "$container" | awk '/^ostype/ {print $2}') + + if [[ "$os" == "ubuntu" || "$os" == "debian" || "$os" == "alpine" ]]; then + echo -e "${BL}[Info]${GN} Checking /usr/bin/update in ${BL}$container${CL} (OS: ${GN}$os${CL})" + + if pct exec "$container" -- [ -e /usr/bin/update ]; then + pct exec "$container" -- bash -c "sed -i 's/ProxmoxVED/ProxmoxVE/g' /usr/bin/update" + + if pct exec "$container" -- grep -q "ProxmoxVE" /usr/bin/update; then + echo -e "${GN}[Success]${CL} /usr/bin/update updated in ${BL}$container${CL}.\n" + else + echo -e "${RD}[Error]${CL} /usr/bin/update in ${BL}$container${CL} could not be updated properly.\n" + fi + else + echo -e "${RD}[Error]${CL} /usr/bin/update not found in container ${BL}$container${CL}.\n" + fi + fi +} + +function update_motd() { + container=$1 + os=$(pct config "$container" | awk '/^ostype/ {print $2}') + + echo -e "${BL}[Debug]${GN} Processing container: ${BL}$container${CL} (OS: ${GN}$os${CL})" + + if [[ "$os" == "ubuntu" || "$os" == "debian" ]]; then + echo -e "${BL}[Debug]${GN} Updating Debian/Ubuntu MOTD in ${BL}$container${CL}" + + pct exec "$container" -- bash -c " + PROFILE_FILE='/etc/profile.d/00_motd.sh' + echo 'echo -e \"\"' > \"\$PROFILE_FILE\" + echo 'echo -e \"🌐 Provided by: community-scripts ORG | GitHub: https://github.com/community-scripts/ProxmoxVE\"' >> \"\$PROFILE_FILE\" + echo 'echo -e \"🖥️ OS: \$(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: \$(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')\"' >> \"\$PROFILE_FILE\" + echo 'echo -e \"🏠 Hostname: \$(hostname)\"' >> \"\$PROFILE_FILE\" + echo 'echo -e \"💡 IP Address: \$(hostname -I | awk '\''{print \$1}'\'')\"' >> \"\$PROFILE_FILE\" + chmod -x /etc/update-motd.d/* + " + + echo -e "${GN}[Debug] Finished Debian/Ubuntu MOTD update for ${BL}$container${CL}" + + elif [[ "$os" == "alpine" ]]; then + echo -e "${BL}[Debug]${GN} Updating Alpine MOTD in ${BL}$container${CL}" + + pct exec "$container" -- /bin/sh -c ' + echo "[Debug] Alpine: Start updating MOTD" > /tmp/motd_debug.log + echo "export TERM=\"xterm-256color\"" >> /root/.bashrc + echo "[Debug] Alpine: Set TERM variable" >> /tmp/motd_debug.log + + IP=$(ip -4 addr show eth0 | awk "/inet / {print \$2}" | cut -d/ -f1 | head -n 1) + echo "[Debug] Alpine: Fetched IP: $IP" >> /tmp/motd_debug.log + + PROFILE_FILE="/etc/profile.d/00_lxc-details.sh" + echo "[Debug] Alpine: Writing to profile file" >> /tmp/motd_debug.log + + echo "echo -e \"\"" > \"$PROFILE_FILE\" + echo "echo -e \" LXC Container\"" >> \"$PROFILE_FILE\" + echo "echo -e \" 🌐 Provided by: community-scripts ORG | GitHub: https://github.com/community-scripts/ProxmoxVE\"" >> \"$PROFILE_FILE\" + echo "[Debug] Alpine: Wrote MOTD header" >> /tmp/motd_debug.log + + echo "echo \"\"" >> \"$PROFILE_FILE\" + echo "echo -e \" 🖥️ OS: $(grep ^NAME /etc/os-release | cut -d= -f2 | tr -d '\"') - Version: $(grep ^VERSION_ID /etc/os-release | cut -d= -f2 | tr -d '\"')\"" >> \"$PROFILE_FILE\" + echo "[Debug] Alpine: Wrote OS details" >> /tmp/motd_debug.log + + echo "echo -e \"🏠 Hostname: $(hostname)\"" >> \"$PROFILE_FILE\" + echo "echo -e \"💡 IP Address: $IP\"" >> \"$PROFILE_FILE\" + echo "[Debug] Alpine: Wrote hostname & IP" >> /tmp/motd_debug.log + ' + + echo -e "${GN}[Debug] Finished Alpine MOTD update for ${BL}$container${CL}" + fi +} + +function remove_dev_tag() { + container=$1 + current_tags=$(pct config "$container" | awk '/^tags/ {print $2}') + + if [[ "$current_tags" == *"dev"* ]]; then + new_tags=$(echo "$current_tags" | sed 's/,*dev,*//g' | sed 's/^,//' | sed 's/,$//') + + if [[ -z "$new_tags" ]]; then + pct set "$container" -delete tags + else + pct set "$container" -tags "$new_tags" + fi + + echo -e "${GN}[Success]${CL} 'dev' tag removed from ${BL}$container${CL}.\n" + fi +} + +header_info +echo "Searching for containers with 'dev' tag..." +for container in $(pct list | awk '{if(NR>1) print $1}'); do + tags=$(pct config "$container" | awk '/^tags/ {print $2}') + if [[ "$tags" == *"dev"* ]]; then + update_container "$container" + update_motd "$container" + remove_dev_tag "$container" + fi +done + +header_info +echo -e "${GN}The process is complete.${CL}\n"