diff --git a/Dockerfile b/Dockerfile index aac0e05..f6ad05e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -65,14 +65,17 @@ COPY requirements.txt . RUN --mount=type=cache,target=/root/.cache/pip \ pip install --no-cache-dir -r requirements.txt -# Copy project files -COPY . . +# Create non-root user early (before copying files) +RUN useradd -m -u 1000 timetracker + +# Copy project files with correct ownership +COPY --chown=timetracker:timetracker . . # Also install certificate generation script to a stable path used by docs/compose -COPY scripts/generate-certs.sh /scripts/generate-certs.sh +COPY --chown=timetracker:timetracker scripts/generate-certs.sh /scripts/generate-certs.sh # Copy compiled assets from frontend stage (overwriting the stale one from COPY .) -COPY --from=frontend /app/app/static/dist/output.css /app/app/static/dist/output.css +COPY --chown=timetracker:timetracker --from=frontend /app/app/static/dist/output.css /app/app/static/dist/output.css # Create all directories and set permissions in a single layer RUN mkdir -p \ @@ -88,7 +91,7 @@ RUN mkdir -p \ && chmod -R 755 /app/app/static/uploads /app/static/uploads # Copy the startup script -COPY docker/start-fixed.py /app/start.py +COPY --chown=timetracker:timetracker docker/start-fixed.py /app/start.py # Fix line endings and set permissions in a single layer RUN find /app/docker -name "*.sh" -o -name "*.py" | xargs dos2unix 2>/dev/null || true \ @@ -113,10 +116,9 @@ RUN find /app/docker -name "*.sh" -o -name "*.py" | xargs dos2unix 2>/dev/null | /app/docker/simple_test.sh \ /scripts/generate-certs.sh -# Create non-root user and set ownership -RUN useradd -m -u 1000 timetracker \ - && chown -R timetracker:timetracker \ - /app \ +# Set ownership only for directories that need write access +# (Most files already have correct ownership from COPY --chown) +RUN chown -R timetracker:timetracker \ /data \ /app/logs \ /app/instance \ diff --git a/app/templates/admin/quote_pdf_layout.html b/app/templates/admin/quote_pdf_layout.html index abfacb4..2fb3560 100644 --- a/app/templates/admin/quote_pdf_layout.html +++ b/app/templates/admin/quote_pdf_layout.html @@ -1863,6 +1863,33 @@ function initializePDFEditor() { } }); + // For Image nodes, convert scale changes to width/height changes + // This ensures the size is stored correctly when resizing + if (node.className === 'Image') { + transformer.on('transformend', function() { + const image = node; + const scaleX = image.scaleX(); + const scaleY = image.scaleY(); + + // Update width and height based on scale + const newWidth = image.width() * scaleX; + const newHeight = image.height() * scaleY; + + // Reset scale to 1 and apply the new dimensions + image.width(newWidth); + image.height(newHeight); + image.scaleX(1); + image.scaleY(1); + + // Update properties panel if visible + if (selectedElement === image) { + updatePropertiesPanel(image); + } + + layer.draw(); + }); + } + layer.add(transformer); layer.draw(); @@ -2538,7 +2565,7 @@ function initializePDFEditor() { const w = Math.round(attrs.width || 100); const h = Math.round(attrs.height || 50); {% raw %} - bodyContent += ` Logo\n`; + bodyContent += ` Logo\n`; {% endraw %} } else if (child.className === 'Rect') { const w = Math.round(attrs.width || 100); @@ -3092,11 +3119,96 @@ table tr:last-child td { // Redraw grid for new size drawGrid(); + // Fix logo images: Konva Image nodes need to reload the image from URL + // When Konva Image nodes are serialized/deserialized, the image data is lost + // We need to reload the image from the URL and restore all attributes + if (LOGO_URL) { + const logoImages = layer.find('[name="logo"]'); + logoImages.forEach(function(logoNode) { + if (logoNode.className === 'Image') { + // Save all attributes before reloading + const savedAttrs = { + x: logoNode.x(), + y: logoNode.y(), + width: logoNode.width(), + height: logoNode.height(), + scaleX: logoNode.scaleX(), + scaleY: logoNode.scaleY(), + rotation: logoNode.rotation(), + opacity: logoNode.opacity(), + draggable: logoNode.draggable(), + visible: logoNode.visible() + }; + + // Store reference to update elements array + const oldNode = logoNode; + const parent = logoNode.getParent(); + const index = logoNode.getZIndex(); + + // Reload the image from URL + Konva.Image.fromURL(LOGO_URL, function(newImage) { + // Calculate actual size considering scale + // If scaleX/scaleY are not 1, the actual size is width*scaleX and height*scaleY + let finalWidth = savedAttrs.width; + let finalHeight = savedAttrs.height; + + // If scale was applied, we need to account for it + // Reset scale to 1 and apply the scaled dimensions as the new width/height + if (savedAttrs.scaleX !== 1 || savedAttrs.scaleY !== 1) { + finalWidth = savedAttrs.width * savedAttrs.scaleX; + finalHeight = savedAttrs.height * savedAttrs.scaleY; + } + + // Restore all attributes, but reset scale to 1 and use calculated dimensions + newImage.setAttrs({ + x: savedAttrs.x, + y: savedAttrs.y, + width: finalWidth, + height: finalHeight, + scaleX: 1, + scaleY: 1, + rotation: savedAttrs.rotation, + opacity: savedAttrs.opacity, + draggable: savedAttrs.draggable, + visible: savedAttrs.visible + }); + newImage.name('logo'); + + // Replace the old node with the new one + oldNode.destroy(); + parent.add(newImage); + newImage.zIndex(index); + + // Update elements array + const elementIndex = elements.findIndex(e => e.node === oldNode); + if (elementIndex !== -1) { + elements[elementIndex].node = newImage; + } else { + // Add to elements array if not found + elements.push({ type: 'logo', node: newImage }); + } + + // Setup selection for the new image + setupSelection(newImage); + + layer.draw(); + }, function(error) { + console.error('Failed to load logo image:', error); + // Keep the old node if image fails to load + }); + } + }); + } + // Re-setup selections for all elements (skip background and grid lines) layer.children.forEach(child => { if (child !== background && child.className !== 'Transformer' && !(child.className === 'Line' && child.attrs.name === 'grid-line')) { + // Skip logo images as they're handled above + if (child.className === 'Image' && child.attrs.name === 'logo') { + return; + } setupSelection(child); // If it's a table Group, also setup selection on children if (child.className === 'Group' && (child.attrs.name === 'items-table' || child.attrs.name === 'expenses-table')) { diff --git a/app/templates/base.html b/app/templates/base.html index 0d0777d..bb6961b 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -219,7 +219,7 @@ {% set work_open = ep.startswith('projects.') or ep.startswith('tasks.') or ep.startswith('issues.') or ep.startswith('timer.') or ep.startswith('kanban.') or ep.startswith('weekly_goals.') or ep.startswith('project_templates.') or ep.startswith('gantt.') %} {% set calendar_open = ep.startswith('calendar.') %} {% set finance_open = ep.startswith('reports.') or ep.startswith('invoices.') or ep.startswith('recurring_invoices.') or ep.startswith('payments.') or ep.startswith('expenses.') or ep.startswith('mileage.') or ep.startswith('per_diem.') or ep.startswith('budget_alerts.') or ep.startswith('invoice_approvals.') or ep.startswith('payment_gateways.') or ep.startswith('scheduled_reports.') or ep.startswith('custom_reports.') %} - {% set crm_open = ep.startswith('clients.') or ep.startswith('quotes.') %} + {% set crm_open = ep.startswith('clients.') or ep.startswith('quotes.') or ep.startswith('contacts.') or ep.startswith('deals.') or ep.startswith('leads.') %} {% set inventory_open = ep.startswith('inventory.') %} {% set analytics_open = ep.startswith('analytics.') %} {% set tools_open = ep.startswith('import_export.') or ep.startswith('saved_filters.') or ep.startswith('integrations.') %} @@ -244,7 +244,7 @@ {{ _('Dashboard') }} - {% if (not settings or settings.ui_allow_calendar) and current_user.ui_show_calendar %} + {% if is_module_enabled('calendar') %}
  • - {% if settings.ui_allow_project_templates and current_user.ui_show_project_templates %} + {% if is_module_enabled('project_templates') %}
  • {{ _('Project Templates') }}
  • {% endif %} - {% if settings.ui_allow_gantt_chart and current_user.ui_show_gantt_chart %} + {% if is_module_enabled('gantt') %}
  • {{ _('Gantt Chart') }} @@ -319,27 +319,34 @@ {{ _('Tasks') }}
  • - {% if settings.ui_allow_issues and current_user.ui_show_issues %} + {% if is_module_enabled('issues') %}
  • {{ _('Issues') }}
  • {% endif %} - {% if settings.ui_allow_kanban_board and current_user.ui_show_kanban_board %} + {% if is_module_enabled('kanban') %}
  • {{ _('Kanban Board') }}
  • {% endif %} - {% if settings.ui_allow_weekly_goals and current_user.ui_show_weekly_goals %} + {% if is_module_enabled('weekly_goals') %}
  • {{ _('Weekly Goals') }}
  • {% endif %} + {% if is_module_enabled('time_entry_templates') %} +
  • + + {{ _('Time Entry Templates') }} + +
  • + {% endif %}
  • @@ -351,12 +358,37 @@
  • {% endif %} + {% if is_module_enabled('invoices') %}
  • {{ _('Invoices') }}
  • - {% if settings.ui_allow_invoice_approvals and current_user.ui_show_invoice_approvals %} + {% endif %} + {% if is_module_enabled('invoice_approvals') %}
  • {{ _('Invoice Approvals') }}
  • {% endif %} - {% if settings.ui_allow_payment_gateways and current_user.ui_show_payment_gateways %} + {% if is_module_enabled('payment_gateways') %}
  • {{ _('Payment Gateways') }}
  • {% endif %} - {% if settings.ui_allow_recurring_invoices and current_user.ui_show_recurring_invoices %} + {% if is_module_enabled('recurring_invoices') %}
  • {{ _('Recurring Invoices') }}
  • {% endif %} - {% if settings.ui_allow_payments and current_user.ui_show_payments %} + {% if is_module_enabled('payments') %}
  • {{ _('Payments') }}
  • {% endif %} + {% if is_module_enabled('expenses') %}
  • {{ _('Expenses') }}
  • - {% if settings.ui_allow_mileage and current_user.ui_show_mileage %} + {% endif %} + {% if is_module_enabled('mileage') %}
  • {{ _('Mileage') }}
  • {% endif %} - {% if settings.ui_allow_per_diem and current_user.ui_show_per_diem %} + {% if is_module_enabled('per_diem') %}
  • {{ _('Per Diem') }}
  • {% endif %} - {% if settings.ui_allow_budget_alerts and current_user.ui_show_budget_alerts %} + {% if is_module_enabled('budget_alerts') %}
  • {{ _('Budget Alerts') }} @@ -479,7 +515,7 @@ {% endif %}
  • - {% if settings.ui_allow_inventory and current_user.ui_show_inventory %} + {% if is_module_enabled('inventory') %}
  • {% endif %} - {% if settings.ui_allow_analytics and current_user.ui_show_analytics %} + {% if is_module_enabled('analytics') %}
  • @@ -564,7 +600,7 @@
  • {% endif %} - {% if settings.ui_allow_tools and current_user.ui_show_tools %} + {% if is_module_enabled('integrations') or is_module_enabled('import_export') or is_module_enabled('saved_filters') %}
  • {% endif %} @@ -650,6 +692,11 @@ {{ _('Settings') }} +
  • + + {{ _('Module Management') }} + +
  • {{ _('Email Configuration') }} @@ -666,7 +713,7 @@ {{ _('PDF Templates') }} -