diff --git a/.env b/.env index 6f74106..731ef8e 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -BASE_URL=http://192.168.0.17 -WWW_PORT=8989 -API_PORT=8988 -SEMVER=0.4.4 +APP_URL=http://192.168.0.17:8999 +API_URL=http://192.168.0.17:8998 +APP_PORT=8999 +API_PORT=8998 diff --git a/.github/workflows/Bump App Version.yml b/.github/workflows/Bump App Version.yml index c77994c..d775a62 100644 --- a/.github/workflows/Bump App Version.yml +++ b/.github/workflows/Bump App Version.yml @@ -1,6 +1,11 @@ name: Bump App Semver on: + push: + branches: + - "main" + paths-ignore: + - "frontend/package.json" pull_request: types: - closed @@ -27,14 +32,6 @@ jobs: with: scheme: conventional_commits - - name: Find and Replace in env - uses: thiagodnf/string-replacer@v1.0.2 - with: - find: "SEMVER=.+" - replace: "SEMVER=${{ steps.version.outputs.version }}" - include: | - .env - - name: Find and Replace in package uses: thiagodnf/string-replacer@v1.0.2 with: @@ -47,4 +44,4 @@ jobs: uses: EndBug/add-and-commit@v9 with: message: ${{ github.event.pull_request.title }} - add: '[".env", "frontend/package.json"]' + add: '["frontend/package.json"]' diff --git a/.github/workflows/Tag and Release.yml b/.github/workflows/Tag and Release.yml index 8e35e18..cd32f93 100644 --- a/.github/workflows/Tag and Release.yml +++ b/.github/workflows/Tag and Release.yml @@ -1,11 +1,6 @@ name: Tag and Release on: - push: - branches: - - 'main' - paths: - - '.env' workflow_dispatch: jobs: @@ -22,7 +17,7 @@ jobs: id: version with: scheme: conventional_commits - + - name: Tag and Release uses: BrandonStudio/tag-and-release@v1 with: diff --git a/README.md b/README.md index 5c56216..6fdcc62 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ [![Docker Frontend CI](https://img.shields.io/github/actions/workflow/status/moddroid94/STLVault/Docker%20Backend%20CI.yml?style=for-the-badge&logo=docker&label=Backend)](https://github.com/moddroid94/STLVault/actions/workflows/Docker%20Backend%20CI.yml) [![Docker Pulls](https://img.shields.io/docker/pulls/moddroid94/stlvault-frontend?style=for-the-badge&logo=docker)](https://hub.docker.com/u/moddroid94) - ![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge) **STLVault** is a containerized 3D Model library manager and organizer, designed specifically for 3D printing enthusiasts. It provides a clean, modern web interface to manage your growing collection of STL, STEP, and 3MF files. @@ -49,7 +48,6 @@ ![Model Viewer/Info Preview](https://github.com/user-attachments/assets/db0c4141-51f6-408d-a6c5-9b3df20a3fc7)![ModelViewer2](https://github.com/user-attachments/assets/dc470ef9-0cf3-4f08-b60d-3985d2461576) ![Setting Page](https://github.com/user-attachments/assets/23c703ce-73b0-43bb-9ff4-f4a64c5f7147) - --- ## 🚀 Deployment @@ -64,9 +62,9 @@ services: image: moddroid94/stlvault-backend:latest pull_policy: build environment: - - FILE_STORAGE=/app/uploads - - DB_PATH=/app/data/data.db - - WEBUI_URL=http://192.168.178.21:8999 + - FILE_STORAGE=/app/uploads #DO NOT CHANGE, MODIFY THE BINDS + - DB_PATH=/app/data/data.db #DO NOT CHANGE, MODIFY THE BINDS + - WEBUI_URL: "${APP_URL}" ports: - '8998:8080' volumes: @@ -76,6 +74,9 @@ services: stlvfrontend: image: moddroid94/stlvault-frontend:latest pull_policy: build + environment: + - TERA_API_URL: "${API_URL}" + - TERA_APP_URL: "${APP_URL}" volumes: - node_modules:/app/node_modules ports: @@ -97,14 +98,16 @@ volumes: ``` 2. **Configure Environment:** - Review the `stack.env` file. You can modify the ports or URL if necessary, don't change the SEMVER tag. + Review the `.env` file. You can modify the ports/URL if necessary. ```bash # stack.env example - BASE_URL=http://0.0.0.0 - WWW_PORT=8989 - API_PORT=8988 - SEMVER=x.x.x + APP_URL=http://192.168.0.17:8999 + API_URL=http://192.168.0.17:8998 + APP_PORT=8999 + API_PORT=8998 + UPLOAD_PATH=/your/mount/path + DATA_PATH=/your/mount/otherpath ``` 3. **Start the Stack:** @@ -114,22 +117,16 @@ volumes: ``` 4. **Access the App:** - Open your browser and navigate to `http://localhost:8989` (or the port you configured). + Open your browser and navigate to `http://localhost:8999` (or the port you configured). -### Portainer +### GitOps (Deploy from Repo) -You can deploy STLVault directly from Portainer using the repository as a stack source. +You can deploy STLVault directly from any git deploy compatible docker manager using the repository as a stack source. 1. Create a new **Stack**. 2. Select **Repository** as the build method. 3. Enter the repository URL: `https://github.com/moddroid94/STLVault`. -4. **Environment Variables:** Define the variables below in the Portainer UI (variable substitution will automatically update `stack.env`). - - | Variable | Default | Description | - | :--------- | :--------------- | :-------------------------------- | - | `BASE_URL` | `http://0.0.0.0` | The base URL for the application. | - | `WWW_PORT` | `8989` | Port for the Frontend Web UI. | - | `API_PORT` | `8988` | Port for the Backend API. | +4. **Environment Variables:** Define the environment variables in the Docker Manager UI. --- diff --git a/docker-compose.yml b/docker-compose.yml index 99d36bf..81f0353 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,26 +4,26 @@ services: context: ./backend pull_policy: build environment: - - FILE_STORAGE=/app/uploads - - DB_PATH=/app/data/data.db - - WEBUI_URL=${BASE_URL}:${WWW_PORT} + - FILE_STORAGE=/app/uploads #DO NOT CHANGE, MODIFY THE BINDS + - DB_PATH=/app/data/data.db #DO NOT CHANGE, MODIFY THE BINDS + - WEBUI_URL=${APP_URL} ports: - "${API_PORT}:8080" volumes: - - ./backend/uploads:/app/uploads - - ./backend/data:/app/data + - ${UPLOAD_PATH}:/app/uploads + - ${DATA_PATH}:/app/data restart: always frontend: build: context: ./frontend - args: - - VITE_APP_API=${BASE_URL}:${API_PORT} - - VITE_APP_TAG=${SEMVER} + environment: + - TERA_API_URL=${API_URL} + - TERA_APP_URL=${APP_URL} pull_policy: build volumes: - node_modules:/app/node_modules ports: - - "${WWW_PORT}:5173" + - "${APP_PORT}:5173" depends_on: - backend restart: always diff --git a/frontend/App.tsx b/frontend/App.tsx index ac251fe..5446cb1 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -73,7 +73,7 @@ const App = () => { ); const [importUrl, setImportUrl] = useState(""); const [importFolderId, setImportFolderId] = useState(""); - + const port = import.meta.env.VITE_API_URL; // Delete Confirmation State const [deleteConfirmState, setDeleteConfirmState] = useState<{ isOpen: boolean; @@ -100,7 +100,6 @@ const App = () => { }; fetchData(); }, []); - const port = localStorage.getItem("api-port-override"); // Refresh storage stats when models change (upload, delete, replace) useEffect(() => { diff --git a/frontend/Dockerfile b/frontend/Dockerfile index bd9cc35..fb360f6 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,12 +1,5 @@ FROM node:22-alpine -# Set env variable -ARG VITE_APP_API -ENV VITE_APP_API=$VITE_APP_API - -ARG VITE_APP_TAG -ENV VITE_APP_TAG=$VITE_APP_TAG - WORKDIR /app COPY package.json . @@ -19,4 +12,9 @@ RUN npm run build EXPOSE 5173 -CMD [ "npm", "run", "preview" ] +# Copy the runtime injection script into the container +COPY env.sh /docker-entrypoint.d/env.sh +RUN dos2unix /docker-entrypoint.d/env.sh +RUN chmod +x /docker-entrypoint.d/env.sh + +ENTRYPOINT ["/docker-entrypoint.d/env.sh"] diff --git a/frontend/components/Settings.tsx b/frontend/components/Settings.tsx index c9d3e3f..e089260 100644 --- a/frontend/components/Settings.tsx +++ b/frontend/components/Settings.tsx @@ -37,7 +37,7 @@ const Settings: React.FC = ({ onBack }) => { }); const [selectedApiPort, setSelectedApiPort] = useState(() => { - const envport = import.meta.env.VITE_APP_API; + const envport = import.meta.env.VITE_API_URL; const port = localStorage.getItem("api-port-override"); if (port) { setApiPortStatus(true); @@ -184,8 +184,6 @@ const Settings: React.FC = ({ onBack }) => { - - diff --git a/frontend/components/Sidebar.tsx b/frontend/components/Sidebar.tsx index c9ba9c1..33d4596 100644 --- a/frontend/components/Sidebar.tsx +++ b/frontend/components/Sidebar.tsx @@ -36,7 +36,7 @@ import Button from "@mui/material/Button"; import OutlinedInput from "@mui/material/OutlinedInput"; import Badge from "@mui/material/Badge"; -const APP_TAG = import.meta.env.VITE_APP_TAG || __APP_VERSION__ || "dev"; +const APP_TAG = import.meta.env.VITE_APP_TAG || "dev"; interface SidebarProps { folders: Folder[]; @@ -75,7 +75,7 @@ const Sidebar: React.FC = ({ const [expandedIds, setExpandedIds] = useState>(new Set()); const [editingId, setEditingId] = useState(null); const [creatingSubfolderId, setCreatingSubfolderId] = useState( - null + null, ); const [dragTargetId, setDragTargetId] = useState(null); @@ -89,7 +89,7 @@ const Sidebar: React.FC = ({ e.preventDefault(); // Prevent text selection setIsResizing(true); }, - [isDesktopVariant] + [isDesktopVariant], ); // Calculate direct counts only (not recursive, matching file system behavior usually) @@ -297,7 +297,7 @@ const Sidebar: React.FC = ({ onClick={onPlusClick} aria-label="select item" size="small" - sx={{ color: 'grey.300' }} + sx={{ color: "grey.300" }} > @@ -306,7 +306,7 @@ const Sidebar: React.FC = ({ aria-label="select item" size="small" edge="end" - sx={{ color: 'grey.300' }} + sx={{ color: "grey.300" }} > @@ -317,7 +317,7 @@ const Sidebar: React.FC = ({ const CustomTreeItem = React.forwardRef(function CustomTreeItem( props: TreeItemProps, - ref: React.Ref + ref: React.Ref, ) { const { interactions, status } = useTreeItemUtils({ itemId: props.itemId, @@ -325,7 +325,7 @@ const Sidebar: React.FC = ({ }); const handleContentClick: UseTreeItemContentSlotOwnProps["onClick"] = ( - event + event, ) => { onSelectFolder(props.itemId); }; diff --git a/frontend/components/Viewer3D.tsx b/frontend/components/Viewer3D.tsx index 319e9d5..eb23873 100644 --- a/frontend/components/Viewer3D.tsx +++ b/frontend/components/Viewer3D.tsx @@ -36,7 +36,7 @@ let API_BASE_URL = ""; if (localStorage.getItem("api-port-override")) { API_BASE_URL = localStorage.getItem("api-port-override"); } else { - const url = import.meta.env.VITE_APP_API; + const url = import.meta.env.VITE_API_URL; API_BASE_URL = url; } diff --git a/frontend/env.sh b/frontend/env.sh new file mode 100644 index 0000000..a42c7c8 --- /dev/null +++ b/frontend/env.sh @@ -0,0 +1,15 @@ +#!/bin/sh +# Replace placeholders in JS files with actual env vars +for i in $(env | grep '^TERA_'); do + key=$(echo $i | cut -d '=' -f 1) + value=$(echo $i | cut -d '=' -f 2-) + echo "Replacing static $key with $value" + find /app/dist -type f -name '*.js' -exec sed -i "s|${key}|${value}|g" '{}' + +done +for i in $(env | grep '^TERA_'); do + key=$(echo $i | cut -d '=' -f 1) + value=$(echo $i | cut -d '=' -f 2-) + echo "Replacing config $key with $value" + find /app -type f -name 'vite.config.ts' -exec sed -i "s|${key}|${value}|g" '{}' + +done +exec npm run preview \ No newline at end of file diff --git a/frontend/services/api.ts b/frontend/services/api.ts index d64f1fd..0018284 100644 --- a/frontend/services/api.ts +++ b/frontend/services/api.ts @@ -6,7 +6,7 @@ let API_BASE_URL = ""; if (localStorage.getItem("api-port-override")) { API_BASE_URL = localStorage.getItem("api-port-override") + "/api"; } else { - const url = import.meta.env.VITE_APP_API + "/api"; + const url = import.meta.env.VITE_API_URL + "/api"; API_BASE_URL = url; } @@ -23,7 +23,7 @@ export const api = { // 2. CREATE Folder createFolder: async ( name: string, - parentId: string | null = null + parentId: string | null = null, ): Promise => { const res = await fetch(`${API_BASE_URL}/folders`, { method: "POST", @@ -66,7 +66,7 @@ export const api = { file: File, folderId: string, thumbnail?: string, - tags: string[] = [] + tags: string[] = [], ): Promise => { const formData = new FormData(); formData.append("file", file); @@ -86,7 +86,7 @@ export const api = { // 7. UPDATE Model updateModel: async ( id: string, - updates: Partial + updates: Partial, ): Promise => { const res = await fetch(`${API_BASE_URL}/models/${id}`, { method: "PATCH", @@ -187,7 +187,14 @@ export const api = { const res = await fetch(`${API_BASE_URL}/printables/importid`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ id, name, parentId, previewPath, folderId, typeName }), + body: JSON.stringify({ + id, + name, + parentId, + previewPath, + folderId, + typeName, + }), }); if (!res.ok) throw new Error("Import failed"); return res.json(); @@ -197,7 +204,7 @@ export const api = { replaceModelFile: async ( id: string, file: File, - thumbnail?: string + thumbnail?: string, ): Promise => { const formData = new FormData(); formData.append("file", file); @@ -212,10 +219,7 @@ export const api = { }, // 14a. REPLACE FILE - replaceModelThumbnail: async ( - id: string, - file: File, - ): Promise => { + replaceModelThumbnail: async (id: string, file: File): Promise => { const formData = new FormData(); formData.append("file", file); diff --git a/frontend/types.ts b/frontend/types.ts index 9023097..2b76955 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -1,14 +1,13 @@ - // Vite environment variables type declaration declare global { + interface ImportMetaEnv { + readonly VITE_APP_TAG: string; + readonly VITE_API_URL: string; + } + interface ImportMeta { - env: { - VITE_APP_TAG: string; - [key: string]: any; - }; + readonly env: ImportMetaEnv; } - - const __APP_VERSION__: string; } export interface Folder { @@ -46,8 +45,8 @@ export interface StorageStats { } export enum ViewMode { - GRID = 'GRID', - LIST = 'LIST', + GRID = "GRID", + LIST = "LIST", } export type AppState = { @@ -56,4 +55,4 @@ export type AppState = { currentFolderId: string; selectedModelId: string | null; sidebarOpen: boolean; -}; \ No newline at end of file +}; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 5ddb6a2..2206044 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -6,22 +6,25 @@ import react from "@vitejs/plugin-react"; export default defineConfig(({ mode }) => { const env = loadEnv(mode, ".", ""); const pkgJson = JSON.parse( - fs.readFileSync(new URL("./package.json", import.meta.url), "utf-8") + fs.readFileSync(new URL("./package.json", import.meta.url), "utf-8"), ); - const appVersion = env.VITE_APP_TAG || pkgJson.version || "dev"; + const appVersion = pkgJson.version || "dev"; + const API_URL = "TERA_API_URL"; return { base: "/", preview: { port: 5173, + allowedHosts: ["TERA_APP_URL"], }, server: { port: 5173, host: "0.0.0.0", }, - plugins: [react()], define: { - __APP_VERSION__: JSON.stringify(appVersion), + "import.meta.env.VITE_APP_TAG": JSON.stringify(appVersion), + "import.meta.env.VITE_API_URL": JSON.stringify(API_URL), }, + plugins: [react()], resolve: { alias: { "@": path.resolve(__dirname, "."),