fix: multiple improvements and CI/CD updates

Frontend fixes:
- Add missing translations for chunk upload (upload.uploadingChunks, common.chunk)
- Fix photo deletion visual bug by tracking deletion state per photo
- Prevent UI confusion when deleting photos in admin grid

Backend fixes:
- Add file existence checks before deleting thumbnails
- Prevent ENOENT errors for missing thumbnail files
- Improve error handling in photo deletion

CI/CD updates:
- Remove Gitea release creation from Drone pipeline
- Simplify GitHub mirror workflow (remove history rewriting, keep file removal)
- Add clean-git-history.sh script for manual history cleanup

These changes improve the admin photo management experience and streamline
the CI/CD process for better maintainability.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-24 16:39:20 +02:00
parent fee369a503
commit bf705674d5
8 changed files with 147 additions and 199 deletions
-46
View File
@@ -108,52 +108,6 @@ steps:
- VERSION=${DRONE_TAG}
- VITE_API_URL=${VITE_API_URL:-/api}
# -------- NEW: Create Gitea Release --------
- name: gitea-release
image: plugins/gitea-release
when:
event: tag
settings:
api_key:
from_secret: GITEA_TOKEN
base_url: https://gitea.local.nothaft.cloud
files: []
title: PicPeak ${DRONE_TAG}
note: |
# PicPeak ${DRONE_TAG}
## 🐳 Docker Images
This release includes Docker images published to local registry:
```bash
# Backend
docker pull registry.local.nothaft.cloud/picpeak-backend:${DRONE_TAG}
docker pull registry.local.nothaft.cloud/picpeak-backend:latest
# Frontend
docker pull registry.local.nothaft.cloud/picpeak-frontend:${DRONE_TAG}
docker pull registry.local.nothaft.cloud/picpeak-frontend:latest
```
## 📦 What's New
See the [README](https://gitea.nothaft.cloud/paul/picpeak#readme) for features and documentation.
## 🚀 Quick Start
```bash
# Clone and deploy
git clone https://gitea.nothaft.cloud/paul/picpeak.git
cd picpeak
# Use the tagged version
docker-compose -f docker-compose.prod.yml up -d
```
---
For detailed deployment instructions, see the Deployment Guide in the repository.
trigger:
event:
+9 -130
View File
@@ -10,120 +10,18 @@ jobs:
mirror:
runs-on: ubuntu-latest
steps:
- name: Checkout repository with full history
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0 # Full history needed for finding the commit
fetch-depth: 0 # Full history for proper mirroring
- name: Setup Git
run: |
git config --global user.name "the-luap"
git config --global user.email "paul-nothaft@hotmail.de"
- name: Debug - Show current branch and status
run: |
echo "Current branch:"
git branch -a
echo "Git status:"
git status
echo "Remote info:"
git remote -v
echo "Checking target commit exists:"
git show --oneline 7aca927937 || echo "Target commit not found!"
- name: Create completely new history from specific commit
run: |
TARGET_COMMIT="7aca927937"
# Verify the target commit exists
if ! git cat-file -e $TARGET_COMMIT^{commit}; then
echo "ERROR: Target commit $TARGET_COMMIT does not exist!"
exit 1
fi
echo "✅ Target commit found: $(git log --oneline -1 $TARGET_COMMIT)"
# Clean up any existing github-mirror branch
git branch -D github-mirror || true
# Create a completely new orphan branch (no history)
git checkout --orphan github-mirror
# Clear the staging area completely
git rm -rf . || true
# Get the file tree from the target commit and create initial commit
echo "Creating new history starting from $TARGET_COMMIT..."
git read-tree $TARGET_COMMIT
git commit -m "Initial commit - imported from $(git log --oneline -1 $TARGET_COMMIT)"
echo "✅ Created new initial commit: $(git log --oneline -1)"
# Now get all commits after the target commit and apply their changes
COMMITS_AFTER_TARGET=$(git rev-list --reverse --no-merges $TARGET_COMMIT..main)
if [ -n "$COMMITS_AFTER_TARGET" ]; then
echo "📋 Applying changes from commits after $TARGET_COMMIT (excluding Claude commits):"
for commit in $COMMITS_AFTER_TARGET; do
# Get the commit author name
COMMIT_AUTHOR_NAME=$(git log --format="%an" -n 1 $commit)
# Skip commits by Claude
if [ "$COMMIT_AUTHOR_NAME" = "Claude" ]; then
echo "⚠️ Skipping commit by Claude: $(git log --oneline -1 $commit)"
continue
fi
echo "Processing: $(git log --oneline -1 $commit)"
# Get the commit message and author info
COMMIT_MSG=$(git log --format="%B" -n 1 $commit)
COMMIT_AUTHOR=$(git log --format="%an <%ae>" -n 1 $commit)
COMMIT_DATE=$(git log --format="%ad" -n 1 $commit)
# Apply the changes from this commit
if git diff-tree --no-commit-id --name-only -r $commit | xargs -I {} git show $commit:{} > /dev/null 2>&1; then
# Apply file changes
git checkout $commit -- . || true
# Stage all changes
git add -A
# Only commit if there are changes
if ! git diff --cached --quiet; then
# Create new commit with original metadata but new SHA
GIT_AUTHOR_NAME=$(echo "$COMMIT_AUTHOR" | cut -d'<' -f1 | xargs)
GIT_AUTHOR_EMAIL=$(echo "$COMMIT_AUTHOR" | cut -d'<' -f2 | cut -d'>' -f1)
GIT_AUTHOR_DATE="$COMMIT_DATE"
export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
git commit -m "$COMMIT_MSG"
echo "✅ Applied changes as new commit: $(git log --oneline -1)"
else
echo "⚠️ No changes to commit for $commit"
fi
else
echo "⚠️ Skipping problematic commit $commit"
fi
done
echo "✅ Finished creating new history"
else
echo "✅ No commits after target commit - history starts fresh"
fi
echo ""
echo "=== New History Summary ==="
echo "Total commits in new history: $(git rev-list --count github-mirror)"
echo "History starts with: $(git log --oneline --reverse | head -1)"
echo "Latest commit: $(git log --oneline -1)"
- name: Remove sensitive files and directories
run: |
# Switch to the github-mirror branch
git checkout github-mirror
echo "Current files before cleanup:"
ls -la | head -10 || true
echo "..."
@@ -164,18 +62,6 @@ jobs:
echo "Final file structure (top level):"
ls -la | head -10 || true
- name: Verify completely new history
run: |
git checkout github-mirror
echo "=== Final History Verification ==="
echo "Total commits in new github-mirror branch: $(git rev-list --count github-mirror)"
echo ""
echo "Complete commit history (should start from target commit content):"
git log --oneline --reverse
echo ""
echo "⚠️ Note: This is a completely NEW history with new commit SHAs"
echo "🔍 Original target commit content preserved but with new commit ID"
- name: Check GitHub token
env:
GITHUBTOKEN: ${{ secrets.GITHUBTOKEN }}
@@ -187,13 +73,10 @@ jobs:
echo "GitHub token is available (length: ${#GITHUBTOKEN})"
fi
- name: Force push completely new history to GitHub
- name: Push to GitHub
env:
GITHUBTOKEN: ${{ secrets.GITHUBTOKEN }}
run: |
# Switch to github-mirror branch
git checkout github-mirror
# Remove existing github remote if it exists
git remote remove github || true
@@ -204,17 +87,13 @@ jobs:
echo "GitHub remote added:"
git remote -v
# Force push the completely new history to GitHub main
echo "🔥 FORCE PUSHING completely new history to GitHub..."
echo "⚠️ This will COMPLETELY REPLACE all history on GitHub!"
git push github github-mirror:main --force
echo "✅ Force push completed - GitHub now has completely new history!"
# Push to GitHub main branch
echo "Pushing to GitHub..."
git push github main --force
echo "✅ Push to GitHub completed!"
- name: Workflow completed
run: |
echo "✅ Mirror to GitHub workflow completed successfully!"
echo "🔥 COMPLETE HISTORY REPLACEMENT: GitHub now has entirely new history"
echo "📊 History starts from commit content: 7aca927937"
echo "🔍 Check https://github.com/the-luap/picpeak to verify the new history"
echo "📈 Total commits pushed: $(git rev-list --count github-mirror)"
echo "🆕 All commit SHAs are NEW - no connection to previous history"
echo "📊 Repository mirrored to: https://github.com/the-luap/picpeak"
echo "🔒 Sensitive files have been removed from the mirror"
+12 -2
View File
@@ -447,9 +447,14 @@ router.delete('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
if (photo.thumbnail_path) {
const thumbPath = path.join(storagePath, 'events/active', photo.thumbnail_path);
try {
// Check if file exists before attempting to delete
await fs.access(thumbPath);
await fs.unlink(thumbPath);
} catch (error) {
console.error('Error deleting thumbnail:', error);
// Only log if it's not a "file not found" error
if (error.code !== 'ENOENT') {
console.error('Error deleting thumbnail:', error);
}
}
}
@@ -534,9 +539,14 @@ router.post('/:eventId/photos/bulk-delete', adminAuth, async (req, res) => {
if (photo.thumbnail_path) {
const thumbPath = path.join(storagePath, photo.thumbnail_path);
try {
// Check if file exists before attempting to delete
await fs.access(thumbPath);
await fs.unlink(thumbPath);
} catch (error) {
console.error('Error deleting thumbnail:', error);
// Only log if it's not a "file not found" error
if (error.code !== 'ENOENT') {
console.error('Error deleting thumbnail:', error);
}
}
}
}
+91
View File
@@ -0,0 +1,91 @@
#!/bin/bash
# Script to clean git history - removes all commits before July 17, 2025
# WARNING: This is destructive and will rewrite history!
set -e
echo "⚠️ WARNING: This script will permanently rewrite git history!"
echo "⚠️ All commits before July 17, 2025 will be removed."
echo "⚠️ This action cannot be undone!"
echo ""
read -p "Are you sure you want to continue? (type 'yes' to proceed): " confirmation
if [ "$confirmation" != "yes" ]; then
echo "Operation cancelled."
exit 1
fi
# Create backup branch
echo "Creating backup branch..."
git checkout -b backup-before-cleanup-$(date +%Y%m%d-%H%M%S)
git checkout main
# Find the first commit on or after July 17, 2025
echo "Finding first commit after July 17, 2025..."
FIRST_COMMIT=$(git log --since="2025-07-17" --reverse --format="%H" | head -1)
if [ -z "$FIRST_COMMIT" ]; then
echo "ERROR: No commits found after July 17, 2025"
exit 1
fi
echo "First commit to keep: $FIRST_COMMIT"
echo "Commit details: $(git log --oneline -1 $FIRST_COMMIT)"
# Get all commits we want to keep
COMMITS_TO_KEEP=$(git log --since="2025-07-17" --reverse --format="%H")
COMMIT_COUNT=$(echo "$COMMITS_TO_KEEP" | wc -l)
echo "Total commits to preserve: $COMMIT_COUNT"
# Create new orphan branch
echo "Creating new clean history..."
git checkout --orphan new-main
# Clean the working directory
git rm -rf . || true
# Get the tree from the first commit
git checkout $FIRST_COMMIT -- .
# Create new initial commit with same content but new message
ORIGINAL_MESSAGE=$(git log --format="%B" -1 $FIRST_COMMIT)
ORIGINAL_AUTHOR=$(git log --format="%an <%ae>" -1 $FIRST_COMMIT)
ORIGINAL_DATE=$(git log --format="%ad" -1 $FIRST_COMMIT)
GIT_AUTHOR_NAME=$(echo "$ORIGINAL_AUTHOR" | cut -d'<' -f1 | xargs)
GIT_AUTHOR_EMAIL=$(echo "$ORIGINAL_AUTHOR" | cut -d'<' -f2 | cut -d'>' -f1)
GIT_AUTHOR_DATE="$ORIGINAL_DATE"
export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE
git add -A
git commit -m "Initial commit - Project start (July 17, 2025)
Original: $ORIGINAL_MESSAGE"
# Cherry-pick remaining commits
echo "Applying remaining commits..."
REMAINING_COMMITS=$(git log --since="2025-07-17" --reverse --format="%H" $FIRST_COMMIT..main)
if [ -n "$REMAINING_COMMITS" ]; then
for commit in $REMAINING_COMMITS; do
echo "Applying: $(git log --oneline -1 $commit)"
git cherry-pick $commit || {
echo "ERROR: Failed to cherry-pick $commit"
echo "You may need to resolve conflicts and continue manually"
exit 1
}
done
fi
echo ""
echo "✅ New history created successfully!"
echo "Total commits in new history: $(git rev-list --count HEAD)"
echo ""
echo "To finalize the cleanup, run these commands:"
echo " git branch -D main"
echo " git branch -m main"
echo " git push origin main --force"
echo ""
echo "⚠️ WARNING: Force pushing will overwrite the remote repository!"
echo "⚠️ Make sure you have a backup and all team members are aware!"
@@ -23,7 +23,7 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
const [selectedPhotos, setSelectedPhotos] = useState<Set<number>>(new Set());
const [isSelectionMode, setIsSelectionMode] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [deletingPhotoId, setDeletingPhotoId] = useState<number | null>(null);
const [deletingPhotos, setDeletingPhotos] = useState<Set<number>>(new Set());
const handlePhotoSelect = (photoId: number, e?: React.MouseEvent) => {
if (e) {
@@ -54,15 +54,18 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
return;
}
setDeletingPhotoId(photo.id);
setDeletingPhotos(prev => new Set(prev).add(photo.id));
try {
await photosService.deletePhoto(eventId, photo.id);
toast.success('Photo deleted successfully');
onPhotosDeleted();
} catch (error) {
toast.error('Failed to delete photo');
} finally {
setDeletingPhotoId(null);
setDeletingPhotos(prev => {
const newSet = new Set(prev);
newSet.delete(photo.id);
return newSet;
});
}
};
@@ -75,14 +78,18 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
}
setIsDeleting(true);
const selectedIds = Array.from(selectedPhotos);
setDeletingPhotos(new Set(selectedIds));
try {
await photosService.deletePhotos(eventId, Array.from(selectedPhotos));
await photosService.deletePhotos(eventId, selectedIds);
toast.success(`${count} photo${count > 1 ? 's' : ''} deleted successfully`);
setSelectedPhotos(new Set());
setIsSelectionMode(false);
onPhotosDeleted();
} catch (error) {
toast.error('Failed to delete photos');
setDeletingPhotos(new Set());
} finally {
setIsDeleting(false);
}
@@ -155,13 +162,15 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
{/* Photo Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{photos.map((photo, index) => (
<div
key={photo.id}
className={`relative group cursor-pointer rounded-lg overflow-hidden bg-neutral-100 ${
isSelectionMode ? 'ring-2 ring-offset-2 ' + (selectedPhotos.has(photo.id) ? 'ring-primary-500' : 'ring-transparent') : ''
}`}
onClick={() => isSelectionMode ? handlePhotoSelect(photo.id) : onPhotoClick(photo, index)}
{photos.map((photo, index) => {
const isDeleting = deletingPhotos.has(photo.id);
return (
<div
key={photo.id}
className={`relative group cursor-pointer rounded-lg overflow-hidden bg-neutral-100 transition-opacity ${
isSelectionMode ? 'ring-2 ring-offset-2 ' + (selectedPhotos.has(photo.id) ? 'ring-primary-500' : 'ring-transparent') : ''
} ${isDeleting ? 'opacity-50' : ''}`}
onClick={() => !isDeleting && (isSelectionMode ? handlePhotoSelect(photo.id) : onPhotoClick(photo, index))}
>
{/* Selection Checkbox */}
{isSelectionMode && (
@@ -219,8 +228,8 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
</button>
<button
onClick={(e) => handleDeleteSingle(photo, e)}
className="p-1 text-white hover:bg-white/20 rounded"
disabled={deletingPhotoId === photo.id}
className="p-1 text-white hover:bg-white/20 rounded disabled:opacity-50"
disabled={isDeleting}
>
<Trash2 className="w-3 h-3" />
</button>
@@ -238,7 +247,8 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
</div>
)}
</div>
))}
);
})}
</div>
{photos.length === 0 && (
@@ -256,7 +256,7 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
<div className="flex justify-between text-sm text-neutral-600 mb-1">
<span>
{t('upload.uploading')}
{totalChunks > 1 && ` (${t('common.chunk') || 'Chunk'} ${currentChunk}/${totalChunks})`}
{totalChunks > 1 && ` (${t('common.chunk')} ${currentChunk}/${totalChunks})`}
</span>
<span>{uploadProgress}%</span>
</div>
@@ -268,7 +268,7 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
</div>
{totalChunks > 1 && (
<p className="text-xs text-neutral-500 mt-1">
{t('upload.uploadingChunks') || `Uploading ${selectedFiles.length} files in ${totalChunks} batches...`}
{t('upload.uploadingChunks', { count: selectedFiles.length, total: totalChunks })}
</p>
)}
</div>
+4 -2
View File
@@ -35,7 +35,8 @@
"days": "Tage",
"customize": "Anpassen",
"hide": "Ausblenden",
"unknown": "Unbekannt"
"unknown": "Unbekannt",
"chunk": "Teil"
},
"upload": {
"photoCategory": "Fotokategorie",
@@ -51,7 +52,8 @@
"uploadPhotos": "Fotos hochladen",
"maxFilesReached": "Maximal 500 Dateien erlaubt",
"someFilesSkipped": "Einige Dateien wurden übersprungen (500 Dateien Limit)",
"tooManyFiles": "Maximal 500 Dateien können gleichzeitig hochgeladen werden"
"tooManyFiles": "Maximal 500 Dateien können gleichzeitig hochgeladen werden",
"uploadingChunks": "Lade {{count}} Dateien in {{total}} Teilen hoch..."
},
"navigation": {
"dashboard": "Dashboard",
+4 -2
View File
@@ -35,7 +35,8 @@
"days": "days",
"customize": "Customize",
"hide": "Hide",
"unknown": "Unknown"
"unknown": "Unknown",
"chunk": "Chunk"
},
"upload": {
"photoCategory": "Photo Category",
@@ -51,7 +52,8 @@
"uploadPhotos": "Upload Photos",
"maxFilesReached": "Maximum 500 files allowed",
"someFilesSkipped": "Some files were skipped (500 file limit)",
"tooManyFiles": "Maximum 500 files can be uploaded at once"
"tooManyFiles": "Maximum 500 files can be uploaded at once",
"uploadingChunks": "Uploading {{count}} files in {{total}} batches..."
},
"navigation": {
"dashboard": "Dashboard",