Compare commits

..

7 Commits

Author SHA1 Message Date
gitea-actions
fb0e6c1ea4 chore: bump version to v0.1.2
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m19s
2026-03-16 08:52:02 +00:00
Matthieu
6d3ecc1322 Merge branch 'feature/client-portal' into develop
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
2026-03-16 09:51:48 +01:00
Matthieu
f5986090c0 feat(deploy) : add deploy script and nginx config for bare Ubuntu server
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:51:29 +01:00
Matthieu
d6399c20e1 fix : fix MCP create-task tool crashing on task creation
CreateTaskTool called nonexistent findMaxNumberByProject instead of
findMaxNumberByProjectForUpdate. Also removed FOR UPDATE clause from the
query as PostgreSQL does not support it with aggregate functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:26:36 +01:00
Matthieu
a972d243f5 style : center and resize view toggle buttons on my-tasks page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:26:34 +01:00
Matthieu
56bf88f293 fix : prevent document delete button from submitting the TaskModal form
The delete button in TaskDocumentList lacked type="button", causing it to
act as a submit button inside the form, which triggered handleSubmit and
closed the modal before the confirmation dialog could appear. Also added
guards to prevent closing TaskModal while a sub-modal is open.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 09:26:32 +01:00
gitea-actions
4216f1b5a1 chore: bump version to v0.1.1
All checks were successful
Auto Tag Develop / tag (push) Successful in 4s
Build Release Artefact / build (push) Successful in 1m17s
2026-03-15 18:07:23 +00:00
9 changed files with 153 additions and 8 deletions

View File

@@ -1,2 +1,2 @@
parameters:
app.version: '0.1.0'
app.version: '0.1.2'

View File

@@ -0,0 +1,50 @@
server {
listen 80;
server_name project.malio-dev.fr;
root /var/www/lesstime/frontend/.output/public;
index index.html;
client_max_body_size 55m;
location ^~ /api/ {
root /var/www/lesstime/public;
try_files $uri /index.php?$query_string;
}
location ^~ /bundles/ {
root /var/www/lesstime/public;
try_files $uri =404;
}
location = /api/login_check {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/lesstime/public/index.php;
fastcgi_param DOCUMENT_ROOT /var/www/lesstime/public;
fastcgi_param SCRIPT_NAME /index.php;
fastcgi_param PATH_INFO /login_check;
fastcgi_param REQUEST_URI /login_check;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
location ^~ /_mcp {
root /var/www/lesstime/public;
try_files $uri /index.php?$query_string;
}
location ~ ^/index\.php(/|$) {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/lesstime/public/index.php;
fastcgi_param DOCUMENT_ROOT /var/www/lesstime/public;
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
internal;
}
location ~ \.php$ {
return 404;
}
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -34,6 +34,7 @@
<!-- Delete button -->
<button
v-if="isAdmin"
type="button"
class="absolute right-1 top-1 hidden rounded p-0.5 text-neutral-400 transition-colors hover:bg-red-50 hover:text-red-500 group-hover:block"
@click.stop="$emit('delete', doc)"
>

View File

@@ -289,6 +289,7 @@ const isOpen = computed({
})
function close() {
if (confirmDeleteDocOpen.value || confirmDeleteOpen.value) return
isOpen.value = false
}
@@ -390,6 +391,8 @@ function populateForm(task: Task | null) {
watch(() => props.modelValue, async (open) => {
if (open) {
confirmDeleteDocOpen.value = false
documentToDelete.value = null
populateForm(props.task)
try {
clientTickets.value = await clientTicketService.getAll({ project: props.projectId })

View File

@@ -2,7 +2,7 @@
<Teleport v-if="modelValue" to="body">
<Transition name="modal" appear>
<div class="fixed inset-0 z-[70] flex items-center justify-center">
<div class="absolute inset-0 bg-black/30" @click="cancel" />
<div class="absolute inset-0 bg-black/30" @click.stop="cancel" />
<div class="relative z-10 w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
<h3 class="text-lg font-bold text-neutral-900">{{ $t('taskDocuments.confirmDeleteTitle') }}</h3>
<p class="mt-3 text-sm text-neutral-600">

View File

@@ -236,20 +236,20 @@ onMounted(() => {
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('myTasks.title') }}</h1>
<div class="flex gap-1">
<button
class="rounded-lg p-2 transition-colors"
class="flex items-center justify-center rounded-md p-1.5 transition-colors"
:class="viewMode === 'kanban' ? 'bg-primary-500 text-white' : 'text-neutral-400 hover:text-primary-500'"
:title="$t('myTasks.viewKanban')"
@click="viewMode = 'kanban'"
>
<Icon name="mdi:view-column-outline" size="20" />
<Icon name="mdi:view-column-outline" size="18" />
</button>
<button
class="rounded-lg p-2 transition-colors"
class="flex items-center justify-center rounded-md p-1.5 transition-colors"
:class="viewMode === 'list' ? 'bg-primary-500 text-white' : 'text-neutral-400 hover:text-primary-500'"
:title="$t('myTasks.viewList')"
@click="viewMode = 'list'"
>
<Icon name="mdi:view-list-outline" size="20" />
<Icon name="mdi:view-list-outline" size="18" />
</button>
</div>
</div>

91
script/deploy-release.sh Executable file
View File

@@ -0,0 +1,91 @@
#!/usr/bin/env bash
set -euo pipefail
# Usage: ./script/deploy-release.sh v0.1.0
# Requires: curl, tar, (optional) rsync
#
# Auth token: set RELEASE_TOKEN env var or create /etc/lesstime-release-token
umask 002
TAG="${1:-}"
if [ -z "$TAG" ]; then
echo "Usage: $0 v0.1.0" >&2
exit 1
fi
REPO_OWNER="MALIO-DEV"
REPO_NAME="Lesstime"
GITEA_API="https://gitea.malio.fr/api/v1"
DEPLOY_DIR="/var/www/lesstime"
if [ -f /etc/lesstime-release-token ] && [ -z "${RELEASE_TOKEN:-}" ]; then
RELEASE_TOKEN="$(cat /etc/lesstime-release-token)"
fi
tmp_dir="$(mktemp -d)"
cleanup() {
rm -rf "$tmp_dir"
}
trap cleanup EXIT
release_json="$tmp_dir/release.json"
curl_opts=(-sS)
if [ -n "${RELEASE_TOKEN:-}" ]; then
curl_opts+=(-H "Authorization: token ${RELEASE_TOKEN}")
fi
curl "${curl_opts[@]}" \
"${GITEA_API}/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${TAG}" \
-o "$release_json"
asset_url="$(python3 - "$release_json" <<'PY'
import json, sys
data = json.load(open(sys.argv[1], 'r'))
assets = data.get("assets", [])
for a in assets:
name = a.get("name", "")
if name.startswith("lesstime-") and name.endswith(".tar.gz"):
print(a.get("browser_download_url", ""))
break
PY
)"
if [ -z "$asset_url" ]; then
echo "Release asset not found for tag ${TAG}" >&2
exit 1
fi
archive="$tmp_dir/artefact.tar.gz"
curl "${curl_opts[@]}" -L "$asset_url" -o "$archive"
tar -xzf "$archive" -C "$tmp_dir"
if command -v rsync >/dev/null 2>&1; then
rsync -a --delete --no-perms --no-owner --no-group \
--exclude ".env" \
--exclude ".env.local" \
--exclude "config/jwt" \
--exclude "var" \
"$tmp_dir"/ "$DEPLOY_DIR"/
else
cp -a "$tmp_dir"/. "$DEPLOY_DIR"/
fi
# Ensure Nginx can traverse the deploy path.
chmod o+rx "$(dirname "$DEPLOY_DIR")" "$DEPLOY_DIR" 2>/dev/null || true
# Create frontend/dist symlink if needed (nginx serves from frontend/dist)
if [ -d "${DEPLOY_DIR}/frontend/.output/public" ] && [ ! -L "${DEPLOY_DIR}/frontend/dist" ]; then
ln -sfn "${DEPLOY_DIR}/frontend/.output/public" "${DEPLOY_DIR}/frontend/dist"
fi
echo "Release ${TAG} deployed to ${DEPLOY_DIR}"
if [ -f "${DEPLOY_DIR}/.env.local" ]; then
echo "Clearing cache..."
php "${DEPLOY_DIR}/bin/console" cache:clear --env=prod --no-debug
echo "Running migrations (if any)..."
php "${DEPLOY_DIR}/bin/console" doctrine:migrations:migrate --no-interaction --env=prod
else
echo "Skip post-deploy: ${DEPLOY_DIR}/.env.local not found" >&2
fi

View File

@@ -54,7 +54,7 @@ class CreateTaskTool
$task = new Task();
$task->setProject($project);
$task->setTitle($title);
$task->setNumber($this->taskRepository->findMaxNumberByProject($project) + 1);
$task->setNumber($this->taskRepository->findMaxNumberByProjectForUpdate($project) + 1);
if (null !== $description) {
$task->setDescription($description);

View File

@@ -28,7 +28,7 @@ class TaskRepository extends ServiceEntityRepository
$conn = $this->getEntityManager()->getConnection();
$result = $conn->fetchOne(
'SELECT COALESCE(MAX(number), 0) FROM task WHERE project_id = :project FOR UPDATE',
'SELECT COALESCE(MAX(number), 0) FROM task WHERE project_id = :project',
['project' => $project->getId()],
);