added docker-compose for linux

master
kikimor 2 weeks ago
parent be91231b05
commit 58b1ae9a5e

@ -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.

@ -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<example>\\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<commentary>\\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</commentary>\\n</example>\\n\\n<example>\\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<commentary>\\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</commentary>\\n</example>\\n\\n<example>\\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<commentary>\\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</commentary>\\n</example>\\n\\n<example>\\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<commentary>\\nRefactoring a complex React context is a core task for the react-expert-dev agent.\\n</commentary>\\n</example>"
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.

@ -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

@ -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"]

@ -8,3 +8,4 @@ sqlparse==0.5.3
psycopg==3.3.2 psycopg==3.3.2
psycopg-binary==3.3.2 psycopg-binary==3.3.2
uvicorn[standard]==0.34.0 uvicorn[standard]==0.34.0
whitenoise==6.9.0

@ -36,10 +36,11 @@ INSTALLED_APPS = [
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'corsheaders.middleware.CorsMiddleware', 'corsheaders.middleware.CorsMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
@ -92,6 +93,11 @@ USE_TZ = True
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles' STATIC_ROOT = BASE_DIR / 'staticfiles'
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
@ -99,12 +105,8 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'api.User' AUTH_USER_MODEL = 'api.User'
# CORS Configuration # CORS Configuration
CORS_ALLOWED_ORIGINS = [ _cors_default = 'http://localhost:3000,http://127.0.0.1:3000,http://localhost:5174,http://127.0.0.1:5174'
'http://localhost:3000', CORS_ALLOWED_ORIGINS = os.getenv('CORS_ALLOWED_ORIGINS', _cors_default).split(',')
'http://127.0.0.1:3000',
'http://localhost:5174',
'http://127.0.0.1:5174',
]
CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [ CORS_ALLOW_HEADERS = [
'accept', 'accept',

@ -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:

@ -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

@ -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";
}
}

@ -1,13 +1,16 @@
import { useState, useEffect, useMemo } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import Header from './Header'; import Header from './Header';
import useSerialData from '../hooks/useSerialData'; import useSerialData from '../hooks/useSerialData';
import { useNomenclatures } from '../contexts/NomenclatureContext'; import { useNomenclatures } from '../contexts/NomenclatureContext';
import { useNomenclatureData } from '../contexts/NomenclatureDataContext'; import { useNomenclatureData } from '../contexts/NomenclatureDataContext';
import NomenclatureDropdown from './NomenclatureUI/NomenclatureDropdown'; import NomenclatureDropdown from './NomenclatureUI/NomenclatureDropdown';
import ReportPreviewModal from './ReportPreviewModal';
import api from '../services/api'; import api from '../services/api';
import './Main.css'; import './Main.css';
export default function Main() { export default function Main() {
const navigate = useNavigate();
const { readings, isConnected } = useSerialData(); const { readings, isConnected } = useSerialData();
const { vehicles } = useNomenclatures(); const { vehicles } = useNomenclatures();
@ -18,9 +21,10 @@ export default function Main() {
const [newExtraData, setNewExtraData] = useState({}); const [newExtraData, setNewExtraData] = useState({});
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [showNoticeReport, setShowNoticeReport] = useState(false);
// Nomenclature data from context // Nomenclature data from context
const { definitions } = useNomenclatureData(); const { definitions, entries } = useNomenclatureData();
const vehicleNomenclatures = useMemo( const vehicleNomenclatures = useMemo(
() => Object.values(definitions) () => Object.values(definitions)
.filter(d => d.applies_to === 'vehicle') .filter(d => d.applies_to === 'vehicle')
@ -164,6 +168,31 @@ export default function Main() {
? selectedVehicle.gross - selectedVehicle.tare ? selectedVehicle.gross - selectedVehicle.tare
: null; : 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 ( return (
<div> <div>
<Header /> <Header />
@ -363,6 +392,23 @@ export default function Main() {
<div className="weight-card weight-card--net"> <div className="weight-card weight-card--net">
<div className="weight-card-header">Net</div> <div className="weight-card-header">Net</div>
<div className="weight-card-value">{netWeight} kg</div> <div className="weight-card-value">{netWeight} kg</div>
<button
className="weight-set-btn"
onClick={() => setShowNoticeReport(true)}
>
Print Notice
</button>
<button
className="weight-set-btn"
onClick={() => navigate('/report-editor', {
state: {
reportName: 'notice_report',
vehicleData: vehicleReportData,
}
})}
>
Design Notice
</button>
</div> </div>
)} )}
</div> </div>
@ -398,6 +444,12 @@ export default function Main() {
)} )}
</div> </div>
</div> </div>
{showNoticeReport && vehicleReportData && (
<ReportPreviewModal
vehicleData={vehicleReportData}
onClose={() => setShowNoticeReport(false)}
/>
)}
</div> </div>
); );
} }

@ -8,6 +8,30 @@ import { useElementSelection } from './hooks/useElementSelection';
import { getElementStyle, isTextMultiLine, getTextWhiteSpace } from './utils/elementUtils'; import { getElementStyle, isTextMultiLine, getTextWhiteSpace } from './utils/elementUtils';
import './elements.css'; 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({ function DBTextField({
element, element,
isSelected, isSelected,
@ -51,12 +75,16 @@ function DBTextField({
}; };
// Resolve data value // Resolve data value
const displayContent = previewMode const rawValue = previewMode
? resolveDBTextValue(reportData, element.objectKey, element.fieldPath, parentBandId, { currentBandData }) ? resolveDBTextValue(reportData, element.objectKey, element.fieldPath, parentBandId, { currentBandData })
: parentBandId : parentBandId
? `{${element.fieldPath}}` // Inside band: show field only ? `{${element.fieldPath}}` // Inside band: show field only
: `{${element.objectKey}.${element.fieldPath}}`; // Outside band: show object.field : `{${element.objectKey}.${element.fieldPath}}`; // Outside band: show object.field
const displayContent = previewMode
? formatValue(rawValue, element.dataType || 'general')
: rawValue;
// Check if data is resolved // Check if data is resolved
// If inside a band, only fieldPath is required; otherwise both objectKey and fieldPath are required // If inside a band, only fieldPath is required; otherwise both objectKey and fieldPath are required
const isUnresolved = parentBandId const isUnresolved = parentBandId
@ -71,7 +99,8 @@ function DBTextField({
...getElementStyle(element, charWidth, charHeight), ...getElementStyle(element, charWidth, charHeight),
minWidth: `${charWidth}px`, minWidth: `${charWidth}px`,
minHeight: `${charHeight}px`, minHeight: `${charHeight}px`,
whiteSpace whiteSpace,
textAlign: element.alignment || 'left',
}; };
return ( return (

@ -237,6 +237,32 @@ function ObjectInspector({ element, onUpdate, allElements = [] }) {
</div> </div>
</> </>
)} )}
<div className="property-row">
<label>Data Type:</label>
<select
value={element.dataType || 'general'}
onChange={(e) => handleChange('dataType', e.target.value)}
>
<option value="general">General</option>
<option value="number">Number</option>
<option value="date">Date</option>
<option value="time">Time</option>
<option value="datetime">Date &amp; Time</option>
</select>
</div>
<div className="property-row">
<label>Alignment:</label>
<select
value={element.alignment || 'left'}
onChange={(e) => handleChange('alignment', e.target.value)}
>
<option value="left">Left</option>
<option value="center">Center</option>
<option value="right">Right</option>
</select>
</div>
</div> </div>
)} )}

@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react'; 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 { BandProvider } from './BandContext';
import Toolbar from './Toolbar'; import Toolbar from './Toolbar';
import EditorCanvas from './EditorCanvas'; import EditorCanvas from './EditorCanvas';
@ -33,6 +34,38 @@ function ReportEditorContent() {
const [savedReports, setSavedReports] = useState([]); const [savedReports, setSavedReports] = useState([]);
const [loadingReports, setLoadingReports] = useState(false); 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) => { const handleAddElement = (element) => {
setReport(prev => { setReport(prev => {
// Check if the new element is positioned inside a band // Check if the new element is positioned inside a band

@ -113,12 +113,14 @@ export class Element {
* Database-bound text field element * Database-bound text field element
*/ */
export class DBTextElement extends 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 }); super({ id, type: 'dbtext', x, y });
this.width = width; this.width = width;
this.height = height; this.height = height;
this.objectKey = objectKey; this.objectKey = objectKey;
this.fieldPath = fieldPath; this.fieldPath = fieldPath;
this.dataType = dataType;
this.alignment = alignment;
} }
getBounds() { getBounds() {
@ -148,7 +150,9 @@ export class DBTextElement extends Element {
width: this.width, width: this.width,
height: this.height, height: this.height,
objectKey: this.objectKey, objectKey: this.objectKey,
fieldPath: this.fieldPath fieldPath: this.fieldPath,
dataType: this.dataType,
alignment: this.alignment
}; };
} }
} }

@ -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;
}

@ -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 (
<BandProvider>
<EditorCanvas
elements={elements}
selectedElementIds={[]}
previewMode={true}
toolMode="select"
borderStyle="single"
onElementSelect={() => {}}
onSelectMultiple={() => {}}
onDeselectAll={() => {}}
onElementUpdate={() => {}}
onElementDelete={() => {}}
onAddElement={() => {}}
/>
</BandProvider>
);
}
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 (
<div className="report-preview-overlay" onClick={onClose}>
<div className="report-preview-modal" onClick={e => e.stopPropagation()}>
<div className="report-preview-header">
<span>Notice Report {vehicleData.vehicle.vehicle_number}</span>
<button className="report-preview-close" onClick={onClose}>×</button>
</div>
<div className="report-preview-body">
{error ? (
<div className="report-preview-error">{error}</div>
) : elements === null ? (
<div className="report-preview-loading">Loading...</div>
) : (
<DataProvider>
<PreviewContent vehicleData={vehicleData} elements={elements} />
</DataProvider>
)}
</div>
</div>
</div>
);
}

@ -6,7 +6,8 @@ function useSerialData() {
const [error, setError] = useState(null); const [error, setError] = useState(null);
useEffect(() => { 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 = () => { eventSource.onopen = () => {
setIsConnected(true); setIsConnected(true);

@ -0,0 +1,2 @@
cd .\backend\
uvicorn scalesapp.asgi:application --host 127.0.0.1 --port 8000 --reload

@ -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"]

@ -214,7 +214,8 @@ def start_app():
# Start Flask app # Start Flask app
debug = os.getenv('DEBUG', 'False').lower() == 'true' 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(): def stop_app():

@ -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

@ -0,0 +1,4 @@
pyserial==3.5
python-dotenv==1.0.0
flask==3.1.0
flask-cors==5.0.0

@ -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.
Loading…
Cancel
Save