Compare commits
2 Commits
bf62714d0d
...
4110083019
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4110083019 | ||
|
|
60dea22ced |
27
.githooks/commit-msg
Executable file
27
.githooks/commit-msg
Executable 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
17
.githooks/post-commit
Executable 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
37
.githooks/pre-push
Executable 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
46
.githooks/prepare-commit-msg
Executable 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
|
||||||
13
Makefile
13
Makefile
@@ -3,7 +3,7 @@ REGISTRY = gitea.chedius.ru/chedius
|
|||||||
PLATFORM = linux/amd64
|
PLATFORM = linux/amd64
|
||||||
|
|
||||||
# Build and push both services
|
# Build and push both services
|
||||||
.PHONY: all build push deploy
|
.PHONY: all build push deploy install-hooks
|
||||||
|
|
||||||
all: build push
|
all: build push
|
||||||
|
|
||||||
@@ -30,11 +30,16 @@ update-server:
|
|||||||
docker pull $(REGISTRY)/delivery-tracker/frontend:latest
|
docker pull $(REGISTRY)/delivery-tracker/frontend:latest
|
||||||
docker-compose up -d --force-recreate backend frontend
|
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:
|
release:
|
||||||
@if [ -z "$(MSG)" ]; then echo "Usage: make release MSG='commit message'"; exit 1; fi
|
@if [ -z "$(MSG)" ]; then echo "Usage: make release MSG='commit message'"; exit 1; fi
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "$(MSG)" || true
|
git commit -m "$(MSG)" || true
|
||||||
git push
|
git push
|
||||||
$(MAKE) build push
|
@echo "✅ Released! Watchtower will deploy within 60 seconds."
|
||||||
@echo "Released! Watchtower will deploy within 60 seconds."
|
|
||||||
|
|||||||
@@ -10,12 +10,45 @@ server {
|
|||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
||||||
|
|
||||||
# Cache static assets
|
# Hashed build assets — safe to cache forever
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
location /assets/ {
|
||||||
expires 1y;
|
expires 1y;
|
||||||
add_header Cache-Control "public, immutable";
|
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
|
# Proxy API requests to backend
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://backend:8080;
|
proxy_pass http://backend:8080;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DeliveryForm } from './components/delivery/DeliveryForm';
|
|||||||
import { LoginForm } from './components/auth/LoginForm';
|
import { LoginForm } from './components/auth/LoginForm';
|
||||||
import { ToastContainer } from './components/ui/Toast';
|
import { ToastContainer } from './components/ui/Toast';
|
||||||
import { Button } from './components/ui/Button';
|
import { Button } from './components/ui/Button';
|
||||||
|
import { UpdatePrompt } from './components/ui/UpdatePrompt';
|
||||||
import { useDeliveryStore } from './stores/deliveryStore';
|
import { useDeliveryStore } from './stores/deliveryStore';
|
||||||
import { useAuthStore } from './stores/authStore';
|
import { useAuthStore } from './stores/authStore';
|
||||||
|
|
||||||
@@ -89,6 +90,7 @@ function App() {
|
|||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<UpdatePrompt />
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</>
|
</>
|
||||||
@@ -97,6 +99,7 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#fbf8fb]">
|
<div className="min-h-screen bg-[#fbf8fb]">
|
||||||
|
<UpdatePrompt />
|
||||||
<header className="sticky top-0 z-40 bg-[#1B263B] text-white shadow-md">
|
<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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex items-center justify-between h-14">
|
<div className="flex items-center justify-between h-14">
|
||||||
|
|||||||
55
frontend/src/components/ui/UpdatePrompt.tsx
Normal file
55
frontend/src/components/ui/UpdatePrompt.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,18 +3,6 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
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(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client", "vite-plugin-pwa/client", "vite-plugin-pwa/react"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ export default defineConfig({
|
|||||||
react(),
|
react(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: 'autoUpdate',
|
registerType: 'prompt',
|
||||||
manifest: false, // manifest.json from public
|
manifest: false, // manifest.json from public
|
||||||
workbox: {
|
workbox: {
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
|
globPatterns: ['**/*.{js,css,html,ico,png,svg,json}'],
|
||||||
|
cleanupOutdatedCaches: true,
|
||||||
runtimeCaching: [
|
runtimeCaching: [
|
||||||
{
|
{
|
||||||
urlPattern: /^https:\/\/.*\/api\//,
|
urlPattern: /^https:\/\/.*\/api\//,
|
||||||
|
|||||||
Reference in New Issue
Block a user