Compare commits
10 Commits
202b516dc3
...
v0.1.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb0e6c1ea4 | ||
|
|
6d3ecc1322 | ||
|
|
f5986090c0 | ||
|
|
d6399c20e1 | ||
|
|
a972d243f5 | ||
|
|
56bf88f293 | ||
| 9d80e017c2 | |||
| 4e91507158 | |||
| 318f14ea88 | |||
|
|
4216f1b5a1 |
@@ -16,7 +16,7 @@ src/Entity/ # Entités Doctrine (User, Client, Project, Task, TaskStatu
|
|||||||
src/ApiResource/ # Ressources API Platform (si découplées des entités)
|
src/ApiResource/ # Ressources API Platform (si découplées des entités)
|
||||||
src/State/ # Providers et Processors API Platform (MeProvider, AppVersionProvider, ActiveTimeEntryProvider, UserPasswordHasherProcessor, TaskNumberProcessor, ClientTicket*Provider/Processor, NotificationProvider, Gitea*Provider, Gitea*Processor)
|
src/State/ # Providers et Processors API Platform (MeProvider, AppVersionProvider, ActiveTimeEntryProvider, UserPasswordHasherProcessor, TaskNumberProcessor, ClientTicket*Provider/Processor, NotificationProvider, Gitea*Provider, Gitea*Processor)
|
||||||
src/Service/ # Services métier (NotificationService)
|
src/Service/ # Services métier (NotificationService)
|
||||||
src/Controller/ # Controllers custom Symfony (NotificationUnreadCountController, MarkAllReadController)
|
src/Controller/ # Controllers custom Symfony (NotificationUnreadCountController, MarkAllReadController, UserAvatarController, TaskDocumentDownloadController)
|
||||||
src/Mcp/Tool/ # MCP tools organisés par domaine (Project/, Task/, TaskMeta/, TimeEntry/, Reference/)
|
src/Mcp/Tool/ # MCP tools organisés par domaine (Project/, Task/, TaskMeta/, TimeEntry/, Reference/)
|
||||||
src/Security/ # Authenticators custom (ApiTokenAuthenticator pour MCP HTTP)
|
src/Security/ # Authenticators custom (ApiTokenAuthenticator pour MCP HTTP)
|
||||||
src/Command/ # Commandes console (GenerateApiTokenCommand)
|
src/Command/ # Commandes console (GenerateApiTokenCommand)
|
||||||
@@ -28,10 +28,10 @@ migrations/ # Migrations Doctrine
|
|||||||
docs/plans/ # Plans d'implémentation
|
docs/plans/ # Plans d'implémentation
|
||||||
docs/superpowers/ # Plans et specs superpowers
|
docs/superpowers/ # Plans et specs superpowers
|
||||||
frontend/ # App Nuxt 4
|
frontend/ # App Nuxt 4
|
||||||
frontend/pages/ # Pages (index, login, my-tasks, projects, projects/[id], projects/[id]/groups, projects/[id]/archives, time-tracking, admin, portal/, portal/projects/[id], portal/projects/[id]/new-ticket)
|
frontend/pages/ # Pages (index, login, my-tasks, profile, projects, projects/[id], projects/[id]/groups, projects/[id]/archives, time-tracking, admin, portal/, portal/projects/[id], portal/projects/[id]/new-ticket)
|
||||||
frontend/layouts/ # Layouts (default, portal)
|
frontend/layouts/ # Layouts (default, portal)
|
||||||
frontend/components/ # Composants Vue organisés en sous-dossiers (ui/, client/, project/, task/, user/, admin/, time-tracking/, client-ticket/, notification/)
|
frontend/components/ # Composants Vue organisés en sous-dossiers (ui/, client/, project/, task/, user/, admin/, time-tracking/, client-ticket/, notification/)
|
||||||
frontend/composables/# Composables (useApi, useAppVersion, useNotifications, useClientTicketHelpers)
|
frontend/composables/# Composables (useApi, useAppVersion, useNotifications, useClientTicketHelpers, useAvatarService)
|
||||||
frontend/stores/ # Stores Pinia (auth, ui, timer)
|
frontend/stores/ # Stores Pinia (auth, ui, timer)
|
||||||
frontend/services/ # Services API (auth, clients, gitea, projects, tasks, task-statuses, task-efforts, task-groups, task-priorities, task-tags, users, time-entries, client-tickets, notifications, task-documents)
|
frontend/services/ # Services API (auth, clients, gitea, projects, tasks, task-statuses, task-efforts, task-groups, task-priorities, task-tags, users, time-entries, client-tickets, notifications, task-documents)
|
||||||
frontend/services/dto/ # Types TypeScript
|
frontend/services/dto/ # Types TypeScript
|
||||||
@@ -80,6 +80,8 @@ Exemples : `feat : add login page`, `fix(auth) : prevent null token crash`
|
|||||||
- PostgreSQL : `LIKE` sur colonne JSON ne marche pas → utiliser `roles::text LIKE` via native SQL
|
- PostgreSQL : `LIKE` sur colonne JSON ne marche pas → utiliser `roles::text LIKE` via native SQL
|
||||||
- Controllers custom sous `/api/` : ajouter `priority: 1` sur `#[Route]` pour éviter le conflit avec API Platform `{id}`
|
- Controllers custom sous `/api/` : ajouter `priority: 1` sur `#[Route]` pour éviter le conflit avec API Platform `{id}`
|
||||||
- Serialization : pour embarquer une relation (pas IRI), ajouter le groupe du parent aux propriétés de l'entité cible
|
- Serialization : pour embarquer une relation (pas IRI), ajouter le groupe du parent aux propriétés de l'entité cible
|
||||||
|
- Upload fichiers : utiliser `$file->getMimeType()` (pas `getClientMimeType()`) pour valider côté serveur — nécessite `symfony/mime`
|
||||||
|
- Auth endpoints mixtes (ROLE_USER + ROLE_CLIENT) : utiliser `#[IsGranted('IS_AUTHENTICATED_FULLY')]` au lieu d'un rôle spécifique
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
|
|||||||
190
README.md
190
README.md
@@ -1,10 +1,173 @@
|
|||||||
# Lesstime
|
# Lesstime
|
||||||
|
|
||||||
Application de gestion de projet. Symfony 8 + API Platform 4 + Nuxt 4.
|
Application de gestion de projet avec suivi du temps et portail client.
|
||||||
|
|
||||||
## MCP Server
|
## Stack
|
||||||
|
|
||||||
Lesstime expose un serveur MCP (Model Context Protocol) permettant aux assistants IA (Claude Code, ChatGPT, Codex) d'interagir avec les projets, tâches et le suivi du temps.
|
| Couche | Technologies |
|
||||||
|
|--------|-------------|
|
||||||
|
| **Backend** | PHP 8.4, Symfony 8, API Platform 4, Doctrine ORM |
|
||||||
|
| **Frontend** | Nuxt 4 (SPA), Vue 3, Pinia, Tailwind CSS |
|
||||||
|
| **Base de données** | PostgreSQL 16 |
|
||||||
|
| **Auth** | JWT HTTP-only cookie (lexik/jwt-authentication-bundle) |
|
||||||
|
| **Infrastructure** | Docker (PHP-FPM, Nginx, PostgreSQL) |
|
||||||
|
|
||||||
|
## Fonctionnalités
|
||||||
|
|
||||||
|
- Gestion de projets et tâches (kanban, groupes, priorités, tags, efforts)
|
||||||
|
- Suivi du temps (timer, calendrier, vue liste)
|
||||||
|
- Portail client avec tickets (bug, amélioration, autre)
|
||||||
|
- Gestion de documents (upload, prévisualisation, téléchargement)
|
||||||
|
- Profil utilisateur avec avatar (crop circulaire)
|
||||||
|
- Notifications temps réel
|
||||||
|
- Intégration Gitea (issues, repos)
|
||||||
|
- Serveur MCP pour assistants IA
|
||||||
|
- Multi-langue (i18n)
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- Git
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Cloner le repo
|
||||||
|
git clone <url> && cd lesstime
|
||||||
|
|
||||||
|
# 2. Démarrer les containers
|
||||||
|
make start
|
||||||
|
|
||||||
|
# 3. Installation complète (composer, migrations, fixtures, build Nuxt)
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
L'application est accessible sur **http://localhost:8082**.
|
||||||
|
|
||||||
|
### Comptes de test (fixtures)
|
||||||
|
|
||||||
|
| Utilisateur | Mot de passe | Rôle | Détails |
|
||||||
|
|-------------|-------------|------|---------|
|
||||||
|
| `admin` | `admin` | ROLE_ADMIN | Administrateur |
|
||||||
|
| `alice` | `alice` | ROLE_USER | Utilisateur interne |
|
||||||
|
| `bob` | `bob` | ROLE_USER | Utilisateur interne |
|
||||||
|
| `charlie` | `charlie` | ROLE_USER | Utilisateur interne |
|
||||||
|
| `client-liot` | `client` | ROLE_CLIENT | Client LIOT (projet SIRH) |
|
||||||
|
| `client-acme` | `client` | ROLE_CLIENT | Client ACME (projet CRM) |
|
||||||
|
|
||||||
|
## Commandes
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make start # Démarrer les containers
|
||||||
|
make stop # Arrêter les containers
|
||||||
|
make restart # Redémarrer les containers
|
||||||
|
make shell # Shell dans le container PHP
|
||||||
|
make shell-root # Shell root dans le container PHP
|
||||||
|
```
|
||||||
|
|
||||||
|
### Développement
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dev-nuxt # Dev server Nuxt (hot reload, port 3002)
|
||||||
|
make cache-clear # Vider le cache Symfony
|
||||||
|
make logs-dev # Tail logs Symfony
|
||||||
|
```
|
||||||
|
|
||||||
|
### Base de données
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make migration-migrate # Lancer les migrations
|
||||||
|
make fixtures # Charger les fixtures
|
||||||
|
make db-reset # Reset BDD + migrations + fixtures (⚠️ supprime les données)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests & Qualité
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test # PHPUnit
|
||||||
|
make php-cs-fixer-allow-risky # Fix code style PHP (Symfony + PSR-12)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installation complète
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install # Composer + migrations + fixtures + build Nuxt
|
||||||
|
make reset # Tout supprimer et réinstaller (⚠️ supprime la BDD)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── Entity/ # Entités Doctrine
|
||||||
|
├── ApiResource/ # Ressources API Platform (découplées)
|
||||||
|
├── State/ # Providers et Processors API Platform
|
||||||
|
├── Controller/ # Controllers custom Symfony
|
||||||
|
├── Service/ # Services métier
|
||||||
|
├── EventListener/ # Listeners Doctrine
|
||||||
|
├── Exception/ # Exceptions custom
|
||||||
|
├── Security/ # Authenticators custom
|
||||||
|
├── Repository/ # Repositories Doctrine
|
||||||
|
├── Command/ # Commandes console
|
||||||
|
├── DataFixtures/ # Fixtures
|
||||||
|
└── Mcp/Tool/ # MCP tools par domaine
|
||||||
|
├── Project/
|
||||||
|
├── Task/
|
||||||
|
├── TaskMeta/
|
||||||
|
├── TimeEntry/
|
||||||
|
└── Reference/
|
||||||
|
|
||||||
|
frontend/
|
||||||
|
├── pages/ # Pages Nuxt (routing auto)
|
||||||
|
│ ├── portal/ # Pages portail client
|
||||||
|
│ └── projects/ # Pages projets
|
||||||
|
├── layouts/ # Layouts (default, portal)
|
||||||
|
├── components/ # Composants Vue
|
||||||
|
│ ├── ui/ # Composants génériques
|
||||||
|
│ ├── task/ # Tâches
|
||||||
|
│ ├── user/ # Utilisateur (avatar, etc.)
|
||||||
|
│ ├── project/ # Projets
|
||||||
|
│ ├── client/ # Clients
|
||||||
|
│ ├── client-ticket/ # Tickets client
|
||||||
|
│ ├── admin/ # Administration
|
||||||
|
│ ├── notification/ # Notifications
|
||||||
|
│ └── time-tracking/ # Suivi du temps
|
||||||
|
├── composables/ # Composables (useApi, useNotifications, etc.)
|
||||||
|
├── stores/ # Stores Pinia (auth, ui, timer)
|
||||||
|
├── services/ # Services API
|
||||||
|
│ └── dto/ # Types TypeScript
|
||||||
|
├── plugins/ # Plugins Nuxt
|
||||||
|
├── utils/ # Utilitaires
|
||||||
|
├── i18n/locales/ # Traductions
|
||||||
|
└── middleware/ # Middleware auth
|
||||||
|
|
||||||
|
config/ # Config Symfony
|
||||||
|
migrations/ # Migrations Doctrine
|
||||||
|
docker/ # Dockerfiles et config Nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
| Container | Port | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `php-lesstime-fpm` | 3002 (dev Nuxt) | PHP-FPM + Node 24 |
|
||||||
|
| `nginx-lesstime` | 8082 | Nginx reverse proxy |
|
||||||
|
| PostgreSQL | 5435 | Base de données |
|
||||||
|
|
||||||
|
Configuration : `docker/.env.docker` (override local : `docker/.env.docker.local`)
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Toutes les routes API sont préfixées `/api` (API Platform).
|
||||||
|
|
||||||
|
- Documentation auto-générée : **http://localhost:8082/api**
|
||||||
|
- Auth : `POST /login_check` avec `{ username, password }` → cookie JWT `BEARER`
|
||||||
|
|
||||||
|
## Serveur MCP
|
||||||
|
|
||||||
|
Lesstime expose un serveur MCP (Model Context Protocol) permettant aux assistants IA d'interagir avec les données.
|
||||||
|
|
||||||
### Tools disponibles (22)
|
### Tools disponibles (22)
|
||||||
|
|
||||||
@@ -16,13 +179,6 @@ Lesstime expose un serveur MCP (Model Context Protocol) permettant aux assistant
|
|||||||
| TaskMeta | `list-statuses`, `list-priorities`, `list-efforts`, `list-tags`, `list-groups`, `create-group`, `update-group` |
|
| TaskMeta | `list-statuses`, `list-priorities`, `list-efforts`, `list-tags`, `list-groups`, `create-group`, `update-group` |
|
||||||
| TimeEntry | `list-time-entries`, `create-time-entry`, `update-time-entry`, `delete-time-entry` |
|
| TimeEntry | `list-time-entries`, `create-time-entry`, `update-time-entry`, `delete-time-entry` |
|
||||||
|
|
||||||
### Transports
|
|
||||||
|
|
||||||
| Transport | Usage | Auth |
|
|
||||||
|-----------|-------|------|
|
|
||||||
| **STDIO** | Claude Code sur la machine locale | Aucune |
|
|
||||||
| **HTTP** (`/_mcp`) | Clients MCP sur le réseau local | API token (`Authorization: Bearer <token>`) |
|
|
||||||
|
|
||||||
### Configuration locale (STDIO)
|
### Configuration locale (STDIO)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -55,17 +211,19 @@ Lesstime expose un serveur MCP (Model Context Protocol) permettant aux assistant
|
|||||||
### Gestion des tokens API
|
### Gestion des tokens API
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Générer un token pour un utilisateur
|
|
||||||
docker exec -u www-data php-lesstime-fpm php bin/console app:generate-api-token <username>
|
docker exec -u www-data php-lesstime-fpm php bin/console app:generate-api-token <username>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Mise en production (réseau local)
|
## Déploiement
|
||||||
|
|
||||||
1. Déployer le code sur le serveur
|
1. Déployer le code sur le serveur
|
||||||
2. `composer install --no-dev --optimize-autoloader`
|
2. `composer install --no-dev --optimize-autoloader`
|
||||||
3. `php bin/console doctrine:migrations:migrate --no-interaction`
|
3. `php bin/console doctrine:migrations:migrate --no-interaction`
|
||||||
4. `php bin/console cache:clear --env=prod`
|
4. `php bin/console cache:clear --env=prod`
|
||||||
5. `docker restart nginx-lesstime`
|
5. `cd frontend && npm install && npm run build:dist`
|
||||||
6. `php bin/console app:generate-api-token admin` — noter le token
|
6. `docker restart nginx-lesstime`
|
||||||
7. Ouvrir le port 8082 sur le firewall du serveur (LAN uniquement)
|
7. Ouvrir le port 8082 sur le firewall (LAN uniquement)
|
||||||
8. Configurer les clients MCP avec l'URL `http://<ip-serveur>:8082/_mcp` + le token
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Propriétaire — Tous droits réservés.
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.0'
|
app.version: '0.1.2'
|
||||||
|
|||||||
50
deploy/nginx/lesstime.conf
Normal file
50
deploy/nginx/lesstime.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
<!-- Delete button -->
|
<!-- Delete button -->
|
||||||
<button
|
<button
|
||||||
v-if="isAdmin"
|
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"
|
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)"
|
@click.stop="$emit('delete', doc)"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ const isOpen = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
if (confirmDeleteDocOpen.value || confirmDeleteOpen.value) return
|
||||||
isOpen.value = false
|
isOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,6 +391,8 @@ function populateForm(task: Task | null) {
|
|||||||
|
|
||||||
watch(() => props.modelValue, async (open) => {
|
watch(() => props.modelValue, async (open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
confirmDeleteDocOpen.value = false
|
||||||
|
documentToDelete.value = null
|
||||||
populateForm(props.task)
|
populateForm(props.task)
|
||||||
try {
|
try {
|
||||||
clientTickets.value = await clientTicketService.getAll({ project: props.projectId })
|
clientTickets.value = await clientTicketService.getAll({ project: props.projectId })
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<Teleport v-if="modelValue" to="body">
|
<Teleport v-if="modelValue" to="body">
|
||||||
<Transition name="modal" appear>
|
<Transition name="modal" appear>
|
||||||
<div class="fixed inset-0 z-[70] flex items-center justify-center">
|
<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">
|
<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>
|
<h3 class="text-lg font-bold text-neutral-900">{{ $t('taskDocuments.confirmDeleteTitle') }}</h3>
|
||||||
<p class="mt-3 text-sm text-neutral-600">
|
<p class="mt-3 text-sm text-neutral-600">
|
||||||
|
|||||||
@@ -236,20 +236,20 @@ onMounted(() => {
|
|||||||
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('myTasks.title') }}</h1>
|
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('myTasks.title') }}</h1>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
<button
|
<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'"
|
:class="viewMode === 'kanban' ? 'bg-primary-500 text-white' : 'text-neutral-400 hover:text-primary-500'"
|
||||||
:title="$t('myTasks.viewKanban')"
|
:title="$t('myTasks.viewKanban')"
|
||||||
@click="viewMode = 'kanban'"
|
@click="viewMode = 'kanban'"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:view-column-outline" size="20" />
|
<Icon name="mdi:view-column-outline" size="18" />
|
||||||
</button>
|
</button>
|
||||||
<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'"
|
:class="viewMode === 'list' ? 'bg-primary-500 text-white' : 'text-neutral-400 hover:text-primary-500'"
|
||||||
:title="$t('myTasks.viewList')"
|
:title="$t('myTasks.viewList')"
|
||||||
@click="viewMode = 'list'"
|
@click="viewMode = 'list'"
|
||||||
>
|
>
|
||||||
<Icon name="mdi:view-list-outline" size="20" />
|
<Icon name="mdi:view-list-outline" size="18" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
91
script/deploy-release.sh
Executable file
91
script/deploy-release.sh
Executable 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
|
||||||
@@ -54,7 +54,7 @@ class CreateTaskTool
|
|||||||
$task = new Task();
|
$task = new Task();
|
||||||
$task->setProject($project);
|
$task->setProject($project);
|
||||||
$task->setTitle($title);
|
$task->setTitle($title);
|
||||||
$task->setNumber($this->taskRepository->findMaxNumberByProject($project) + 1);
|
$task->setNumber($this->taskRepository->findMaxNumberByProjectForUpdate($project) + 1);
|
||||||
|
|
||||||
if (null !== $description) {
|
if (null !== $description) {
|
||||||
$task->setDescription($description);
|
$task->setDescription($description);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class TaskRepository extends ServiceEntityRepository
|
|||||||
$conn = $this->getEntityManager()->getConnection();
|
$conn = $this->getEntityManager()->getConnection();
|
||||||
|
|
||||||
$result = $conn->fetchOne(
|
$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()],
|
['project' => $project->getId()],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user