5 Commits

Author SHA1 Message Date
Paul R Kartchner
8dbc24f335 chore: bump version to 2026.01.004
All checks were successful
Basil CI/CD Pipeline / Shared Package Tests (push) Successful in 1m8s
Basil CI/CD Pipeline / Code Linting (push) Successful in 1m13s
Basil CI/CD Pipeline / Web Tests (push) Successful in 1m27s
Basil CI/CD Pipeline / API Tests (push) Successful in 1m36s
Basil CI/CD Pipeline / Security Scanning (push) Successful in 1m9s
Basil CI/CD Pipeline / Build All Packages (push) Successful in 1m32s
Basil CI/CD Pipeline / Build & Push Docker Images (push) Has been skipped
Basil CI/CD Pipeline / Trigger Deployment (push) Has been skipped
Basil CI/CD Pipeline / E2E Tests (push) Has been skipped
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 00:25:10 -07:00
Paul R Kartchner
2953bb9f04 fix: ensure tag input maintains focus after adding tags [dev]
Some checks failed
Basil CI/CD Pipeline / Code Linting (push) Successful in 1m21s
Basil CI/CD Pipeline / Shared Package Tests (push) Successful in 1m10s
Basil CI/CD Pipeline / Web Tests (push) Successful in 1m40s
Basil CI/CD Pipeline / API Tests (push) Successful in 1m52s
Basil CI/CD Pipeline / Build All Packages (push) Has been cancelled
Basil CI/CD Pipeline / E2E Tests (push) Has been cancelled
Basil CI/CD Pipeline / Build & Push Docker Images (push) Has been cancelled
Basil CI/CD Pipeline / Trigger Deployment (push) Has been cancelled
Basil CI/CD Pipeline / Security Scanning (push) Has been cancelled
- Add focus restoration after recipe state update
- Add focus in finally block to ensure it happens even on error
- Keeps cursor in tag input field for rapid tag entry

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 00:22:50 -07:00
Paul R Kartchner
beff2d1b4b feat: add local Docker dev environment setup [dev]
Some checks failed
Basil CI/CD Pipeline / Shared Package Tests (push) Successful in 1m33s
Basil CI/CD Pipeline / Code Linting (push) Successful in 1m35s
Basil CI/CD Pipeline / Security Scanning (push) Successful in 1m39s
Basil CI/CD Pipeline / Web Tests (push) Successful in 1m46s
Basil CI/CD Pipeline / API Tests (push) Successful in 1m59s
Basil CI/CD Pipeline / Build All Packages (push) Successful in 1m31s
Basil CI/CD Pipeline / E2E Tests (push) Has been skipped
Basil CI/CD Pipeline / Trigger Deployment (push) Has been cancelled
Basil CI/CD Pipeline / Build & Push Docker Images (push) Has been cancelled
- Add .env.dev with localhost configuration
- Docker Compose builds dev-tagged images
- Access dev environment at http://localhost:8088
- CI/CD skips deployment for commits with [dev] tag

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 00:15:42 -07:00
Paul R Kartchner
1ec5e5f189 ci: skip deployment for commits with [dev] or [skip-deploy]
Allows building Docker images without triggering production deployment by
adding [dev] or [skip-deploy] to the commit message.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 00:09:06 -07:00
Paul R Kartchner
d87210f8d3 fix: prevent page jump when adding/removing tags with optimistic updates
- Update UI immediately when adding/removing tags without full page reload
- Fetch updated recipe data in background to get proper tag IDs
- Revert optimistic update on error and reload
- Maintains scroll position and focus in tag input field

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-17 00:06:00 -07:00
7 changed files with 101 additions and 12 deletions

23
.env.dev Normal file
View 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

View File

@@ -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: |

View File

@@ -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';

View File

@@ -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);
}

View File

@@ -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';

View 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
View 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