From 19afa660dada2c2b935bd3c7764928404fb66a2c Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 13 Jul 2025 02:10:52 +0200 Subject: [PATCH 1/5] Fix form border --- .../js/components/Display/InlineForm.tsx | 91 ++++++++++--------- resources/js/components/Display/StatsBox.tsx | 10 +- resources/js/components/ui/ComponentTitle.tsx | 15 +++ resources/js/pages/dashboard.tsx | 37 ++++---- 4 files changed, 85 insertions(+), 68 deletions(-) create mode 100644 resources/js/components/ui/ComponentTitle.tsx diff --git a/resources/js/components/Display/InlineForm.tsx b/resources/js/components/Display/InlineForm.tsx index 9d6b693..fe189bc 100644 --- a/resources/js/components/Display/InlineForm.tsx +++ b/resources/js/components/Display/InlineForm.tsx @@ -3,6 +3,7 @@ import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm'; import { cn } from '@/lib/utils'; import { X } from 'lucide-react'; +import ComponentTitle from '@/components/ui/ComponentTitle'; interface InlineFormProps { type: 'purchase' | 'milestone' | 'price' | null; @@ -13,66 +14,66 @@ interface InlineFormProps { className?: string; } -export default function InlineForm({ - type, - onClose, +export default function InlineForm({ + type, + onClose, onPurchaseSuccess, onMilestoneSuccess, onPriceSuccess, - className + className }: InlineFormProps) { if (!type) return null; const title = type === 'purchase' ? 'ADD PURCHASE' : type === 'milestone' ? 'ADD MILESTONE' : 'UPDATE PRICE'; return ( -
{/* Header */} -
-

- {title} -

- -
+
+
+ {title} - {/* Form Content */} -
- {type === 'purchase' ? ( - { - if (onPurchaseSuccess) onPurchaseSuccess(); - onClose(); - }} - /> - ) : type === 'milestone' ? ( - { - if (onMilestoneSuccess) onMilestoneSuccess(); - onClose(); - }} - /> - ) : ( - { - if (onPriceSuccess) onPriceSuccess(); - onClose(); - }} - /> - )} + +
+ + {/* Form Content */} +
+ {type === 'purchase' ? ( + { + if (onPurchaseSuccess) onPurchaseSuccess(); + onClose(); + }} + /> + ) : type === 'milestone' ? ( + { + if (onMilestoneSuccess) onMilestoneSuccess(); + onClose(); + }} + /> + ) : ( + { + if (onPriceSuccess) onPriceSuccess(); + onClose(); + }} + /> + )} +
); -} \ No newline at end of file +} diff --git a/resources/js/components/Display/StatsBox.tsx b/resources/js/components/Display/StatsBox.tsx index eafa4aa..1fc6a23 100644 --- a/resources/js/components/Display/StatsBox.tsx +++ b/resources/js/components/Display/StatsBox.tsx @@ -1,6 +1,7 @@ import { cn } from '@/lib/utils'; import { Plus, ChevronRight } from 'lucide-react'; import { useState } from 'react'; +import ComponentTitle from '@/components/ui/ComponentTitle'; interface Milestone { target: number; @@ -72,9 +73,8 @@ export default function StatsBox({
{/* STATS Title and Current Price */}
-

- STATS -

+ Stats +
{stats.currentPrice && (
@@ -185,8 +185,8 @@ export default function StatsBox({ key={index} className={cn( isSelectedMilestone - ? "text-red-500 font-bold" - : "bg-red-500 text-black" + ? "bg-red-500 text-black" + : "text-red-500 font-bold" )} > diff --git a/resources/js/components/ui/ComponentTitle.tsx b/resources/js/components/ui/ComponentTitle.tsx new file mode 100644 index 0000000..7c39a62 --- /dev/null +++ b/resources/js/components/ui/ComponentTitle.tsx @@ -0,0 +1,15 @@ +import { FC, ReactNode } from 'react'; + +interface ComponentTitleProps { + children: ReactNode; +} + +const ComponentTitle: FC = ({ children }) => { + return ( +

+ { children } +

+ ) +} + +export default ComponentTitle diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 6753014..23c7449 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -27,11 +27,11 @@ export default function Dashboard() { total_investment: 0, average_cost_per_share: 0, }); - + const [priceData, setPriceData] = useState({ current_price: null, }); - + const [milestones, setMilestones] = useState([]); const [selectedMilestoneIndex, setSelectedMilestoneIndex] = useState(0); const [showProgressBar, setShowProgressBar] = useState(false); @@ -121,14 +121,14 @@ export default function Dashboard() { // Calculate portfolio stats - const currentValue = priceData.current_price - ? purchaseData.total_shares * priceData.current_price + const currentValue = priceData.current_price + ? purchaseData.total_shares * priceData.current_price : undefined; - - const profitLoss = currentValue - ? currentValue - purchaseData.total_investment + + const profitLoss = currentValue + ? currentValue - purchaseData.total_investment : undefined; - + const profitLossPercentage = profitLoss && purchaseData.total_investment > 0 ? (profitLoss / purchaseData.total_investment) * 100 : undefined; @@ -168,36 +168,37 @@ export default function Dashboard() { const handleProgressClick = () => { setShowStatsBox(!showStatsBox); + setActiveForm(null) }; return ( <> - + {/* Stacked Layout */}
{/* Box 1: LED Number Display - Fixed position from top */}
-
- + {/* Box 2: Progress Bar (toggleable) */} -
- +
- + {/* Box 3: Stats Box (toggleable) */} -
- + setActiveForm('price')} />
- + {/* Box 4: Forms (only when active form is set) */} -
+
setActiveForm(null)} From c4c21f689c4934b46360c473d518ef1d6c346559 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 13 Jul 2025 04:06:37 +0200 Subject: [PATCH 2/5] Style forms --- resources/css/app.css | 9 +++ .../js/components/Display/InlineForm.tsx | 17 ++---- .../js/components/Display/ProgressBar.tsx | 2 +- resources/js/components/Display/StatsBox.tsx | 2 +- .../Milestones/AddMilestoneForm.tsx | 45 +++++++++----- .../js/components/Pricing/UpdatePriceForm.tsx | 59 +++++++++++-------- .../Transactions/AddPurchaseForm.tsx | 59 ++++++++++++------- 7 files changed, 118 insertions(+), 75 deletions(-) diff --git a/resources/css/app.css b/resources/css/app.css index 912a683..1760079 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -16,6 +16,15 @@ .font-digital { font-family: '7Segment', monospace; } +.glow-red { + box-shadow: 0 0 20px rgba(239, 68, 68, 0.4); + transition: box-shadow 300ms ease; +} + +.glow-red:hover { + box-shadow: 0 0 25px rgba(239, 68, 68, 0.6); +} + @custom-variant dark (&:is(.dark *)); @theme { diff --git a/resources/js/components/Display/InlineForm.tsx b/resources/js/components/Display/InlineForm.tsx index fe189bc..7dd4ca8 100644 --- a/resources/js/components/Display/InlineForm.tsx +++ b/resources/js/components/Display/InlineForm.tsx @@ -2,7 +2,6 @@ import AddMilestoneForm from '@/components/Milestones/AddMilestoneForm'; import AddPurchaseForm from '@/components/Transactions/AddPurchaseForm'; import UpdatePriceForm from '@/components/Pricing/UpdatePriceForm'; import { cn } from '@/lib/utils'; -import { X } from 'lucide-react'; import ComponentTitle from '@/components/ui/ComponentTitle'; interface InlineFormProps { @@ -35,18 +34,7 @@ export default function InlineForm({ )} > {/* Header */} -
-
- {title} - - -
+
{/* Form Content */}
@@ -56,6 +44,7 @@ export default function InlineForm({ if (onPurchaseSuccess) onPurchaseSuccess(); onClose(); }} + onCancel={onClose} /> ) : type === 'milestone' ? ( ) : ( )}
diff --git a/resources/js/components/Display/ProgressBar.tsx b/resources/js/components/Display/ProgressBar.tsx index 05fdf7e..4343cb5 100644 --- a/resources/js/components/Display/ProgressBar.tsx +++ b/resources/js/components/Display/ProgressBar.tsx @@ -43,7 +43,7 @@ export default function ProgressBar({ {/* Progress Bar Container */}
{/* Old-school progress bar with overlaid text */} -
+
{/* Inner container */}
{/* Progress fill */} diff --git a/resources/js/components/Display/StatsBox.tsx b/resources/js/components/Display/StatsBox.tsx index 1fc6a23..4d5a59f 100644 --- a/resources/js/components/Display/StatsBox.tsx +++ b/resources/js/components/Display/StatsBox.tsx @@ -70,7 +70,7 @@ export default function StatsBox({ className )} > -
+
{/* STATS Title and Current Price */}
Stats diff --git a/resources/js/components/Milestones/AddMilestoneForm.tsx b/resources/js/components/Milestones/AddMilestoneForm.tsx index 629831f..8433bf0 100644 --- a/resources/js/components/Milestones/AddMilestoneForm.tsx +++ b/resources/js/components/Milestones/AddMilestoneForm.tsx @@ -5,6 +5,7 @@ import InputError from '@/components/InputError'; import { useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; +import ComponentTitle from '@/components/ui/ComponentTitle'; interface MilestoneFormData { target: string; @@ -14,9 +15,10 @@ interface MilestoneFormData { interface AddMilestoneFormProps { onSuccess?: () => void; + onCancel?: () => void; } -export default function AddMilestoneForm({ onSuccess }: AddMilestoneFormProps) { +export default function AddMilestoneForm({ onSuccess, onCancel }: AddMilestoneFormProps) { const { data, setData, post, processing, errors, reset } = useForm({ target: '', description: '', @@ -36,11 +38,12 @@ export default function AddMilestoneForm({ onSuccess }: AddMilestoneFormProps) { }; return ( -
+
+ ADD MILESTONE
- + setData('target', e.target.value)} - className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30" + className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red" />
- + setData('description', e.target.value)} - className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30" + className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red" />
- +
+ + {onCancel && ( + + )} +
); -} \ No newline at end of file +} diff --git a/resources/js/components/Pricing/UpdatePriceForm.tsx b/resources/js/components/Pricing/UpdatePriceForm.tsx index bd18cb9..fe99e3b 100644 --- a/resources/js/components/Pricing/UpdatePriceForm.tsx +++ b/resources/js/components/Pricing/UpdatePriceForm.tsx @@ -1,11 +1,11 @@ import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import InputError from '@/components/InputError'; import { useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; +import ComponentTitle from '@/components/ui/ComponentTitle'; interface PriceUpdateFormData { date: string; @@ -17,9 +17,10 @@ interface UpdatePriceFormProps { currentPrice?: number; className?: string; onSuccess?: () => void; + onCancel?: () => void; } -export default function UpdatePriceForm({ currentPrice, className, onSuccess }: UpdatePriceFormProps) { +export default function UpdatePriceForm({ currentPrice, className, onSuccess, onCancel }: UpdatePriceFormProps) { const { data, setData, post, processing, errors } = useForm({ date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format price: currentPrice?.toString() || '', @@ -38,31 +39,30 @@ export default function UpdatePriceForm({ currentPrice, className, onSuccess }: }; return ( - - - Update Asset Price +
+
+ UPDATE PRICE {currentPrice && ( -

- Current price: €{currentPrice.toFixed(4)} +

+ [CURRENT] €{currentPrice.toFixed(4)}

)} - -
- + setData('date', e.target.value)} max={new Date().toISOString().split('T')[0]} + className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none transition-all glow-red" />
- + setData('price', e.target.value)} + className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none focus:shadow-[0_0_10px_rgba(239,68,68,0.5)] placeholder:text-red-400/40 transition-all" /> -

- Price per unit/share of the asset +

+ [UNIT] price per share

- +
+ + {onCancel && ( + + )} +
-
- +
+
); } diff --git a/resources/js/components/Transactions/AddPurchaseForm.tsx b/resources/js/components/Transactions/AddPurchaseForm.tsx index 68732b2..c360177 100644 --- a/resources/js/components/Transactions/AddPurchaseForm.tsx +++ b/resources/js/components/Transactions/AddPurchaseForm.tsx @@ -5,6 +5,7 @@ import InputError from '@/components/InputError'; import { useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler, useEffect } from 'react'; +import ComponentTitle from '@/components/ui/ComponentTitle'; interface PurchaseFormData { date: string; @@ -16,9 +17,10 @@ interface PurchaseFormData { interface AddPurchaseFormProps { onSuccess?: () => void; + onCancel?: () => void; } -export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) { +export default function AddPurchaseForm({ onSuccess, onCancel }: AddPurchaseFormProps) { const { data, setData, post, processing, errors, reset } = useForm({ date: new Date().toISOString().split('T')[0], // Today's date in YYYY-MM-DD format shares: '', @@ -31,7 +33,7 @@ export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) { if (data.shares && data.price_per_share) { const shares = parseFloat(data.shares); const pricePerShare = parseFloat(data.price_per_share); - + if (!isNaN(shares) && !isNaN(pricePerShare)) { const totalCost = (shares * pricePerShare).toFixed(2); setData('total_cost', totalCost); @@ -54,24 +56,25 @@ export default function AddPurchaseForm({ onSuccess }: AddPurchaseFormProps) { }; return ( -
+
+ ADD PURCHASE
- + setData('date', e.target.value)} max={new Date().toISOString().split('T')[0]} - className="bg-black border-red-500/30 text-red-400 focus:border-red-400" + className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none transition-all glow-red" />
- + setData('shares', e.target.value)} - className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30" + className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red" />
- + setData('price_per_share', e.target.value)} - className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30" + className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red" />
- + setData('total_cost', e.target.value)} - className="bg-black border-red-500/30 text-red-400 focus:border-red-400 placeholder:text-red-400/30" + className="bg-black border-red-500 text-red-400 focus:border-red-300 font-mono text-sm rounded-none border-2 focus:ring-0 focus:outline-none placeholder:text-red-400/40 transition-all glow-red" /> -

- Auto-calculated from shares × price +

+ [AUTO-CALC] shares × price

- +
+ + {onCancel && ( + + )} +
); -} \ No newline at end of file +} From 1fd3fa3f1893a8676869ad483bc3976b246b70a5 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 13 Jul 2025 12:10:09 +0200 Subject: [PATCH 3/5] Containerize application --- docker/Dockerfile | 84 +++++++++++++++++++++++++++++++++++++++ docker/docker-compose.yml | 65 ++++++++++++++++++++++++++++++ docker/nginx.conf | 26 ++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.yml create mode 100644 docker/nginx.conf diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..9d46aca --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,84 @@ +# Multi-stage build for Laravel + React application +FROM node:20-alpine AS frontend-builder + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install Node dependencies +RUN npm ci --only=production + +# Copy frontend source +COPY resources/ resources/ +COPY public/ public/ +COPY vite.config.ts ./ +COPY tsconfig.json ./ +COPY components.json ./ +COPY eslint.config.js ./ + +# Build frontend assets +RUN npm run build + +# PHP runtime stage +FROM php:8.2-fpm-alpine + +# Install system dependencies +RUN apk add --no-cache \ + git \ + curl \ + libpng-dev \ + libxml2-dev \ + zip \ + unzip \ + oniguruma-dev \ + mysql-client + +# Install PHP extensions +RUN docker-php-ext-install \ + pdo_mysql \ + mbstring \ + exif \ + pcntl \ + bcmath \ + gd + +# Install Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Set working directory +WORKDIR /var/www/html + +# Copy application code first +COPY . . + +# Install PHP dependencies after copying all files +RUN composer install --no-dev --optimize-autoloader --no-interaction + +# Copy built frontend assets from builder stage +COPY --from=frontend-builder /app/public/build/ ./public/build/ + +# Set proper permissions +RUN chown -R www-data:www-data /var/www/html \ + && chmod -R 755 /var/www/html/storage \ + && chmod -R 755 /var/www/html/bootstrap/cache + +# Create a startup script +RUN echo '#!/bin/sh' > /usr/local/bin/start-app \ + && echo 'cp -r /var/www/html/public/. /var/www/html/public_shared/ 2>/dev/null || true' >> /usr/local/bin/start-app \ + && echo 'php artisan config:cache' >> /usr/local/bin/start-app \ + && echo 'php artisan route:cache' >> /usr/local/bin/start-app \ + && echo 'php artisan view:cache' >> /usr/local/bin/start-app \ + && echo 'php artisan migrate --force' >> /usr/local/bin/start-app \ + && echo 'php-fpm' >> /usr/local/bin/start-app \ + && chmod +x /usr/local/bin/start-app + +# Expose port 9000 for PHP-FPM +EXPOSE 9000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD php artisan --version || exit 1 + +# Start the application +CMD ["/usr/local/bin/start-app"] \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..a133062 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,65 @@ +version: '3.8' + +services: + app: + image: codeberg.org/lvl0/incr:v0.1.0-alpha-1 + # build: + # context: ../ + # dockerfile: docker/Dockerfile + container_name: incr-app + restart: unless-stopped + working_dir: /var/www/html + environment: + - APP_ENV=production + - APP_DEBUG=false + - DB_CONNECTION=mysql + - DB_HOST=db + - DB_PORT=3306 + - DB_DATABASE=incr + - DB_USERNAME=incr_user + - DB_PASSWORD=incr_password + volumes: + - ../storage:/var/www/html/storage + - ../public:/var/www/html/public + depends_on: + - db + networks: + - incr-network + + db: + image: mysql:8.0 + container_name: incr-db + restart: unless-stopped + environment: + - MYSQL_DATABASE=incr + - MYSQL_USER=incr_user + - MYSQL_PASSWORD=incr_password + - MYSQL_ROOT_PASSWORD=root_password + volumes: + - db_data:/var/lib/mysql + ports: + - "3306:3306" + networks: + - incr-network + + nginx: + image: nginx:alpine + container_name: incr-nginx + restart: unless-stopped + ports: + - "80:80" + volumes: + - ../public:/var/www/html/public:ro + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - app + networks: + - incr-network + +networks: + incr-network: + driver: bridge + +volumes: + db_data: + driver: local \ No newline at end of file diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..3877706 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,26 @@ +server { + listen 80; + server_name localhost; + root /var/www/html/public; + index index.php index.html; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass app:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + } + + location ~ /\.ht { + deny all; + } + + location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} \ No newline at end of file From fb5ea6a2dcbc3ce72e7874b494dcd4c83c8c197a Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 13 Jul 2025 12:18:58 +0200 Subject: [PATCH 4/5] Add codeberg CI instructions --- .forgejo/workflows/docker-build.yml | 43 +++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .forgejo/workflows/docker-build.yml diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml new file mode 100644 index 0000000..6b0bf0a --- /dev/null +++ b/.forgejo/workflows/docker-build.yml @@ -0,0 +1,43 @@ +name: Build and Push Docker Image + +on: + push: + tags: [v*] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Codeberg Container Registry + uses: docker/login-action@v3 + with: + registry: codeberg.org + username: ${{ secrets.CODEBERG_USERNAME }} + password: ${{ secrets.CODEBERG_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: codeberg.org/lvl0/incr + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max \ No newline at end of file From 6196668d8368cf18a72504f7a11015770a511e34 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 13 Jul 2025 12:21:01 +0200 Subject: [PATCH 5/5] Add manual dispatch --- .forgejo/workflows/docker-build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/docker-build.yml b/.forgejo/workflows/docker-build.yml index 6b0bf0a..46f554d 100644 --- a/.forgejo/workflows/docker-build.yml +++ b/.forgejo/workflows/docker-build.yml @@ -3,6 +3,7 @@ name: Build and Push Docker Image on: push: tags: [v*] + workflow_dispatch: jobs: build: