Compare commits

..

1 Commits

Author SHA1 Message Date
Gregor Vostrak
15b31b549c move project task count into its own sortable column
Also removes the ring around the project color dot, shrinks it slightly and tightens its spacing to the name.
2026-09-28 00:11:03 +02:00
15 changed files with 193 additions and 189 deletions

View File

@@ -1,6 +1,7 @@
ARG PHP_VERSION=8.3 ARG PHP_VERSION=8.3
ARG FRANKENPHP_VERSION=1.11 ARG FRANKENPHP_VERSION=1.8
ARG COMPOSER_VERSION=2.8 ARG COMPOSER_VERSION=2.8
ARG BUN_VERSION="latest"
ARG APP_ENV ARG APP_ENV
ARG DOCKER_FILES_BASE_PATH="docker/prod/" ARG DOCKER_FILES_BASE_PATH="docker/prod/"
@@ -15,13 +16,13 @@ RUN CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \ XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS=$(php-config --includes) \ CGO_CFLAGS=$(php-config --includes) \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \ CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
xcaddy build \ xcaddy build v2.10.0 \
--output /usr/local/bin/frankenphp \ --output /usr/local/bin/frankenphp \
--with github.com/dunglas/frankenphp=./ \ --with github.com/dunglas/frankenphp=./ \
--with github.com/dunglas/frankenphp/caddy=./caddy/ \ --with github.com/dunglas/frankenphp/caddy=./caddy/ \
--with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/caddy-cbrotli
FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION} FROM dunglas/frankenphp:${FRANKENPHP_VERSION}-php${PHP_VERSION} AS base
COPY --from=upstream /usr/local/bin/frankenphp /usr/local/bin/frankenphp COPY --from=upstream /usr/local/bin/frankenphp /usr/local/bin/frankenphp
@@ -31,28 +32,26 @@ LABEL org.opencontainers.image.description="solidtime is a modern open source ti
LABEL org.opencontainers.image.source="https://github.com/solidtime-io/solidtime" LABEL org.opencontainers.image.source="https://github.com/solidtime-io/solidtime"
LABEL org.opencontainers.image.licenses="AGPL" LABEL org.opencontainers.image.licenses="AGPL"
ARG USER_ID=1000 ARG WWWUSER=1000
ARG GROUP_ID=1000 ARG WWWGROUP=1000
ARG TZ=UTC ARG TZ=UTC
ARG APP_DIR=/var/www/html
ARG APP_ENV
ARG APP_HOST
ARG DOCKER_FILES_BASE_PATH ARG DOCKER_FILES_BASE_PATH
ENV DEBIAN_FRONTEND=noninteractive \ ENV DEBIAN_FRONTEND=noninteractive \
TERM=xterm-color \ TERM=xterm-color \
OCTANE_SERVER=frankenphp \ OCTANE_SERVER=frankenphp \
TZ=${TZ} \ TZ=${TZ} \
LANG=C.UTF-8 \ USER=octane \
USER=laravel \ ROOT=${APP_DIR} \
ROOT=/var/www/html \ APP_ENV=${APP_ENV} \
APP_ENV=production \
COMPOSER_ALLOW_SUPERUSER=1 \
COMPOSER_FUND=0 \ COMPOSER_FUND=0 \
COMPOSER_MAX_PARALLEL_HTTP=48 \ COMPOSER_MAX_PARALLEL_HTTP=24 \
WITH_HORIZON=false \ XDG_CONFIG_HOME=${APP_DIR}/.config \
WITH_SCHEDULER=false \ XDG_DATA_HOME=${APP_DIR}/.data \
WITH_REVERB=false \ SERVER_NAME=${APP_HOST}
WITH_SSR=false
ENV XDG_CONFIG_HOME=${ROOT}/.config XDG_DATA_HOME=${ROOT}/.data
WORKDIR ${ROOT} WORKDIR ${ROOT}
@@ -61,9 +60,6 @@ SHELL ["/bin/bash", "-eou", "pipefail", "-c"]
RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ RUN ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \
&& echo ${TZ} > /etc/timezone && echo ${TZ} > /etc/timezone
RUN echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
RUN apt-get update; \ RUN apt-get update; \
apt-get upgrade -yqq; \ apt-get upgrade -yqq; \
apt-get install -yqq --no-install-recommends --show-progress \ apt-get install -yqq --no-install-recommends --show-progress \
@@ -72,36 +68,40 @@ RUN apt-get update; \
wget \ wget \
vim \ vim \
git \ git \
unzip \
ncdu \ ncdu \
procps \ procps \
unzip \
ca-certificates \ ca-certificates \
supervisor \ supervisor \
libsodium-dev \ libsodium-dev \
# && curl -fsSL https://bun.sh/install | BUN_INSTALL=/usr bash \ libbrotli-dev \
# Install PHP extensions (included with dunglas/frankenphp)
&& install-php-extensions \ && install-php-extensions \
apcu \
bz2 \ bz2 \
pcntl \ pcntl \
mbstring \ mbstring \
bcmath \ bcmath \
sockets \ sockets \
pgsql \
pdo_pgsql \ pdo_pgsql \
opcache \ opcache \
exif \ exif \
pdo_mysql \ pdo_mysql \
zip \ zip \
uv \ uv \
vips \
intl \ intl \
gd \ gd \
redis \ redis \
rdkafka \ rdkafka \
ffi \ memcached \
igbinary \
ldap \ ldap \
&& apt-get -y autoremove \ && apt-get -y autoremove \
&& apt-get clean \ && apt-get clean \
&& docker-php-source delete \ && docker-php-source delete \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/lastlog /var/log/faillog && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& rm /var/log/lastlog /var/log/faillog
RUN arch="$(uname -m)" \ RUN arch="$(uname -m)" \
&& case "$arch" in \ && case "$arch" in \
@@ -111,64 +111,106 @@ RUN arch="$(uname -m)" \
x86) _cronic_fname='supercronic-linux-386' ;; \ x86) _cronic_fname='supercronic-linux-386' ;; \
*) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \ *) echo >&2 "error: unsupported architecture: $arch"; exit 1 ;; \
esac \ esac \
&& wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.38/${_cronic_fname}" \ && wget -q "https://github.com/aptible/supercronic/releases/download/v0.2.29/${_cronic_fname}" \
-O /usr/bin/supercronic \ -O /usr/bin/supercronic \
&& chmod +x /usr/bin/supercronic \ && chmod +x /usr/bin/supercronic \
&& mkdir -p /etc/supercronic \ && mkdir -p /etc/supercronic \
&& echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel && echo "*/1 * * * * php ${ROOT}/artisan schedule:run --no-interaction" > /etc/supercronic/laravel
RUN userdel --remove --force www-data \ RUN userdel --remove --force www-data \
&& groupadd --force -g ${GROUP_ID} ${USER} \ && groupadd --force -g ${WWWGROUP} ${USER} \
&& useradd -ms /bin/bash --no-log-init --no-user-group -g ${GROUP_ID} -u ${USER_ID} ${USER} && useradd -ms /bin/bash --no-log-init --no-user-group -g ${WWWGROUP} -u ${WWWUSER} ${USER} \
&& setcap -r /usr/local/bin/frankenphp
RUN chown -R ${USER}:${USER} ${ROOT} /var/{log,run} \
&& chmod -R a+rw ${ROOT} /var/{log,run}
RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini RUN cp ${PHP_INI_DIR}/php.ini-production ${PHP_INI_DIR}/php.ini
COPY --link --from=vendor /usr/bin/composer /usr/bin/composer USER ${USER}
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/supervisord.conf /etc/
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/octane/FrankenPHP/supervisord.frankenphp.conf /etc/supervisor/conf.d/
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/supervisord.*.conf /etc/supervisor/conf.d/
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/start-container /usr/local/bin/start-container
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/healthcheck /usr/local/bin/healthcheck
COPY --link ${DOCKER_FILES_BASE_PATH}deployment/php.ini ${PHP_INI_DIR}/conf.d/99-php.ini
#COPY --link composer.* ./
COPY --link --chown=${WWWUSER}:${WWWUSER} --from=vendor /usr/bin/composer /usr/bin/composer
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/supervisord.conf /etc/
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/octane/FrankenPHP/supervisord.frankenphp.conf /etc/supervisor/conf.d/
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/supervisord.*.conf /etc/supervisor/conf.d/
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/start-container /usr/local/bin/start-container
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/healthcheck /usr/local/bin/healthcheck
COPY --link --chown=${WWWUSER}:${WWWUSER} ${DOCKER_FILES_BASE_PATH}deployment/php.ini ${PHP_INI_DIR}/conf.d/99-octane.ini
RUN chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck
###########################################
#FROM base AS common
#
#USER ${USER}
#
#COPY --link --chown=${WWWUSER}:${WWWUSER} . .
#
#RUN composer install \ #RUN composer install \
# --no-dev \ # --no-dev \
# --no-interaction \ # --no-interaction \
# --no-autoloader \ # --no-autoloader \
# --no-ansi \ # --no-ansi \
# --no-scripts \ # --no-scripts \
# --no-progress \
# --audit # --audit
###########################################
# Build frontend assets with Bun
###########################################
#FROM oven/bun:${BUN_VERSION} AS build
#
#ARG APP_ENV
#
#ENV ROOT=/var/www/html \
# APP_ENV=${APP_ENV} \
# NODE_ENV=${APP_ENV:-production}
#
#WORKDIR ${ROOT}
#
#COPY --link package.json bun.lock* ./ #COPY --link package.json bun.lock* ./
#
#RUN bun install --frozen-lockfile #RUN bun install --frozen-lockfile
#
#COPY --link . .
#COPY --link --from=common ${ROOT}/vendor vendor
#
#RUN bun run build
COPY --link . . ###########################################
#FROM common AS runner
USER ${USER}
ENV WITH_HORIZON=false \
WITH_SCHEDULER=false \
WITH_REVERB=false
COPY --link --chown=${WWWUSER}:${WWWUSER} . ./
RUN test -z "$(find . -name .git -print -quit)"
#COPY --link --chown=${WWWUSER}:${WWWUSER} --from=build ${ROOT}/public public
RUN mkdir -p \ RUN mkdir -p \
storage/framework/{sessions,views,cache,testing} \ storage/framework/{sessions,views,cache,testing} \
storage/logs \ storage/logs \
bootstrap/cache \ bootstrap/cache && chmod -R a+rw storage
&& chmod +x /usr/local/bin/start-container /usr/local/bin/healthcheck
RUN composer dump-autoload \ #RUN composer install \
--optimize \ # --classmap-authoritative \
--apcu \ # --no-interaction \
--no-dev # --no-ansi \
# --no-dev \
# && composer clear-cache
#RUN bun run build RUN cat .env
#RUN php artisan env
RUN chown -R ${USER_ID}:${GROUP_ID} ${ROOT} \
&& find / -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null || true
USER ${USER}
EXPOSE 8000 EXPOSE 8000
#EXPOSE 2019
#EXPOSE 8080
ENTRYPOINT ["start-container"] ENTRYPOINT ["start-container"]
#HEALTHCHECK --start-period=30s --interval=10s --timeout=3s --retries=3 CMD healthcheck || exit 1 #HEALTHCHECK --start-period=5s --interval=2s --timeout=5s --retries=8 CMD healthcheck || exit 1

View File

@@ -22,13 +22,6 @@ elif [ "${container_mode}" = "reverb" ]; then
echo "Healthcheck failed." echo "Healthcheck failed."
exit 1 exit 1
fi fi
elif [ "${container_mode}" = "ssr" ]; then
if [ "$(supervisorctl status inertia-ssr-server:inertia-ssr-server_0 | awk '{print tolower($2)}')" = "running" ]; then
exit 0
else
echo "Healthcheck failed."
exit 1
fi
elif [ "${container_mode}" = "worker" ]; then elif [ "${container_mode}" = "worker" ]; then
if [ "$(supervisorctl status worker:worker_0 | awk '{print tolower($2)}')" = "running" ]; then if [ "$(supervisorctl status worker:worker_0 | awk '{print tolower($2)}')" = "running" ]; then
exit 0 exit 0

View File

@@ -3,14 +3,6 @@
admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT} admin {$CADDY_SERVER_ADMIN_HOST}:{$CADDY_SERVER_ADMIN_PORT}
log {
level {$CADDY_SERVER_LOG_LEVEL:WARN}
}
auto_https off
skip_install_trust
frankenphp { frankenphp {
worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT} worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT}
} }
@@ -28,7 +20,7 @@
{$CADDY_SERVER_SERVER_NAME} { {$CADDY_SERVER_SERVER_NAME} {
log { log {
level {$CADDY_SERVER_LOG_LEVEL:WARN} level WARN
format filter { format filter {
wrap {$CADDY_SERVER_LOGGER} wrap {$CADDY_SERVER_LOGGER}
@@ -68,6 +60,7 @@
error @rejected 401 error @rejected 401
php_server { php_server {
index frankenphp-worker.php
try_files {path} frankenphp-worker.php try_files {path} frankenphp-worker.php
resolve_root_symlink resolve_root_symlink
} }

View File

@@ -1,18 +1,65 @@
[program:octane] [program:octane]
process_name = %(program_name)s_%(process_num)s process_name = %(program_name)s_%(process_num)s
command = php %(ENV_ROOT)s/artisan octane:frankenphp --host=0.0.0.0 --port=8000 --admin-host=0.0.0.0 --admin-port=2019 --log-level=WARN --caddyfile=%(ENV_ROOT)s/docker/prod/deployment/octane/FrankenPHP/Caddyfile command = php %(ENV_ROOT)s/artisan octane:frankenphp --host=0.0.0.0 --port=8000 --admin-port=2019 --caddyfile=%(ENV_ROOT)s/docker/prod/deployment/octane/FrankenPHP/Caddyfile
user = %(ENV_USER)s user = %(ENV_USER)s
priority = 1 priority = 1
autostart = true autostart = true
autorestart = true autorestart = true
stopwaitsecs = 30
stopasgroup = true
killasgroup = true
environment = LARAVEL_OCTANE = "1" environment = LARAVEL_OCTANE = "1"
stdout_logfile = /dev/stdout stdout_logfile = /dev/stdout
stdout_logfile_maxbytes = 0 stdout_logfile_maxbytes = 0
stderr_logfile = /dev/stderr stderr_logfile = /dev/stderr
stderr_logfile_maxbytes = 0 stderr_logfile_maxbytes = 0
[program:horizon]
process_name = %(program_name)s_%(process_num)s
command = php %(ENV_ROOT)s/artisan horizon
user = %(ENV_USER)s
priority = 3
autostart = %(ENV_WITH_HORIZON)s
autorestart = true
stdout_logfile = %(ENV_ROOT)s/storage/logs/horizon.log
stdout_logfile_maxbytes = 200MB
stderr_logfile = %(ENV_ROOT)s/storage/logs/horizon.log
stderr_logfile_maxbytes = 200MB
stopwaitsecs = 3600
[program:scheduler]
process_name = %(program_name)s_%(process_num)s
command = supercronic -overlapping /etc/supercronic/laravel
user = %(ENV_USER)s
autostart = %(ENV_WITH_SCHEDULER)s
autorestart = true
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
stdout_logfile_maxbytes = 200MB
stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
stderr_logfile_maxbytes = 200MB
[program:clear-scheduler-cache]
process_name = %(program_name)s_%(process_num)s
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
user = %(ENV_USER)s
autostart = %(ENV_WITH_SCHEDULER)s
autorestart = false
startsecs = 0
startretries = 1
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
stdout_logfile_maxbytes = 200MB
stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
stderr_logfile_maxbytes = 200MB
[program:reverb]
process_name = %(program_name)s_%(process_num)s
command = php %(ENV_ROOT)s/artisan reverb:start
user = %(ENV_USER)s
priority = 2
autostart = %(ENV_WITH_REVERB)s
autorestart = true
stdout_logfile = %(ENV_ROOT)s/storage/logs/reverb.log
stdout_logfile_maxbytes = 200MB
stderr_logfile = %(ENV_ROOT)s/storage/logs/reverb.log
stderr_logfile_maxbytes = 200MB
minfds = 10000
[include] [include]
files = /etc/supervisord.conf /etc/supervisor/conf.d/supervisord.services.conf files = /etc/supervisord.conf

View File

@@ -2,9 +2,8 @@
post_max_size = 100M post_max_size = 100M
upload_max_filesize = 100M upload_max_filesize = 100M
expose_php = 0 expose_php = 0
realpath_cache_size = 32M realpath_cache_size = 16M
realpath_cache_ttl = 720 realpath_cache_ttl = 360
memory_limit = 256M
max_input_time = 5 max_input_time = 5
register_argc_argv = 0 register_argc_argv = 0
date.timezone = ${TZ:-UTC} date.timezone = ${TZ:-UTC}
@@ -12,24 +11,18 @@ date.timezone = ${TZ:-UTC}
[Opcache] [Opcache]
opcache.enable = 1 opcache.enable = 1
opcache.enable_cli = 1 opcache.enable_cli = 1
opcache.memory_consumption = 256 opcache.memory_consumption = 256M
opcache.use_cwd = 0 opcache.use_cwd = 0
opcache.save_comments = 1
opcache.max_file_size = 0 opcache.max_file_size = 0
opcache.max_accelerated_files = 32531 opcache.max_accelerated_files = 32531
opcache.validate_timestamps = 0 opcache.validate_timestamps = 0
opcache.file_update_protection = 0 opcache.file_update_protection = 0
opcache.interned_strings_buffer = 16 opcache.interned_strings_buffer = 16
opcache.enable_file_override = 1
opcache.file_cache_consistency_checks = 0
opcache.file_cache = /tmp/opcache-file-cache
[JIT] [JIT]
opcache.jit_buffer_size = 128M opcache.jit_buffer_size = 128M
opcache.jit = tracing opcache.jit = function
opcache.jit_hot_loop = 16 opcache.jit_prof_threshold = 0.001
opcache.jit_hot_func = 32
opcache.jit_hot_return = 4
opcache.jit_max_root_traces = 2048 opcache.jit_max_root_traces = 2048
opcache.jit_max_side_traces = 256 opcache.jit_max_side_traces = 256

View File

@@ -1,6 +1,8 @@
[supervisord] [supervisord]
nodaemon = true nodaemon = true
user = %(ENV_USER)s user = %(ENV_USER)s
logfile = /var/log/supervisor/supervisord.log
pidfile = /var/run/supervisord.pid
[supervisorctl] [supervisorctl]
@@ -8,4 +10,4 @@ user = %(ENV_USER)s
port = 127.0.0.1:9001 port = 127.0.0.1:9001
[rpcinterface:supervisor] [rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

View File

@@ -4,8 +4,6 @@ command = php %(ENV_ROOT)s/artisan horizon
user = %(ENV_USER)s user = %(ENV_USER)s
autostart = true autostart = true
autorestart = true autorestart = true
stopasgroup = true
killasgroup = true
stdout_logfile = /dev/stdout stdout_logfile = /dev/stdout
stdout_logfile_maxbytes = 0 stdout_logfile_maxbytes = 0
stderr_logfile = /dev/stderr stderr_logfile = /dev/stderr

View File

@@ -1,15 +0,0 @@
[program:inertia-ssr-server]
process_name = %(program_name)s_%(process_num)s
command = php %(ENV_ROOT)s/artisan inertia:start-ssr --runtime=bun --quiet
user = %(ENV_USER)s
autostart = true
autorestart = true
stopasgroup = true
killasgroup = true
stdout_logfile = /dev/stdout
stdout_logfile_maxbytes = 0
stderr_logfile = /dev/stderr
stderr_logfile_maxbytes = 0
[include]
files = /etc/supervisord.conf

View File

@@ -4,8 +4,6 @@ command = php %(ENV_ROOT)s/artisan reverb:start
user = %(ENV_USER)s user = %(ENV_USER)s
autostart = true autostart = true
autorestart = true autorestart = true
stopasgroup = true
killasgroup = true
stdout_logfile = /dev/stdout stdout_logfile = /dev/stdout
stdout_logfile_maxbytes = 0 stdout_logfile_maxbytes = 0
stderr_logfile = /dev/stderr stderr_logfile = /dev/stderr

View File

@@ -4,8 +4,6 @@ command = supercronic -overlapping /etc/supercronic/laravel
user = %(ENV_USER)s user = %(ENV_USER)s
autostart = true autostart = true
autorestart = true autorestart = true
stopasgroup = true
killasgroup = true
stdout_logfile = /dev/stdout stdout_logfile = /dev/stdout
stdout_logfile_maxbytes = 0 stdout_logfile_maxbytes = 0
stderr_logfile = /dev/stderr stderr_logfile = /dev/stderr

View File

@@ -1,69 +0,0 @@
[program:horizon]
process_name = %(program_name)s_%(process_num)s
command = php %(ENV_ROOT)s/artisan horizon
user = %(ENV_USER)s
priority = 4
autostart = %(ENV_WITH_HORIZON)s
autorestart = true
stopasgroup = true
killasgroup = true
stdout_logfile = %(ENV_ROOT)s/storage/logs/horizon.log
stdout_logfile_maxbytes = 200MB
stderr_logfile = %(ENV_ROOT)s/storage/logs/horizon.log
stderr_logfile_maxbytes = 200MB
stopwaitsecs = 3600
[program:scheduler]
process_name = %(program_name)s_%(process_num)s
command = supercronic -overlapping /etc/supercronic/laravel
user = %(ENV_USER)s
autostart = %(ENV_WITH_SCHEDULER)s
autorestart = true
stopasgroup = true
killasgroup = true
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
stdout_logfile_maxbytes = 200MB
stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
stderr_logfile_maxbytes = 200MB
[program:clear-scheduler-cache]
process_name = %(program_name)s_%(process_num)s
command = php %(ENV_ROOT)s/artisan schedule:clear-cache
user = %(ENV_USER)s
autostart = %(ENV_WITH_SCHEDULER)s
autorestart = false
startsecs = 0
startretries = 1
stdout_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
stdout_logfile_maxbytes = 200MB
stderr_logfile = %(ENV_ROOT)s/storage/logs/scheduler.log
stderr_logfile_maxbytes = 200MB
[program:reverb]
process_name = %(program_name)s_%(process_num)s
command = php %(ENV_ROOT)s/artisan reverb:start
user = %(ENV_USER)s
priority = 3
autostart = %(ENV_WITH_REVERB)s
autorestart = true
stopasgroup = true
killasgroup = true
stdout_logfile = %(ENV_ROOT)s/storage/logs/reverb.log
stdout_logfile_maxbytes = 200MB
stderr_logfile = %(ENV_ROOT)s/storage/logs/reverb.log
stderr_logfile_maxbytes = 200MB
minfds = 10000
[program:inertia-ssr-server]
process_name = %(program_name)s_%(process_num)s
command = php %(ENV_ROOT)s/artisan inertia:start-ssr --runtime=bun --quiet
user = %(ENV_USER)s
priority = 2
autostart = %(ENV_WITH_SSR)s
autorestart = true
stopasgroup = true
killasgroup = true
stdout_logfile = %(ENV_ROOT)s/storage/logs/inertia-ssr-server.log
stdout_logfile_maxbytes = 200MB
stderr_logfile = %(ENV_ROOT)s/storage/logs/inertia-ssr-server.log
stderr_logfile_maxbytes = 200MB

View File

@@ -4,8 +4,6 @@ command = %(ENV_WORKER_COMMAND)s
user = %(ENV_USER)s user = %(ENV_USER)s
autostart = true autostart = true
autorestart = true autorestart = true
stopasgroup = true
killasgroup = true
stdout_logfile = /dev/stdout stdout_logfile = /dev/stdout
stdout_logfile_maxbytes = 0 stdout_logfile_maxbytes = 0
stderr_logfile = /dev/stderr stderr_logfile = /dev/stderr

View File

@@ -10,13 +10,21 @@ import Pagination from '@/packages/ui/src/Pagination.vue';
import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue'; import LoadingSpinner from '@/packages/ui/src/LoadingSpinner.vue';
export type SortColumn = export type SortColumn =
'name' | 'client_name' | 'spent_time' | 'progress' | 'billable_rate' | 'status' | 'visibility'; | 'name'
| 'tasks'
| 'client_name'
| 'spent_time'
| 'progress'
| 'billable_rate'
| 'status'
| 'visibility';
export type { SortDirection } from '@/utils/useSortableTable'; export type { SortDirection } from '@/utils/useSortableTable';
import { canCreateProjects } from '@/utils/permissions'; import { canCreateProjects } from '@/utils/permissions';
import type { CreateProjectBody, Project, Client, CreateClientBody } from '@/packages/api/src'; import type { CreateProjectBody, Project, Client, CreateClientBody } from '@/packages/api/src';
import { useProjectsStore } from '@/utils/useProjects'; import { useProjectsStore } from '@/utils/useProjects';
import { useClientsStore } from '@/utils/useClients'; import { useClientsStore } from '@/utils/useClients';
import { useClientsQuery } from '@/utils/useClientsQuery'; import { useClientsQuery } from '@/utils/useClientsQuery';
import { useTasksQuery } from '@/utils/useTasksQuery';
import { getOrganizationCurrencyString } from '@/utils/money'; import { getOrganizationCurrencyString } from '@/utils/money';
import { isAllowedToPerformPremiumAction } from '@/utils/billing'; import { isAllowedToPerformPremiumAction } from '@/utils/billing';
import { useOrganizationQuery } from '@/utils/useOrganizationQuery'; import { useOrganizationQuery } from '@/utils/useOrganizationQuery';
@@ -56,6 +64,16 @@ const clientNameMap = computed(() => {
return map; return map;
}); });
const { tasks } = useTasksQuery();
const taskCountMap = computed(() => {
const map = new Map<string, number>();
tasks.value.forEach((task) => {
map.set(task.project_id, (map.get(task.project_id) ?? 0) + 1);
});
return map;
});
// Define column accessors for sorting. // Define column accessors for sorting.
// Numeric columns use sortDescFirst so that the first click (chevron down) sorts highest-first, // Numeric columns use sortDescFirst so that the first click (chevron down) sorts highest-first,
// while text columns default to ascending (A-Z) on first click (chevron down). // while text columns default to ascending (A-Z) on first click (chevron down).
@@ -64,6 +82,11 @@ const columns = computed<SortableColumnDef<Project, SortColumn>[]>(() => [
id: 'name', id: 'name',
accessorFn: (row: Project) => row.name.toLowerCase(), accessorFn: (row: Project) => row.name.toLowerCase(),
}, },
{
id: 'tasks',
sortDescFirst: true,
accessorFn: (row: Project) => taskCountMap.value.get(row.id) ?? 0,
},
{ {
id: 'client_name', id: 'client_name',
accessorFn: (row: Project) => { accessorFn: (row: Project) => {
@@ -161,7 +184,7 @@ async function createClient(client: CreateClientBody): Promise<Client | undefine
} }
const gridTemplate = computed(() => { const gridTemplate = computed(() => {
return `grid-template-columns: minmax(300px, 1fr) minmax(150px, auto) minmax(140px, auto) minmax(130px, auto) ${props.showBillableRate ? 'minmax(130px, auto)' : ''} minmax(120px, auto) minmax(120px, auto) 80px;`; return `grid-template-columns: minmax(300px, 1fr) minmax(100px, auto) minmax(150px, auto) minmax(140px, auto) minmax(130px, auto) ${props.showBillableRate ? 'minmax(130px, auto)' : ''} minmax(120px, auto) minmax(120px, auto) 80px;`;
}); });
</script> </script>

View File

@@ -36,6 +36,9 @@ function handleSort(column: SortColumn) {
@sort="handleSort"> @sort="handleSort">
Name Name
</SortableTableHeaderCell> </SortableTableHeaderCell>
<SortableTableHeaderCell column="tasks" v-bind="sortState" @sort="handleSort">
Tasks
</SortableTableHeaderCell>
<SortableTableHeaderCell column="client_name" v-bind="sortState" @sort="handleSort"> <SortableTableHeaderCell column="client_name" v-bind="sortState" @sort="handleSort">
Client Client
</SortableTableHeaderCell> </SortableTableHeaderCell>

View File

@@ -88,17 +88,17 @@ const showEditProjectModal = ref(false);
<ContextMenuTrigger as-child> <ContextMenuTrigger as-child>
<TableRow :href="route('projects.show', { project: project.id })"> <TableRow :href="route('projects.show', { project: project.id })">
<div <div
class="whitespace-nowrap min-w-0 flex items-center space-x-5 py-4 pr-3 text-sm font-medium text-text-primary pl-2 sm:pl-4 lg:pl-6"> class="whitespace-nowrap min-w-0 flex items-center space-x-3 py-4 pr-3 text-sm font-medium text-text-primary pl-2 sm:pl-4 lg:pl-6">
<div <div
:style="{ :style="{ backgroundColor: project.color }"
backgroundColor: project.color, class="w-2.5 h-2.5 ml-1 rounded-full"></div>
boxShadow: `var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) ${project.color}30`,
}"
class="w-3 h-3 ml-1 rounded-full"></div>
<span class="overflow-ellipsis overflow-hidden"> <span class="overflow-ellipsis overflow-hidden">
{{ project.name }} {{ project.name }}
</span> </span>
<span class="text-text-secondary"> {{ projectTasksCount }} Tasks </span> </div>
<div class="whitespace-nowrap px-3 py-4 text-sm text-text-primary">
<span v-if="projectTasksCount">{{ projectTasksCount }}</span>
<span v-else class="text-text-tertiary">--</span>
</div> </div>
<div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-text-primary"> <div class="whitespace-nowrap min-w-0 px-3 py-4 text-sm text-text-primary">
<div v-if="project.client_id" class="overflow-ellipsis overflow-hidden"> <div v-if="project.client_id" class="overflow-ellipsis overflow-hidden">