Compare commits
5 Commits
v2026.01.0
...
v2026.01.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dbc24f335 | ||
|
|
2953bb9f04 | ||
|
|
beff2d1b4b | ||
|
|
1ec5e5f189 | ||
|
|
d87210f8d3 |
23
.env.dev
Normal file
23
.env.dev
Normal file
@@ -0,0 +1,23 @@
|
||||
# Development Environment Variables
|
||||
IMAGE_TAG=dev
|
||||
DOCKER_REGISTRY=localhost
|
||||
DOCKER_USERNAME=basil
|
||||
|
||||
# Database - uses local postgres from docker-compose
|
||||
DATABASE_URL=postgresql://basil:basil@postgres:5432/basil?schema=public
|
||||
|
||||
# CORS for local development
|
||||
CORS_ORIGIN=http://localhost
|
||||
|
||||
# JWT Secrets (dev only - not secure)
|
||||
JWT_SECRET=dev-secret-change-this-in-production-min-32-chars
|
||||
JWT_REFRESH_SECRET=dev-refresh-secret-change-this-in-prod-min-32
|
||||
|
||||
# Google OAuth (optional for dev)
|
||||
GOOGLE_CLIENT_ID=
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
GOOGLE_CALLBACK_URL=http://localhost/api/auth/google/callback
|
||||
|
||||
# Application URLs
|
||||
APP_URL=http://localhost
|
||||
API_URL=http://localhost/api
|
||||
@@ -398,7 +398,8 @@ jobs:
|
||||
name: Trigger Deployment
|
||||
runs-on: ubuntu-latest
|
||||
needs: docker-build-and-push
|
||||
if: success()
|
||||
# Skip deployment if commit message contains [skip-deploy] or [dev]
|
||||
if: success() && !contains(github.event.head_commit.message, '[skip-deploy]') && !contains(github.event.head_commit.message, '[dev]')
|
||||
steps:
|
||||
- name: Trigger webhook
|
||||
run: |
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
* Example: 2026.01.002 (January 2026, patch 2), 2026.02.003 (February 2026, patch 3)
|
||||
* Month and patch are zero-padded. Patch increments with each deployment in a month.
|
||||
*/
|
||||
export const APP_VERSION = '2026.01.003';
|
||||
export const APP_VERSION = '2026.01.004';
|
||||
|
||||
@@ -141,36 +141,62 @@ function RecipeDetail() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Optimistically update the UI immediately
|
||||
const optimisticTag = { tag: { id: 'temp', name: trimmedTag } };
|
||||
setRecipe({
|
||||
...recipe,
|
||||
tags: [...(recipe.tags || []), optimisticTag]
|
||||
});
|
||||
setTagInput('');
|
||||
// Keep focus in input field
|
||||
setTimeout(() => tagInputRef.current?.focus(), 0);
|
||||
|
||||
try {
|
||||
setSavingTags(true);
|
||||
// Send array of tag names (strings) to API
|
||||
const updatedTags = [...existingTagNames, trimmedTag];
|
||||
await recipesApi.update(id, { tags: updatedTags });
|
||||
|
||||
// Reload the recipe to get the updated tag structure from API
|
||||
await loadRecipe(id);
|
||||
setTagInput('');
|
||||
|
||||
// Reload available tags to include newly created ones
|
||||
loadTags();
|
||||
|
||||
// Keep focus in input field
|
||||
setTimeout(() => tagInputRef.current?.focus(), 0);
|
||||
// Fetch the updated recipe to get the proper tag IDs, but don't reload the whole page
|
||||
const response = await recipesApi.getById(id);
|
||||
if (response.data) {
|
||||
setRecipe(response.data);
|
||||
// Restore focus after state update
|
||||
setTimeout(() => tagInputRef.current?.focus(), 0);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to add tag:', err);
|
||||
alert('Failed to add tag');
|
||||
// Revert optimistic update on error
|
||||
await loadRecipe(id);
|
||||
} finally {
|
||||
setSavingTags(false);
|
||||
// Ensure focus is maintained
|
||||
setTimeout(() => tagInputRef.current?.focus(), 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTag = async (tagToRemove: string) => {
|
||||
if (!id || !recipe) return;
|
||||
|
||||
// Optimistically update the UI immediately
|
||||
const previousTags = recipe.tags;
|
||||
const updatedTagsOptimistic = (recipe.tags || []).filter(tagItem => {
|
||||
const tagName = typeof tagItem === 'string' ? tagItem : tagItem.tag?.name || tagItem.name;
|
||||
return tagName !== tagToRemove;
|
||||
});
|
||||
setRecipe({
|
||||
...recipe,
|
||||
tags: updatedTagsOptimistic
|
||||
});
|
||||
|
||||
try {
|
||||
setSavingTags(true);
|
||||
// Convert existing tags to string array and filter out the removed tag
|
||||
const existingTagNames = (recipe.tags || [])
|
||||
const existingTagNames = (previousTags || [])
|
||||
.map(tagItem =>
|
||||
typeof tagItem === 'string' ? tagItem : tagItem.tag?.name || tagItem.name
|
||||
)
|
||||
@@ -178,11 +204,16 @@ function RecipeDetail() {
|
||||
const updatedTags = existingTagNames.filter(tag => tag !== tagToRemove);
|
||||
await recipesApi.update(id, { tags: updatedTags });
|
||||
|
||||
// Reload the recipe to get the updated tag structure from API
|
||||
await loadRecipe(id);
|
||||
// Fetch the updated recipe to get the proper tag structure
|
||||
const response = await recipesApi.getById(id);
|
||||
if (response.data) {
|
||||
setRecipe(response.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to remove tag:', err);
|
||||
alert('Failed to remove tag');
|
||||
// Revert optimistic update on error
|
||||
await loadRecipe(id);
|
||||
} finally {
|
||||
setSavingTags(false);
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
* Example: 2026.01.002 (January 2026, patch 2), 2026.02.003 (February 2026, patch 3)
|
||||
* Month and patch are zero-padded. Patch increments with each deployment in a month.
|
||||
*/
|
||||
export const APP_VERSION = '2026.01.003';
|
||||
export const APP_VERSION = '2026.01.004';
|
||||
|
||||
14
traefik-local/dynamic-dev.yml
Normal file
14
traefik-local/dynamic-dev.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
http:
|
||||
routers:
|
||||
basil-dev:
|
||||
rule: "Host(`localhost`) || Host(`127.0.0.1`)"
|
||||
entryPoints:
|
||||
- http
|
||||
service: basil-dev-service
|
||||
priority: 1000 # Higher priority than Docker labels (default is 0)
|
||||
|
||||
services:
|
||||
basil-dev-service:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://basil-web:80"
|
||||
20
traefik-local/traefik.yml
Normal file
20
traefik-local/traefik.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
# Static Traefik configuration for local development
|
||||
entryPoints:
|
||||
http:
|
||||
address: ":80"
|
||||
|
||||
providers:
|
||||
docker:
|
||||
endpoint: "unix:///var/run/docker.sock"
|
||||
exposedByDefault: false
|
||||
network: traefik
|
||||
file:
|
||||
filename: /dynamic-dev.yml
|
||||
watch: true
|
||||
|
||||
api:
|
||||
insecure: true
|
||||
dashboard: true
|
||||
|
||||
log:
|
||||
level: INFO
|
||||
Reference in New Issue
Block a user