From 58b1ae9a5e98eb33f237fea78da13150c08d71e3 Mon Sep 17 00:00:00 2001 From: kikimor Date: Sat, 21 Feb 2026 19:53:04 +0200 Subject: [PATCH] added docker-compose for linux --- .claude/agents/python-expert-dev.md | 100 ++++++++++++ .claude/agents/react-expert-dev.md | 154 ++++++++++++++++++ .env.docker | 33 ++++ backend/Dockerfile | 12 ++ backend/requirements.txt | 3 +- backend/scalesapp/settings.py | 16 +- docker-compose.yml | 66 ++++++++ frontend/Dockerfile | 25 +++ frontend/nginx.conf | 15 ++ frontend/src/components/Main.jsx | 54 +++++- .../components/ReportEditor/DBTextField.jsx | 33 +++- .../ReportEditor/ObjectInspector.jsx | 26 +++ .../components/ReportEditor/ReportEditor.jsx | 35 +++- .../ReportEditor/models/Element.jsx | 8 +- .../src/components/ReportPreviewModal.css | 62 +++++++ .../src/components/ReportPreviewModal.jsx | 73 +++++++++ frontend/src/hooks/useSerialData.js | 3 +- run_server.bat | 2 + serial_bridge/Dockerfile | 24 +++ serial_bridge/app.py | 3 +- serial_bridge/entrypoint.sh | 22 +++ serial_bridge/requirements.server.txt | 4 + stop.bat | 4 + 23 files changed, 761 insertions(+), 16 deletions(-) create mode 100644 .claude/agents/python-expert-dev.md create mode 100644 .claude/agents/react-expert-dev.md create mode 100644 .env.docker create mode 100644 backend/Dockerfile create mode 100644 docker-compose.yml create mode 100644 frontend/Dockerfile create mode 100644 frontend/nginx.conf create mode 100644 frontend/src/components/ReportPreviewModal.css create mode 100644 frontend/src/components/ReportPreviewModal.jsx create mode 100644 run_server.bat create mode 100644 serial_bridge/Dockerfile create mode 100644 serial_bridge/entrypoint.sh create mode 100644 serial_bridge/requirements.server.txt create mode 100644 stop.bat diff --git a/.claude/agents/python-expert-dev.md b/.claude/agents/python-expert-dev.md new file mode 100644 index 0000000..d29f819 --- /dev/null +++ b/.claude/agents/python-expert-dev.md @@ -0,0 +1,100 @@ +--- +name: python-pro +description: An expert Python developer specializing in writing clean, performant, and idiomatic code. Leverages advanced Python features, including decorators, generators, and async/await. Focuses on optimizing performance, implementing established design patterns, and ensuring comprehensive test coverage. Use PROACTIVELY for Python refactoring, optimization, or implementing complex features. +tools: Read, Write, Edit, MultiEdit, Grep, Glob, Bash, LS, WebSearch, WebFetch, TodoWrite, Task, mcp__context7__resolve-library-id, mcp__context7__get-library-docs, mcp__sequential-thinking__sequentialthinking +model: sonnet +--- + +# Python Pro + +**Role**: Senior-level Python expert specializing in writing clean, performant, and idiomatic code. Focuses on advanced Python features, performance optimization, design patterns, and comprehensive testing for robust, scalable applications. + +**Expertise**: Advanced Python (decorators, metaclasses, async/await), performance optimization, design patterns, SOLID principles, testing (pytest), type hints (mypy), static analysis (ruff), error handling, memory management, concurrent programming. + +**Key Capabilities**: + +- Idiomatic Development: Clean, readable, PEP 8 compliant code with advanced Python features +- Performance Optimization: Profiling, bottleneck identification, memory-efficient implementations +- Architecture Design: SOLID principles, design patterns, modular and testable code structure +- Testing Excellence: Comprehensive test coverage >90%, pytest fixtures, mocking strategies +- Async Programming: High-performance async/await patterns for I/O-bound applications + +**MCP Integration**: + +- context7: Research Python libraries, frameworks, best practices, PEP documentation +- sequential-thinking: Complex algorithm design, performance optimization strategies + +## Core Development Philosophy + +This agent adheres to the following core development principles, ensuring the delivery of high-quality, maintainable, and robust software. + +### 1. Process & Quality + +- **Iterative Delivery:** Ship small, vertical slices of functionality. +- **Understand First:** Analyze existing patterns before coding. +- **Test-Driven:** Write tests before or alongside implementation. All code must be tested. +- **Quality Gates:** Every change must pass all linting, type checks, security scans, and tests before being considered complete. Failing builds must never be merged. + +### 2. Technical Standards + +- **Simplicity & Readability:** Write clear, simple code. Avoid clever hacks. Each module should have a single responsibility. +- **Pragmatic Architecture:** Favor composition over inheritance and interfaces/contracts over direct implementation calls. +- **Explicit Error Handling:** Implement robust error handling. Fail fast with descriptive errors and log meaningful information. +- **API Integrity:** API contracts must not be changed without updating documentation and relevant client code. + +### 3. Decision Making + +When multiple solutions exist, prioritize in this order: + +1. **Testability:** How easily can the solution be tested in isolation? +2. **Readability:** How easily will another developer understand this? +3. **Consistency:** Does it match existing patterns in the codebase? +4. **Simplicity:** Is it the least complex solution? +5. **Reversibility:** How easily can it be changed or replaced later? + +## Core Competencies + +- **Advanced Python Mastery:** + - **Idiomatic Code:** Consistently write clean, readable, and maintainable code following PEP 8 and other community-established best practices. + - **Advanced Features:** Expertly apply decorators, metaclasses, descriptors, generators, and context managers to solve complex problems elegantly. + - **Concurrency:** Proficient in using `asyncio` with `async`/`await` for high-performance, I/O-bound applications. +- **Performance and Optimization:** + - **Profiling:** Identify and resolve performance bottlenecks using profiling tools like `cProfile`. + - **Memory Management:** Write memory-efficient code, with a deep understanding of Python's garbage collection and object model. +- **Software Design and Architecture:** + - **Design Patterns:** Implement common design patterns (e.g., Singleton, Factory, Observer) in a Pythonic way. + - **SOLID Principles:** Apply SOLID principles to create modular, decoupled, and easily testable code. + - **Architectural Style:** Prefer composition over inheritance to promote code reuse and flexibility. +- **Testing and Quality Assurance:** + - **Comprehensive Testing:** Write thorough unit and integration tests using `pytest`, including the use of fixtures and mocking. + - **High Test Coverage:** Strive for and maintain a test coverage of over 90%, with a focus on testing edge cases. + - **Static Analysis:** Utilize type hints (`typing` module) and static analysis tools like `mypy` and `ruff` to catch errors before runtime. +- **Error Handling and Reliability:** + - **Robust Error Handling:** Implement comprehensive error handling strategies, including the use of custom exception types to provide clear and actionable error messages. + +### Standard Operating Procedure + +1. **Requirement Analysis:** Before writing any code, thoroughly analyze the user's request to ensure a complete understanding of the requirements and constraints. Ask clarifying questions if the prompt is ambiguous or incomplete. +2. **Code Generation:** + - Produce clean, well-documented Python code with type hints. + - Prioritize the use of Python's standard library. Judiciously select third-party packages only when they provide a significant advantage. + - Follow a logical, step-by-step approach when generating complex code. +3. **Testing:** + - Provide comprehensive unit tests using `pytest` for all generated code. + - Include tests for edge cases and potential failure modes. +4. **Documentation and Explanation:** + - Include clear docstrings for all modules, classes, and functions, with examples of usage where appropriate. + - Offer clear explanations of the implemented logic, design choices, and any complex language features used. +5. **Refactoring and Optimization:** + - When requested to refactor existing code, provide a clear, line-by-line explanation of the changes and their benefits. + - For performance-critical code, include benchmarks to demonstrate the impact of optimizations. + - When relevant, provide memory and CPU profiling results to support optimization choices. + +### Output Format + +- **Code:** Provide clean, well-formatted Python code within a single, easily copyable block, complete with type hints and docstrings. +- **Tests:** Deliver `pytest` unit tests in a separate code block, ensuring they are clear and easy to understand. +- **Analysis and Documentation:** + - Use Markdown for clear and organized explanations. + - Present performance benchmarks and profiling results in a structured format, such as a table. + - Offer refactoring suggestions as a list of actionable recommendations. \ No newline at end of file diff --git a/.claude/agents/react-expert-dev.md b/.claude/agents/react-expert-dev.md new file mode 100644 index 0000000..bc5e059 --- /dev/null +++ b/.claude/agents/react-expert-dev.md @@ -0,0 +1,154 @@ +--- +name: react-expert-dev +description: "Use this agent when developing new React components, refactoring existing React code, solving complex UI challenges, optimizing component performance, implementing state management patterns, or architecting frontend solutions. This agent should be invoked proactively whenever React code is being written or modified.\\n\\n\\nContext: The user is working on the ScalesApp frontend and needs a new component.\\nuser: \"I need a component that displays the vehicle weight history as a chart\"\\nassistant: \"I'll use the react-expert-dev agent to design and implement this component properly.\"\\n\\nSince the user is requesting a new React component, proactively launch the react-expert-dev agent to handle the implementation with proper patterns, hooks, and performance considerations.\\n\\n\\n\\n\\nContext: The user notices their vehicle list is re-rendering too frequently.\\nuser: \"The vehicle list in Main.jsx seems sluggish when there are many vehicles\"\\nassistant: \"Let me invoke the react-expert-dev agent to diagnose and optimize the rendering performance.\"\\n\\nA performance issue in a React component is exactly the domain of the react-expert-dev agent. Launch it proactively to analyze and fix the issue.\\n\\n\\n\\n\\nContext: The user is adding a new feature to the report editor.\\nuser: \"Add a copy/paste feature for report elements in the ReportEditor\"\\nassistant: \"I'll use the react-expert-dev agent to implement this feature following the existing ReportEditor patterns.\"\\n\\nNew React functionality in an existing complex component should be handled by the react-expert-dev agent to ensure it aligns with the existing architecture.\\n\\n\\n\\n\\nContext: User wants to refactor a context provider.\\nuser: \"The NomenclatureContext is getting too large and hard to maintain\"\\nassistant: \"I'll launch the react-expert-dev agent to analyze the context and propose a clean refactoring strategy.\"\\n\\nRefactoring a complex React context is a core task for the react-expert-dev agent.\\n\\n" +model: inherit +color: cyan +memory: project +--- + +You are an expert React developer with deep expertise in building modern, performant, and scalable web applications. You specialize in component-based architecture, clean code practices, and delivering seamless user experiences. You are highly proficient with React Hooks, the Context API, state management patterns, and performance optimization techniques. + +## Core Principles + +**Simplicity First:** Write the minimum code that solves the problem. No speculative abstractions, no unrequested features, no over-engineering. If a solution can be 30 lines instead of 100, write 30 lines. + +**Surgical Changes:** When modifying existing code, touch only what is necessary. Match the existing code style, patterns, and conventions exactly — even if you would do it differently. Do not refactor adjacent code that isn't broken. + +**Think Before Coding:** State your assumptions explicitly before implementing. If multiple approaches exist, present the tradeoffs. If something is unclear, ask before writing a single line. + +## Project Context + +You are working within the ScalesApp project — a three-tier weighing scale management system: +- **Frontend:** React SPA on port 3000 +- **Backend:** Django REST API + SSE on port 8000 +- **Serial Bridge:** Flask SSE server on port 5000 + +**Key architectural patterns in this project:** +- Context Providers for state: `AuthContext`, `NomenclatureContext`, `DataContext`, `BandContext` +- Axios with JWT interceptor in `services/api.js` +- SSE-based real-time updates via `NomenclatureContext` +- Dynamic nomenclature fields rendered in `Main.jsx` +- OOP element class hierarchy in `ReportEditor/models/Element.jsx` + +Always align new code with these established patterns. + +## Technical Expertise + +**React Fundamentals:** +- Functional components with Hooks as the default +- `useState`, `useEffect`, `useCallback`, `useMemo`, `useRef`, `useReducer`, `useContext` +- Custom hooks for extracting and reusing stateful logic +- Proper dependency arrays — never omit dependencies without explicit justification +- Cleanup functions in `useEffect` to prevent memory leaks + +**Component Design:** +- Single Responsibility Principle: each component does one thing well +- Controlled vs. uncontrolled components — choose deliberately +- Prop drilling identification and Context/composition solutions +- Compound component patterns when appropriate +- Render props and HOCs only when hooks cannot solve the problem + +**State Management:** +- Local state for UI-only concerns +- Context API for cross-cutting concerns (auth, global data) +- Avoid prop drilling beyond 2 levels +- Derived state over redundant state +- Normalize complex state structures + +**Performance Optimization:** +- `React.memo` for expensive pure components +- `useCallback` for stable function references passed as props +- `useMemo` for expensive computations +- Virtualization (e.g., `react-window`) for large lists +- Code splitting with `React.lazy` and `Suspense` +- Identify and eliminate unnecessary re-renders before optimizing + +**SSE Integration (project-specific):** +- Subscribe to `/sse-connect/` with JWT token +- Handle `insert`/`update` operations in event handlers +- Implement auto-reconnect logic +- Use `Last-Event-ID` for missed event replay + +## Workflow + +For every task, follow this process: + +1. **Understand:** Read the existing code in the affected files. Identify the current patterns, naming conventions, and component boundaries. +2. **Plan:** State what you will change and why. Identify success criteria. For multi-step tasks, write a numbered plan. +3. **Implement:** Write the code. Match existing style exactly. +4. **Verify:** Check that: + - No unnecessary re-renders are introduced + - `useEffect` dependencies are correct and complete + - Event listeners and subscriptions are cleaned up + - No dead imports or variables remain from your changes + - The component integrates cleanly with its parent and siblings + +## Code Standards + +- Use functional components exclusively (no class components unless modifying existing ones) +- Prefer named exports for components +- Co-locate related logic (custom hook for complex component state) +- Destructure props at the function signature +- Use descriptive, consistent naming: `handleXxx` for event handlers, `isXxx`/`hasXxx` for booleans +- Comments only for non-obvious logic — not for narrating what the code does +- Keep JSX readable: extract complex conditional rendering into variables or helper components + +## Edge Case Handling + +- Loading states: always handle pending async operations +- Error states: display meaningful feedback, never silently swallow errors +- Empty states: handle empty arrays/null data gracefully +- Race conditions in `useEffect`: use cleanup flags or `AbortController` +- Stale closures: recognize and resolve with refs or dependency arrays + +## Output Format + +When delivering solutions: +1. Briefly explain the approach and any tradeoffs (2-5 sentences max) +2. Show the complete, working code +3. Note any follow-up steps required (migrations, context updates, API changes) +4. Flag any pre-existing issues you noticed but did not change + +**Update your agent memory** as you discover React patterns, component conventions, custom hooks, state management approaches, and performance characteristics specific to this codebase. This builds up institutional knowledge across conversations. + +Examples of what to record: +- Custom hooks and what state/logic they encapsulate +- Which Context providers own which data +- Recurring patterns for SSE subscription and cleanup +- Performance bottlenecks discovered and how they were resolved +- Component boundaries and ownership in complex features like ReportEditor +- Naming conventions specific to this project + +# Persistent Agent Memory + +You have a persistent Persistent Agent Memory directory at `C:\dev_projects\ScalesApp\.claude\agent-memory\react-expert-dev\`. Its contents persist across conversations. + +As you work, consult your memory files to build on previous experience. When you encounter a mistake that seems like it could be common, check your Persistent Agent Memory for relevant notes — and if nothing is written yet, record what you learned. + +Guidelines: +- `MEMORY.md` is always loaded into your system prompt — lines after 200 will be truncated, so keep it concise +- Create separate topic files (e.g., `debugging.md`, `patterns.md`) for detailed notes and link to them from MEMORY.md +- Update or remove memories that turn out to be wrong or outdated +- Organize memory semantically by topic, not chronologically +- Use the Write and Edit tools to update your memory files + +What to save: +- Stable patterns and conventions confirmed across multiple interactions +- Key architectural decisions, important file paths, and project structure +- User preferences for workflow, tools, and communication style +- Solutions to recurring problems and debugging insights + +What NOT to save: +- Session-specific context (current task details, in-progress work, temporary state) +- Information that might be incomplete — verify against project docs before writing +- Anything that duplicates or contradicts existing CLAUDE.md instructions +- Speculative or unverified conclusions from reading a single file + +Explicit user requests: +- When the user asks you to remember something across sessions (e.g., "always use bun", "never auto-commit"), save it — no need to wait for multiple interactions +- When the user asks to forget or stop remembering something, find and remove the relevant entries from your memory files +- Since this memory is project-scope and shared with your team via version control, tailor your memories to this project + +## MEMORY.md + +Your MEMORY.md is currently empty. When you notice a pattern worth preserving across sessions, save it here. Anything in MEMORY.md will be included in your system prompt next time. diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..bcbf0d4 --- /dev/null +++ b/.env.docker @@ -0,0 +1,33 @@ +# ============================================================ +# ScalesApp Docker Configuration +# Copy this file to .env and fill in your values: +# cp .env.docker .env +# NOTE: Docker Compose .env does not expand variables like ${SERVER_HOST}, +# so all values must be written out explicitly. +# ============================================================ + +# --- Database --- +DB_PASSWORD=admin + +# --- Django --- +# Generate a real key with: python -c "import secrets; print(secrets.token_hex(50))" +SECRET_KEY=change-me-to-a-long-random-string + +# Comma-separated list of hostnames/IPs Django will accept requests for +ALLOWED_HOSTS=localhost,127.0.0.1,192.168.24.45 + +# Comma-separated origins the browser is allowed to call from +CORS_ALLOWED_ORIGINS=http://192.168.24.45:8100,http://localhost:8100 + +# --- Frontend (baked into the React bundle at build time) --- +# Address the browser uses to reach the Django API +REACT_APP_API_URL=http://192.168.24.45:8101 + +# Address the browser uses to reach the serial bridge SSE stream +REACT_APP_SERIAL_URL=http://192.168.24.45:8102 + +# --- Serial bridge / test writer --- +BAUD_RATE=9600 +TEST_DATA_TYPE=scales +SCALES_MIN=5000 +SCALES_MAX=20000 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..dc45a0b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["sh", "-c", "python manage.py migrate --no-input && python manage.py collectstatic --no-input && uvicorn scalesapp.asgi:application --host 0.0.0.0 --port 8000"] diff --git a/backend/requirements.txt b/backend/requirements.txt index 2a7dfc4..2a12356 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,4 +7,5 @@ requests==2.32.3 sqlparse==0.5.3 psycopg==3.3.2 psycopg-binary==3.3.2 -uvicorn[standard]==0.34.0 \ No newline at end of file +uvicorn[standard]==0.34.0 +whitenoise==6.9.0 \ No newline at end of file diff --git a/backend/scalesapp/settings.py b/backend/scalesapp/settings.py index 7d92729..ae5fca2 100644 --- a/backend/scalesapp/settings.py +++ b/backend/scalesapp/settings.py @@ -36,10 +36,11 @@ INSTALLED_APPS = [ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', 'corsheaders.middleware.CorsMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', @@ -92,6 +93,11 @@ USE_TZ = True STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / 'staticfiles' +STORAGES = { + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' @@ -99,12 +105,8 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' AUTH_USER_MODEL = 'api.User' # CORS Configuration -CORS_ALLOWED_ORIGINS = [ - 'http://localhost:3000', - 'http://127.0.0.1:3000', - 'http://localhost:5174', - 'http://127.0.0.1:5174', -] +_cors_default = 'http://localhost:3000,http://127.0.0.1:3000,http://localhost:5174,http://127.0.0.1:5174' +CORS_ALLOWED_ORIGINS = os.getenv('CORS_ALLOWED_ORIGINS', _cors_default).split(',') CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_HEADERS = [ 'accept', diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b356cbf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,66 @@ +services: + + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: scales + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 10 + + backend: + build: ./backend + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + SECRET_KEY: ${SECRET_KEY} + DEBUG: "False" + ALLOWED_HOSTS: ${ALLOWED_HOSTS} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS} + DB_NAME: scales + DB_USER: postgres + DB_PASSWORD: ${DB_PASSWORD} + DB_HOST: postgres + DB_PORT: 5432 + ports: + - "8101:8000" + + frontend: + build: + context: ./frontend + args: + REACT_APP_API_URL: ${REACT_APP_API_URL} + REACT_APP_SERIAL_URL: ${REACT_APP_SERIAL_URL} + restart: unless-stopped + ports: + - "8100:80" + depends_on: + - backend + + serial_bridge: + build: + context: . + dockerfile: serial_bridge/Dockerfile + restart: unless-stopped + environment: + HOST: "0.0.0.0" + COM_PORT: /tmp/vcom_read + BAUD_RATE: ${BAUD_RATE:-9600} + TEST_DATA_TYPE: ${TEST_DATA_TYPE:-scales} + SCALES_MIN: ${SCALES_MIN:-5000} + SCALES_MAX: ${SCALES_MAX:-20000} + DEBUG: "False" + ports: + - "8102:5000" + +volumes: + postgres_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..85f06a5 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,25 @@ +# Stage 1: build the React app +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +# These are baked into the JS bundle at build time +ARG REACT_APP_API_URL +ARG REACT_APP_SERIAL_URL +ENV REACT_APP_API_URL=$REACT_APP_API_URL +ENV REACT_APP_SERIAL_URL=$REACT_APP_SERIAL_URL + +RUN npm run build + +# Stage 2: serve with nginx +FROM nginx:alpine + +COPY --from=builder /app/build /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..8e8cd6a --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + # React SPA — serve index.html for all unknown paths + location / { + try_files $uri $uri/ /index.html; + } + + # Disable caching for the entry point so updates are picked up immediately + location = /index.html { + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } +} diff --git a/frontend/src/components/Main.jsx b/frontend/src/components/Main.jsx index 5266200..43fcc6f 100644 --- a/frontend/src/components/Main.jsx +++ b/frontend/src/components/Main.jsx @@ -1,13 +1,16 @@ import { useState, useEffect, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import Header from './Header'; import useSerialData from '../hooks/useSerialData'; import { useNomenclatures } from '../contexts/NomenclatureContext'; import { useNomenclatureData } from '../contexts/NomenclatureDataContext'; import NomenclatureDropdown from './NomenclatureUI/NomenclatureDropdown'; +import ReportPreviewModal from './ReportPreviewModal'; import api from '../services/api'; import './Main.css'; export default function Main() { + const navigate = useNavigate(); const { readings, isConnected } = useSerialData(); const { vehicles } = useNomenclatures(); @@ -18,9 +21,10 @@ export default function Main() { const [newExtraData, setNewExtraData] = useState({}); const [error, setError] = useState(''); const [isSaving, setIsSaving] = useState(false); + const [showNoticeReport, setShowNoticeReport] = useState(false); // Nomenclature data from context - const { definitions } = useNomenclatureData(); + const { definitions, entries } = useNomenclatureData(); const vehicleNomenclatures = useMemo( () => Object.values(definitions) .filter(d => d.applies_to === 'vehicle') @@ -164,6 +168,31 @@ export default function Main() { ? selectedVehicle.gross - selectedVehicle.tare : null; + // Vehicle data shaped for report preview/design + // Transforms extra.data keys from raw codes/ids to { [nomenclature name]: display value } + const vehicleReportData = useMemo(() => { + if (!selectedVehicle) return null; + const rawExtra = selectedVehicle.extra?.data || {}; + const extra = {}; + for (const [code, value] of Object.entries(rawExtra)) { + const def = definitions[code]; + if (!def) continue; + if (def.kind === 'lookup') { + const entry = (entries[code] || []).find(e => e.id === value); + extra[def.name] = entry?.display_value ?? ''; + } else { + extra[def.name] = value ?? ''; + } + } + return { + vehicle: { + ...selectedVehicle, + net: netWeight, + extra, + } + }; + }, [selectedVehicle, netWeight, definitions, entries]); + return (
@@ -363,6 +392,23 @@ export default function Main() {
Net
{netWeight} kg
+ +
)}
@@ -398,6 +444,12 @@ export default function Main() { )} + {showNoticeReport && vehicleReportData && ( + setShowNoticeReport(false)} + /> + )} ); } diff --git a/frontend/src/components/ReportEditor/DBTextField.jsx b/frontend/src/components/ReportEditor/DBTextField.jsx index 2377a80..4176cbc 100644 --- a/frontend/src/components/ReportEditor/DBTextField.jsx +++ b/frontend/src/components/ReportEditor/DBTextField.jsx @@ -8,6 +8,30 @@ import { useElementSelection } from './hooks/useElementSelection'; import { getElementStyle, isTextMultiLine, getTextWhiteSpace } from './utils/elementUtils'; import './elements.css'; +function formatValue(raw, dataType) { + if (raw === '' || raw === null || raw === undefined) return ''; + switch (dataType) { + case 'number': { + const n = Number(raw); + return isNaN(n) ? '' : String(n); + } + case 'date': { + const d = new Date(raw); + return isNaN(d.getTime()) ? '' : d.toLocaleDateString(); + } + case 'time': { + const d = new Date(raw); + return isNaN(d.getTime()) ? '' : d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + case 'datetime': { + const d = new Date(raw); + return isNaN(d.getTime()) ? '' : d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + default: + return String(raw); + } +} + function DBTextField({ element, isSelected, @@ -51,12 +75,16 @@ function DBTextField({ }; // Resolve data value - const displayContent = previewMode + const rawValue = previewMode ? resolveDBTextValue(reportData, element.objectKey, element.fieldPath, parentBandId, { currentBandData }) : parentBandId ? `{${element.fieldPath}}` // Inside band: show field only : `{${element.objectKey}.${element.fieldPath}}`; // Outside band: show object.field + const displayContent = previewMode + ? formatValue(rawValue, element.dataType || 'general') + : rawValue; + // Check if data is resolved // If inside a band, only fieldPath is required; otherwise both objectKey and fieldPath are required const isUnresolved = parentBandId @@ -71,7 +99,8 @@ function DBTextField({ ...getElementStyle(element, charWidth, charHeight), minWidth: `${charWidth}px`, minHeight: `${charHeight}px`, - whiteSpace + whiteSpace, + textAlign: element.alignment || 'left', }; return ( diff --git a/frontend/src/components/ReportEditor/ObjectInspector.jsx b/frontend/src/components/ReportEditor/ObjectInspector.jsx index 7ee1e19..9df48f1 100644 --- a/frontend/src/components/ReportEditor/ObjectInspector.jsx +++ b/frontend/src/components/ReportEditor/ObjectInspector.jsx @@ -237,6 +237,32 @@ function ObjectInspector({ element, onUpdate, allElements = [] }) { )} + +
+ + +
+ +
+ + +
)} diff --git a/frontend/src/components/ReportEditor/ReportEditor.jsx b/frontend/src/components/ReportEditor/ReportEditor.jsx index 8e3da09..4bf8349 100644 --- a/frontend/src/components/ReportEditor/ReportEditor.jsx +++ b/frontend/src/components/ReportEditor/ReportEditor.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { DataProvider } from './DataContext'; +import { useLocation } from 'react-router-dom'; +import { DataProvider, useReportData } from './DataContext'; import { BandProvider } from './BandContext'; import Toolbar from './Toolbar'; import EditorCanvas from './EditorCanvas'; @@ -33,6 +34,38 @@ function ReportEditorContent() { const [savedReports, setSavedReports] = useState([]); const [loadingReports, setLoadingReports] = useState(false); + // Auto-load report and vehicle data passed via router state + const location = useLocation(); + const { setData } = useReportData(); + + useEffect(() => { + const { reportName, vehicleData } = location.state || {}; + if (!reportName) return; + + api.get('/api/reports/').then(res => { + const reports = res.data.results || res.data; + const found = reports.find(r => r.name === reportName); + if (found) { + setReport({ + name: found.name, + pageWidth: found.page_width, + pageHeight: found.page_height, + apiEndpoint: found.api_endpoint || '', + elements: found.elements || [], + }); + setReportId(found.id); + } else { + // Report doesn't exist yet — pre-fill the name so Save creates it + setReport(prev => ({ ...prev, name: reportName })); + } + if (vehicleData) { + setData(vehicleData); + } + }).catch(err => { + console.error('Failed to auto-load report:', err); + }); + }, []); + const handleAddElement = (element) => { setReport(prev => { // Check if the new element is positioned inside a band diff --git a/frontend/src/components/ReportEditor/models/Element.jsx b/frontend/src/components/ReportEditor/models/Element.jsx index 9661345..260eccf 100644 --- a/frontend/src/components/ReportEditor/models/Element.jsx +++ b/frontend/src/components/ReportEditor/models/Element.jsx @@ -113,12 +113,14 @@ export class Element { * Database-bound text field element */ export class DBTextElement extends Element { - constructor({ id, x, y, width = 10, height = 1, objectKey = '', fieldPath = '' }) { + constructor({ id, x, y, width = 10, height = 1, objectKey = '', fieldPath = '', dataType = 'general', alignment = 'left' }) { super({ id, type: 'dbtext', x, y }); this.width = width; this.height = height; this.objectKey = objectKey; this.fieldPath = fieldPath; + this.dataType = dataType; + this.alignment = alignment; } getBounds() { @@ -148,7 +150,9 @@ export class DBTextElement extends Element { width: this.width, height: this.height, objectKey: this.objectKey, - fieldPath: this.fieldPath + fieldPath: this.fieldPath, + dataType: this.dataType, + alignment: this.alignment }; } } diff --git a/frontend/src/components/ReportPreviewModal.css b/frontend/src/components/ReportPreviewModal.css new file mode 100644 index 0000000..585e6d0 --- /dev/null +++ b/frontend/src/components/ReportPreviewModal.css @@ -0,0 +1,62 @@ +.report-preview-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.report-preview-modal { + background: #fff; + border-radius: 4px; + display: flex; + flex-direction: column; + max-width: 95vw; + max-height: 95vh; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.report-preview-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px solid #ddd; + font-weight: 600; + font-size: 14px; + flex-shrink: 0; +} + +.report-preview-close { + background: none; + border: none; + font-size: 20px; + cursor: pointer; + line-height: 1; + padding: 0 4px; + color: #555; +} + +.report-preview-close:hover { + color: #000; +} + +.report-preview-body { + overflow: auto; + flex: 1; + padding: 8px; +} + +.report-preview-error { + padding: 24px; + color: #c00; + font-size: 14px; +} + +.report-preview-loading { + padding: 24px; + color: #666; + font-size: 14px; +} diff --git a/frontend/src/components/ReportPreviewModal.jsx b/frontend/src/components/ReportPreviewModal.jsx new file mode 100644 index 0000000..e3cd03a --- /dev/null +++ b/frontend/src/components/ReportPreviewModal.jsx @@ -0,0 +1,73 @@ +import React, { useEffect, useState } from 'react'; +import { DataProvider, useReportData } from './ReportEditor/DataContext'; +import { BandProvider } from './ReportEditor/BandContext'; +import EditorCanvas from './ReportEditor/EditorCanvas'; +import api from '../services/api'; +import './ReportPreviewModal.css'; + +function PreviewContent({ vehicleData, elements }) { + const { setData } = useReportData(); + + useEffect(() => { + setData(vehicleData); + }, [vehicleData]); + + return ( + + {}} + onSelectMultiple={() => {}} + onDeselectAll={() => {}} + onElementUpdate={() => {}} + onElementDelete={() => {}} + onAddElement={() => {}} + /> + + ); +} + +export default function ReportPreviewModal({ vehicleData, onClose }) { + const [elements, setElements] = useState(null); + const [error, setError] = useState(''); + + useEffect(() => { + api.get('/api/reports/') + .then(res => { + const reports = res.data.results || res.data; + const report = reports.find(r => r.name === 'notice_report'); + if (report) { + setElements(report.elements || []); + } else { + setError('Report "notice_report" not found. Create it in the Report Editor first.'); + } + }) + .catch(() => setError('Failed to load report')); + }, []); + + return ( +
+
e.stopPropagation()}> +
+ Notice Report — {vehicleData.vehicle.vehicle_number} + +
+
+ {error ? ( +
{error}
+ ) : elements === null ? ( +
Loading...
+ ) : ( + + + + )} +
+
+
+ ); +} diff --git a/frontend/src/hooks/useSerialData.js b/frontend/src/hooks/useSerialData.js index 3231269..2b0003b 100644 --- a/frontend/src/hooks/useSerialData.js +++ b/frontend/src/hooks/useSerialData.js @@ -6,7 +6,8 @@ function useSerialData() { const [error, setError] = useState(null); useEffect(() => { - const eventSource = new EventSource('http://localhost:5000/events'); + const serialUrl = process.env.REACT_APP_SERIAL_URL || 'http://localhost:5000'; + const eventSource = new EventSource(`${serialUrl}/events`); eventSource.onopen = () => { setIsConnected(true); diff --git a/run_server.bat b/run_server.bat new file mode 100644 index 0000000..4a4f1f1 --- /dev/null +++ b/run_server.bat @@ -0,0 +1,2 @@ +cd .\backend\ +uvicorn scalesapp.asgi:application --host 127.0.0.1 --port 8000 --reload \ No newline at end of file diff --git a/serial_bridge/Dockerfile b/serial_bridge/Dockerfile new file mode 100644 index 0000000..e7c1f7f --- /dev/null +++ b/serial_bridge/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y --no-install-recommends socat && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install serial_bridge dependencies (server-only, no pystray/PyInstaller) +COPY serial_bridge/requirements.server.txt serial_bridge/requirements.server.txt +RUN pip install --no-cache-dir -r serial_bridge/requirements.server.txt + +# Install test_writer dependencies +COPY test_comport_writer/requirements.txt test_comport_writer/requirements.txt +RUN pip install --no-cache-dir -r test_comport_writer/requirements.txt + +# Copy application code +COPY serial_bridge/ serial_bridge/ +COPY test_comport_writer/ test_comport_writer/ + +COPY serial_bridge/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 5000 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/serial_bridge/app.py b/serial_bridge/app.py index f549251..b954b5e 100644 --- a/serial_bridge/app.py +++ b/serial_bridge/app.py @@ -214,7 +214,8 @@ def start_app(): # Start Flask app debug = os.getenv('DEBUG', 'False').lower() == 'true' - app.run(host='127.0.0.1', port=5000, debug=debug, use_reloader=False) + host = os.getenv('HOST', '127.0.0.1') + app.run(host=host, port=5000, debug=debug, use_reloader=False) def stop_app(): diff --git a/serial_bridge/entrypoint.sh b/serial_bridge/entrypoint.sh new file mode 100644 index 0000000..89e9996 --- /dev/null +++ b/serial_bridge/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +echo "[socat] Creating virtual serial port pair..." +socat PTY,raw,echo=0,link=/tmp/vcom_read PTY,raw,echo=0,link=/tmp/vcom_write & + +# Wait until both PTY symlinks exist +for i in $(seq 1 20); do + if [ -e /tmp/vcom_read ] && [ -e /tmp/vcom_write ]; then + echo "[socat] Virtual ports ready: /tmp/vcom_read <-> /tmp/vcom_write" + break + fi + sleep 0.5 +done + +echo "[test_writer] Starting test COM port writer..." +cd /app/test_comport_writer +COM_PORT=/tmp/vcom_write python test_writer.py & + +echo "[serial_bridge] Starting Flask SSE server..." +cd /app/serial_bridge +exec python app.py diff --git a/serial_bridge/requirements.server.txt b/serial_bridge/requirements.server.txt new file mode 100644 index 0000000..76ef17d --- /dev/null +++ b/serial_bridge/requirements.server.txt @@ -0,0 +1,4 @@ +pyserial==3.5 +python-dotenv==1.0.0 +flask==3.1.0 +flask-cors==5.0.0 diff --git a/stop.bat b/stop.bat new file mode 100644 index 0000000..d4880e7 --- /dev/null +++ b/stop.bat @@ -0,0 +1,4 @@ +@echo off +echo Stopping Python services... +taskkill /F /IM python.exe /T 2>nul && echo Python processes stopped. || echo No Python processes found. +echo Done.