feat: add CI/CD pipeline, backup system, and deployment automation
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Has been cancelled
CI/CD Pipeline / Code Quality (pull_request) Has been cancelled
CI Pipeline / Lint Code (pull_request) Has been cancelled
CI Pipeline / Test API Package (pull_request) Has been cancelled
CI Pipeline / Test Web Package (pull_request) Has been cancelled
CI Pipeline / Test Shared Package (pull_request) Has been cancelled
Docker Build & Deploy / Build Docker Images (pull_request) Has been cancelled
E2E Tests / End-to-End Tests (pull_request) Has been cancelled
E2E Tests / E2E Tests (Mobile) (pull_request) Has been cancelled
Security Scanning / NPM Audit (pull_request) Has been cancelled
Security Scanning / Dependency License Check (pull_request) Has been cancelled
Security Scanning / Code Quality Scan (pull_request) Has been cancelled
Security Scanning / Docker Image Security (pull_request) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (pull_request) Has been cancelled
CI Pipeline / Build All Packages (pull_request) Has been cancelled
CI Pipeline / Generate Coverage Report (pull_request) Has been cancelled
Docker Build & Deploy / Push Docker Images (pull_request) Has been cancelled
Docker Build & Deploy / Deploy to Staging (pull_request) Has been cancelled
Docker Build & Deploy / Deploy to Production (pull_request) Has been cancelled
Security Scanning / Security Summary (pull_request) Has been cancelled
Some checks failed
CI/CD Pipeline / Run Tests (pull_request) Has been cancelled
CI/CD Pipeline / Code Quality (pull_request) Has been cancelled
CI Pipeline / Lint Code (pull_request) Has been cancelled
CI Pipeline / Test API Package (pull_request) Has been cancelled
CI Pipeline / Test Web Package (pull_request) Has been cancelled
CI Pipeline / Test Shared Package (pull_request) Has been cancelled
Docker Build & Deploy / Build Docker Images (pull_request) Has been cancelled
E2E Tests / End-to-End Tests (pull_request) Has been cancelled
E2E Tests / E2E Tests (Mobile) (pull_request) Has been cancelled
Security Scanning / NPM Audit (pull_request) Has been cancelled
Security Scanning / Dependency License Check (pull_request) Has been cancelled
Security Scanning / Code Quality Scan (pull_request) Has been cancelled
Security Scanning / Docker Image Security (pull_request) Has been cancelled
CI/CD Pipeline / Build and Push Docker Images (pull_request) Has been cancelled
CI Pipeline / Build All Packages (pull_request) Has been cancelled
CI Pipeline / Generate Coverage Report (pull_request) Has been cancelled
Docker Build & Deploy / Push Docker Images (pull_request) Has been cancelled
Docker Build & Deploy / Deploy to Staging (pull_request) Has been cancelled
Docker Build & Deploy / Deploy to Production (pull_request) Has been cancelled
Security Scanning / Security Summary (pull_request) Has been cancelled
## Summary - Add complete CI/CD pipeline with Gitea Actions for automated testing, building, and deployment - Implement backup and restore system with full database and file backup to ZIP - Add deployment automation with webhook receiver and systemd service - Enhance recipe editing UI with improved ingredient parsing and cooking mode features - Add comprehensive documentation for CI/CD, deployment, and backup features ## CI/CD Pipeline - New workflow in .gitea/workflows/ci-cd.yml with test, build, and deploy stages - Automated Docker image building and pushing to registry - Webhook-triggered deployments to production servers ## Backup & Restore - New backup service with ZIP creation including database dump and uploads - REST API endpoints for create, list, download, restore, and delete operations - Configurable backup path via BACKUP_PATH environment variable ## Deployment - Automated deployment scripts (deploy.sh, manual-deploy.sh) - Webhook receiver with systemd service for deployment triggers - Environment configuration template (.env.deploy.example) ## Documentation - docs/CI-CD-SETUP.md - Complete CI/CD pipeline setup guide - docs/DEPLOYMENT-QUICK-START.md - Quick deployment reference - docs/BACKUP.md - Backup and restore documentation - docs/REMOTE_DATABASE.md - Remote database configuration guide - scripts/README.md - Deployment scripts documentation ## Web Improvements - Enhanced ingredient parser with better unit and quantity detection - Improved recipe editing interface with unified edit experience - Better cooking mode functionality - Updated dependencies in package.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { Recipe, Ingredient, Instruction, RecipeSection, Tag } from '@basil/shared';
|
||||
import { recipesApi, tagsApi } from '../services/api';
|
||||
import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd';
|
||||
import '../styles/UnifiedRecipeEdit.css';
|
||||
|
||||
interface MappingChange {
|
||||
@@ -92,7 +93,10 @@ function UnifiedEditRecipe() {
|
||||
setServings(loadedRecipe.servings?.toString() || '');
|
||||
setCuisine(loadedRecipe.cuisine || '');
|
||||
setRecipeCategories(loadedRecipe.categories || []);
|
||||
setRecipeTags(loadedRecipe.tags || []);
|
||||
|
||||
// Handle tags - API returns array of {tag: {id, name}} objects, we need string[]
|
||||
const tagNames = (loadedRecipe.tags as any)?.map((t: any) => t.tag?.name || t).filter(Boolean) || [];
|
||||
setRecipeTags(tagNames);
|
||||
|
||||
// Set sections or simple mode
|
||||
const hasSections = !!(loadedRecipe.sections && loadedRecipe.sections.length > 0);
|
||||
@@ -301,6 +305,19 @@ function UnifiedEditRecipe() {
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const reorderInstructions = (result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
|
||||
const items = Array.from(instructions);
|
||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||
items.splice(result.destination.index, 0, reorderedItem);
|
||||
|
||||
// Update step numbers
|
||||
const updatedItems = items.map((item, index) => ({ ...item, step: index + 1 }));
|
||||
setInstructions(updatedItems);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
// Drag and drop
|
||||
const handleIngredientDragStart = (ingredient: Ingredient) => {
|
||||
setDraggedIngredient(ingredient);
|
||||
@@ -1099,132 +1116,161 @@ function UnifiedEditRecipe() {
|
||||
<div className="instructions-panel">
|
||||
<h3>Instructions</h3>
|
||||
|
||||
<ul className="instructions-list">
|
||||
{allInstructions.map((instruction, index) => {
|
||||
const isEditing = editingInstructionId === instruction.id;
|
||||
const mappedIngredients = getMappedIngredientsForInstruction(
|
||||
instruction.id || ''
|
||||
);
|
||||
const isDragOver = dragOverInstructionId === instruction.id;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={instruction.id || index}
|
||||
className={`instruction-item ${isDragOver ? 'drag-over' : ''}`}
|
||||
onDragOver={(e) => handleInstructionDragOver(e, instruction.id || '')}
|
||||
onDragLeave={handleInstructionDragLeave}
|
||||
onDrop={(e) => handleInstructionDrop(e, instruction.id || '')}
|
||||
<DragDropContext onDragEnd={reorderInstructions}>
|
||||
<Droppable droppableId="instructions">
|
||||
{(provided) => (
|
||||
<ul
|
||||
className="instructions-list"
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
<div className="instruction-header">
|
||||
<span className="step-number">Step {instruction.step}</span>
|
||||
{allInstructions.map((instruction, index) => {
|
||||
const isEditing = editingInstructionId === instruction.id;
|
||||
const mappedIngredients = getMappedIngredientsForInstruction(
|
||||
instruction.id || ''
|
||||
);
|
||||
const isDragOver = dragOverInstructionId === instruction.id;
|
||||
|
||||
{!isEditing && (
|
||||
<div className="instruction-controls">
|
||||
<button
|
||||
className="btn-edit-instruction"
|
||||
onClick={() => startEditingInstruction(instruction)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="btn-delete-instruction"
|
||||
onClick={() => removeInstruction(index)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
className="instruction-timing-input"
|
||||
value={editingInstructionTiming}
|
||||
onChange={(e) => setEditingInstructionTiming(e.target.value)}
|
||||
placeholder="Timing (optional, e.g., 8:00am)"
|
||||
/>
|
||||
<textarea
|
||||
className="instruction-text-input"
|
||||
value={editingInstructionText}
|
||||
onChange={(e) => setEditingInstructionText(e.target.value)}
|
||||
placeholder="Instruction text"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="instruction-edit-actions">
|
||||
<button
|
||||
className="btn-save-instruction"
|
||||
onClick={saveEditingInstruction}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
className="btn-cancel-instruction"
|
||||
onClick={cancelEditingInstruction}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{instruction.timing && (
|
||||
<div className="instruction-timing-display">
|
||||
{instruction.timing}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="instruction-text-display"
|
||||
onClick={() => startEditingInstruction(instruction)}
|
||||
title="Click to edit"
|
||||
return (
|
||||
<Draggable
|
||||
key={instruction.id || `instruction-${index}`}
|
||||
draggableId={instruction.id || `instruction-${index}`}
|
||||
index={index}
|
||||
>
|
||||
{instruction.text || <em>Click to add instruction text</em>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Drop zone for ingredients */}
|
||||
<div className="drop-zone">
|
||||
<span className="drop-zone-header">
|
||||
Ingredients for this step:
|
||||
</span>
|
||||
|
||||
{mappedIngredients.length === 0 ? (
|
||||
<p className="no-ingredients-mapped">
|
||||
Drag ingredients here or use bulk actions
|
||||
</p>
|
||||
) : (
|
||||
<ul className="mapped-ingredients-list">
|
||||
{mappedIngredients.map((ingredient) => (
|
||||
{(provided, snapshot) => (
|
||||
<li
|
||||
key={ingredient.id}
|
||||
className="mapped-ingredient-item"
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
className={`instruction-item ${isDragOver ? 'drag-over' : ''} ${snapshot.isDragging ? 'dragging' : ''}`}
|
||||
onDragOver={(e) => handleInstructionDragOver(e, instruction.id || '')}
|
||||
onDragLeave={handleInstructionDragLeave}
|
||||
onDrop={(e) => handleInstructionDrop(e, instruction.id || '')}
|
||||
>
|
||||
<span className="mapped-ingredient-text">
|
||||
{getIngredientText(ingredient)}
|
||||
</span>
|
||||
<button
|
||||
className="btn-remove-ingredient"
|
||||
onClick={() =>
|
||||
removeIngredientFromInstruction(
|
||||
ingredient.id || '',
|
||||
instruction.id || ''
|
||||
)
|
||||
}
|
||||
title="Remove ingredient from this step"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<div className="instruction-header">
|
||||
<div className="instruction-header-left">
|
||||
<div
|
||||
{...provided.dragHandleProps}
|
||||
className="instruction-drag-handle"
|
||||
title="Drag to reorder"
|
||||
>
|
||||
⋮⋮
|
||||
</div>
|
||||
<span className="step-number">Step {instruction.step}</span>
|
||||
</div>
|
||||
|
||||
{!isEditing && (
|
||||
<div className="instruction-controls">
|
||||
<button
|
||||
className="btn-edit-instruction"
|
||||
onClick={() => startEditingInstruction(instruction)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="btn-delete-instruction"
|
||||
onClick={() => removeInstruction(index)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
className="instruction-timing-input"
|
||||
value={editingInstructionTiming}
|
||||
onChange={(e) => setEditingInstructionTiming(e.target.value)}
|
||||
placeholder="Timing (optional, e.g., 8:00am)"
|
||||
/>
|
||||
<textarea
|
||||
className="instruction-text-input"
|
||||
value={editingInstructionText}
|
||||
onChange={(e) => setEditingInstructionText(e.target.value)}
|
||||
placeholder="Instruction text"
|
||||
autoFocus
|
||||
/>
|
||||
<div className="instruction-edit-actions">
|
||||
<button
|
||||
className="btn-save-instruction"
|
||||
onClick={saveEditingInstruction}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
className="btn-cancel-instruction"
|
||||
onClick={cancelEditingInstruction}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{instruction.timing && (
|
||||
<div className="instruction-timing-display">
|
||||
{instruction.timing}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="instruction-text-display"
|
||||
onClick={() => startEditingInstruction(instruction)}
|
||||
title="Click to edit"
|
||||
>
|
||||
{instruction.text || <em>Click to add instruction text</em>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Drop zone for ingredients */}
|
||||
<div className="drop-zone">
|
||||
<span className="drop-zone-header">
|
||||
Ingredients for this step:
|
||||
</span>
|
||||
|
||||
{mappedIngredients.length === 0 ? (
|
||||
<p className="no-ingredients-mapped">
|
||||
Drag ingredients here or use bulk actions
|
||||
</p>
|
||||
) : (
|
||||
<ul className="mapped-ingredients-list">
|
||||
{mappedIngredients.map((ingredient) => (
|
||||
<li
|
||||
key={ingredient.id}
|
||||
className="mapped-ingredient-item"
|
||||
>
|
||||
<span className="mapped-ingredient-text">
|
||||
{getIngredientText(ingredient)}
|
||||
</span>
|
||||
<button
|
||||
className="btn-remove-ingredient"
|
||||
onClick={() =>
|
||||
removeIngredientFromInstruction(
|
||||
ingredient.id || '',
|
||||
instruction.id || ''
|
||||
)
|
||||
}
|
||||
title="Remove ingredient from this step"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</ul>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
|
||||
<button className="btn-add-instruction" onClick={addInstruction}>
|
||||
+ Add Instruction
|
||||
|
||||
Reference in New Issue
Block a user