Compare commits

...

2 Commits

Author SHA1 Message Date
Egor Pozharov
4110083019 refine nginx caching strategy and add service worker update prompt with periodic checks
Some checks failed
Build and Push Docker Images / build-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (push) Has been cancelled
2026-04-21 12:55:01 +06:00
Egor Pozharov
60dea22ced add git hooks for commit validation, auto-suggestions, and Docker build/push automation on git push 2026-04-21 12:54:39 +06:00
11 changed files with 232 additions and 20 deletions

27
.githooks/commit-msg Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
# Commit message hook: validate commit message format
COMMIT_MSG_FILE=$1
MSG=$(head -n1 "$COMMIT_MSG_FILE")
# Skip validation for merge commits
if echo "$MSG" | grep -q "^Merge"; then
exit 0
fi
# Check message length
if [ ${#MSG} -lt 5 ]; then
echo "❌ Commit message too short (min 5 characters)"
exit 1
fi
# Optional: enforce conventional commits format
# Uncomment if you want to enforce prefixes like "feat:", "fix:", etc.
# if ! echo "$MSG" | grep -qE "^(feat|fix|docs|style|refactor|test|chore|backend|frontend|deploy)(\(.+\))?:"; then
# echo "❌ Commit message should follow format: type: description"
# echo " Allowed types: feat, fix, docs, style, refactor, test, chore, backend, frontend, deploy"
# exit 1
# fi
echo "✅ Commit message OK"
exit 0

17
.githooks/post-commit Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
# Post-commit hook: show info after successful commit
COMMIT_HASH=$(git rev-parse --short HEAD)
COMMIT_MSG=$(git log -1 --pretty=%s)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo ""
echo "✅ Commit created: $COMMIT_HASH"
echo "📝 Message: $COMMIT_MSG"
echo "🌿 Branch: $BRANCH"
echo ""
# Remind to push if needed
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
echo "💡 Tip: Run 'git push' to trigger deployment"
fi

37
.githooks/pre-push Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# Pre-push hook: build and push Docker images (blocks until complete)
# This ensures push only succeeds after successful build
REGISTRY="gitea.chedius.ru/chedius"
PLATFORM="linux/amd64"
echo "🔨 Building Docker images..."
# Build backend
docker build --platform $PLATFORM -t $REGISTRY/delivery-tracker/backend:latest ./backend || {
echo "❌ Backend build failed"
exit 1
}
# Build frontend
docker build --platform $PLATFORM -t $REGISTRY/delivery-tracker/frontend:latest ./frontend || {
echo "❌ Frontend build failed"
exit 1
}
echo "📤 Pushing Docker images..."
# Push backend
docker push $REGISTRY/delivery-tracker/backend:latest || {
echo "❌ Backend push failed"
exit 1
}
# Push frontend
docker push $REGISTRY/delivery-tracker/frontend:latest || {
echo "❌ Frontend push failed"
exit 1
}
echo "✅ Docker images built and pushed successfully"
echo "📡 Git push will now proceed..."

46
.githooks/prepare-commit-msg Executable file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
# Prepare commit message hook: auto-generate commit message based on changes
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
# Only suggest message for regular commits (not merge, squash, etc.)
if [ -z "$COMMIT_SOURCE" ] || [ "$COMMIT_SOURCE" = "message" ]; then
# Get list of changed files
CHANGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$CHANGED_FILES" ]; then
exit 0
fi
# Determine commit type based on changed files
if echo "$CHANGED_FILES" | grep -q "backend/"; then
PREFIX="backend:"
elif echo "$CHANGED_FILES" | grep -q "frontend/"; then
PREFIX="frontend:"
elif echo "$CHANGED_FILES" | grep -q "\.github\|\.gitea\|deploy\|docker"; then
PREFIX="deploy:"
else
PREFIX="chore:"
fi
# Count files
FILE_COUNT=$(echo "$CHANGED_FILES" | wc -l | tr -d ' ')
# Generate suggested message
if [ "$FILE_COUNT" -eq 1 ]; then
FILENAME=$(basename "$CHANGED_FILES")
SUGGESTED_MSG="$PREFIX update $FILENAME"
else
SUGGESTED_MSG="$PREFIX update $FILE_COUNT files"
fi
# If message file is empty or has default template, add suggestion
if [ ! -s "$COMMIT_MSG_FILE" ] || ! grep -v '^#' "$COMMIT_MSG_FILE" | grep -q '[^[:space:]]'; then
echo "$SUGGESTED_MSG" > "$COMMIT_MSG_FILE"
echo "" >> "$COMMIT_MSG_FILE"
echo "# Suggested commit message generated based on changes" >> "$COMMIT_MSG_FILE"
echo "# Changed files:" >> "$COMMIT_MSG_FILE"
echo "$CHANGED_FILES" | sed 's/^/# /' >> "$COMMIT_MSG_FILE"
fi
fi

View File

@@ -3,7 +3,7 @@ REGISTRY = gitea.chedius.ru/chedius
PLATFORM = linux/amd64
# Build and push both services
.PHONY: all build push deploy
.PHONY: all build push deploy install-hooks
all: build push
@@ -30,11 +30,16 @@ update-server:
docker pull $(REGISTRY)/delivery-tracker/frontend:latest
docker-compose up -d --force-recreate backend frontend
# Full workflow: commit, build, push
# Install git hooks for automation
install-hooks:
git config core.hooksPath .githooks
chmod +x .githooks/*
@echo "✅ Git hooks installed from .githooks/"
# Full release: commit, push, and auto-build via hook
release:
@if [ -z "$(MSG)" ]; then echo "Usage: make release MSG='commit message'"; exit 1; fi
git add -A
git commit -m "$(MSG)" || true
git push
$(MAKE) build push
@echo "Released! Watchtower will deploy within 60 seconds."
@echo "✅ Released! Watchtower will deploy within 60 seconds."

View File

@@ -10,12 +10,45 @@ server {
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# Hashed build assets — safe to cache forever
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Other static files (icons, fonts at root etc.) — short cache
location ~* \.(png|jpg|jpeg|gif|ico|woff|woff2|ttf|eot)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
}
# Never cache entry points — must always revalidate so clients
# can detect new frontend versions and service worker updates
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires 0;
}
location = /sw.js {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires 0;
}
location = /registerSW.js {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires 0;
}
location = /manifest.webmanifest {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires 0;
}
location = /manifest.json {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires 0;
}
# Proxy API requests to backend
location /api/ {
proxy_pass http://backend:8080;

View File

@@ -4,6 +4,7 @@ import { DeliveryForm } from './components/delivery/DeliveryForm';
import { LoginForm } from './components/auth/LoginForm';
import { ToastContainer } from './components/ui/Toast';
import { Button } from './components/ui/Button';
import { UpdatePrompt } from './components/ui/UpdatePrompt';
import { useDeliveryStore } from './stores/deliveryStore';
import { useAuthStore } from './stores/authStore';
@@ -89,6 +90,7 @@ function App() {
if (!isAuthenticated) {
return (
<>
<UpdatePrompt />
<LoginForm />
<ToastContainer />
</>
@@ -97,6 +99,7 @@ function App() {
return (
<div className="min-h-screen bg-[#fbf8fb]">
<UpdatePrompt />
<header className="sticky top-0 z-40 bg-[#1B263B] text-white shadow-md">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-14">

View File

@@ -0,0 +1,55 @@
import { useRegisterSW } from 'virtual:pwa-register/react';
import { RefreshCw } from 'lucide-react';
import { Button } from './Button';
// Check for SW updates every hour and on tab focus/visibility change
const UPDATE_INTERVAL_MS = 60 * 60 * 1000;
export function UpdatePrompt() {
const {
needRefresh: [needRefresh],
updateServiceWorker,
} = useRegisterSW({
onRegisteredSW(_swUrl, registration) {
if (!registration) return;
const checkForUpdate = async () => {
if (registration.installing || !navigator) return;
if ('connection' in navigator && !navigator.onLine) return;
try {
await registration.update();
} catch {
// network error — ignore, will retry
}
};
setInterval(checkForUpdate, UPDATE_INTERVAL_MS);
const onVisible = () => {
if (document.visibilityState === 'visible') checkForUpdate();
};
document.addEventListener('visibilitychange', onVisible);
window.addEventListener('focus', checkForUpdate);
},
});
if (!needRefresh) return null;
return (
<div className="fixed top-0 inset-x-0 z-[60] bg-[#1B263B] text-white shadow-lg">
<div className="max-w-7xl mx-auto px-4 py-2 flex items-center justify-between gap-3">
<div className="flex items-center gap-2 text-sm">
<RefreshCw size={16} className="shrink-0" />
<span>Доступна новая версия приложения</span>
</div>
<Button
size="sm"
onClick={() => updateServiceWorker(true)}
className="bg-white text-[#1B263B] hover:bg-white/90"
>
Обновить
</Button>
</div>
</div>
);
}

View File

@@ -3,18 +3,6 @@ import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then((registration) => {
console.log('SW registered:', registration)
})
.catch((error) => {
console.log('SW registration failed:', error)
})
})
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />

View File

@@ -5,7 +5,7 @@
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"types": ["vite/client", "vite-plugin-pwa/client", "vite-plugin-pwa/react"],
"skipLibCheck": true,
/* Bundler mode */

View File

@@ -9,10 +9,11 @@ export default defineConfig({
react(),
tailwindcss(),
VitePWA({
registerType: 'autoUpdate',
registerType: 'prompt',
manifest: false, // manifest.json from public
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: /^https:\/\/.*\/api\//,