Compare commits
139 Commits
943e449c97
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e917b2965c | |||
| f6b9af2f38 | |||
| 9e513c2f5e | |||
| 2a87a18edd | |||
| 451d04cf39 | |||
| e492ea9a2e | |||
| 27d622bfe6 | |||
| ca1eed5061 | |||
| 7713eb2fa7 | |||
| 26c217e0eb | |||
| e6e9e11b18 | |||
| b04d157915 | |||
| d9f992ca9a | |||
| 0fb507f247 | |||
| a240bce8fc | |||
| 69bcc3e342 | |||
| 30d27d6b8a | |||
| 9dd49c5014 | |||
| 8c9da36312 | |||
| 6ddb58dc7c | |||
| e079758f1d | |||
| 026323b72a | |||
| 67a7590525 | |||
| c3520b5ad4 | |||
| 2d94a83e73 | |||
| 47c89c9915 | |||
| f9bf3156eb | |||
| 0ebb39150f | |||
| 67c3170067 | |||
| 7bbb5a63b5 | |||
| 505339aa33 | |||
| 740e8e5897 | |||
| e491a6c67f | |||
| ff179430aa | |||
| 6c38101e5c | |||
| a1a2c33427 | |||
| 996f0d774b | |||
| 6cd5b80adb | |||
| 8b8f8b7b6f | |||
| 33190bd8e0 | |||
| 4d2942a8e5 | |||
| 187d0283a5 | |||
| 0ce1db5471 | |||
| c720f611ea | |||
| e658f2e6f1 | |||
| a5cd49284a | |||
| 3f242f9a6d | |||
| f6ccdd859f | |||
| d5f903db98 | |||
| 24a96c3da1 | |||
| a31aff7c35 | |||
| f42132ed5b | |||
| e6961fc593 | |||
| 50816a6844 | |||
| bb7a4c1ff2 | |||
| 3d9b124766 | |||
| 0da4bbf4cf | |||
| 4492d7aa1c | |||
| 4471e344eb | |||
| 621ced0e43 | |||
| a4d16cdbae | |||
| 6ff58ed013 | |||
| 37f931fa0b | |||
| 8184fbd7e9 | |||
| 46b9be4791 | |||
| fb6b371fb7 | |||
| 0784c1a032 | |||
| 44ea3cdd7e | |||
| 320a646950 | |||
| acb1e56506 | |||
| 2a230a6994 | |||
| 43c91881a7 | |||
| 98ee8a3ad6 | |||
| 2dda34d4d2 | |||
| d135aa643a | |||
| d645d3ad9d | |||
| 08d14bf9e6 | |||
| acca5dcbae | |||
| cd84e25890 | |||
| f19c157e8f | |||
| 8e0166c68a | |||
| 3e0af925d5 | |||
| 2281df3716 | |||
| 68476142c1 | |||
| a635f1002a | |||
| d05b7da8bc | |||
| a4f65c6065 | |||
| d75fd00666 | |||
| 9b4d1f94bf | |||
| d038d30831 | |||
| 1db30c9b6f | |||
| 9b468d9a13 | |||
| 3ad6cfee50 | |||
| f132983b75 | |||
| 573c12cdc3 | |||
| c63afad672 | |||
| 56050a896b | |||
| 06056c6182 | |||
| afbc5b91b2 | |||
| 06492ff099 | |||
| 1d2c3c9032 | |||
| d4a7983afb | |||
| 8e6e0e96b8 | |||
| 84ccabe2fe | |||
| edf9c74e75 | |||
| ca8987d9e4 | |||
| 1709bb1dad | |||
| 65137b41fb | |||
| b342de906e | |||
| 7aa93ff5d0 | |||
| ae6d7aad1a | |||
| 14a1107466 | |||
| 8e276a34fe | |||
| da193b26ef | |||
| 9f3f5c1cef | |||
| 3ff27701fc | |||
| d2f651fa2c | |||
| b121000bf7 | |||
| b09ea28ff0 | |||
| 4d5c55f459 | |||
| 000a28bea4 | |||
| 97e7b09bcd | |||
| 1966a92a87 | |||
| 8354abbc8f | |||
| 853e853e5e | |||
| bd78b1de81 | |||
| e3bbd7d99e | |||
| a5273158e2 | |||
| fc366547c7 | |||
| 84dbe8490d | |||
| 5019bf6acf | |||
| 148fee8859 | |||
| 5aadc34d4a | |||
| 0873fa42bb | |||
| 73309cb110 | |||
| d92272e554 | |||
| f7446cc2df | |||
| 17893824d5 | |||
| 4affb03504 |
@@ -19,3 +19,7 @@ SEED_USER2_PASSWORD=Test-Anv2-FBG
|
|||||||
AUTH_SECRET=WheqAss4F/al9yRZRqepJEBs6TzPsN3brX0iBiF4Oww=
|
AUTH_SECRET=WheqAss4F/al9yRZRqepJEBs6TzPsN3brX0iBiF4Oww=
|
||||||
JWT_SECRET=uK9yRQpyyWOcHYcYbpAdsJ7NJcEsyCYZcgF82OnBz2k=
|
JWT_SECRET=uK9yRQpyyWOcHYcYbpAdsJ7NJcEsyCYZcgF82OnBz2k=
|
||||||
MISTRAL_API_KEY=JGPjLuNnzaLSYMxKbexLZohUOegrSLye
|
MISTRAL_API_KEY=JGPjLuNnzaLSYMxKbexLZohUOegrSLye
|
||||||
|
FLYER_AI_TIMEOUT_MS=60000
|
||||||
|
FLYER_AI_RETRIES=2
|
||||||
|
FLYER_AI_DEBUG=1
|
||||||
|
FLYER_AI_DEBUG_DIR=/app/debug
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ JWT_SECRET=byt-ut-mig
|
|||||||
# Mistral AI
|
# Mistral AI
|
||||||
# Hämtas från: https://console.mistral.ai/
|
# Hämtas från: https://console.mistral.ai/
|
||||||
MISTRAL_API_KEY=
|
MISTRAL_API_KEY=
|
||||||
|
FLYER_AI_TIMEOUT_MS=45000
|
||||||
|
FLYER_AI_RETRIES=2
|
||||||
|
FLYER_AI_DEBUG=0
|
||||||
|
# Linux-container: /app/debug, lokalt: ./debug
|
||||||
|
FLYER_AI_DEBUG_DIR=/app/debug
|
||||||
|
|
||||||
# Publik URL (används av frontend)
|
# Publik URL (används av frontend)
|
||||||
NEXT_PUBLIC_APP_URL=https://recept.gynther.se
|
NEXT_PUBLIC_APP_URL=https://recept.gynther.se
|
||||||
@@ -26,6 +31,13 @@ NEXT_PUBLIC_API_URL=https://recept.gynther.se
|
|||||||
# CORS — tillåtna origins för backend-API (normalt samma som APP_URL)
|
# CORS — tillåtna origins för backend-API (normalt samma som APP_URL)
|
||||||
ALLOWED_ORIGIN=https://recept.gynther.se
|
ALLOWED_ORIGIN=https://recept.gynther.se
|
||||||
|
|
||||||
|
# Importer integration
|
||||||
|
IMPORTER_SERVICE_URL=http://importer-api:3001
|
||||||
|
RECEIPT_TRACE_DECISIONS=0
|
||||||
|
|
||||||
|
# Optional webhook hardening
|
||||||
|
GITEA_WEBHOOK_SECRET=
|
||||||
|
|
||||||
# Bootstrap-användare (skapas/uppdateras vid appstart)
|
# Bootstrap-användare (skapas/uppdateras vid appstart)
|
||||||
ADMIN_NADMIN_PASSWORD=byt-ut-mig
|
ADMIN_NADMIN_PASSWORD=byt-ut-mig
|
||||||
ADMIN_PADMIN_PASSWORD=byt-ut-mig
|
ADMIN_PADMIN_PASSWORD=byt-ut-mig
|
||||||
|
|||||||
@@ -0,0 +1,254 @@
|
|||||||
|
name: Test Suite
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend-pr-quick:
|
||||||
|
if: gitea.event_name == 'pull_request'
|
||||||
|
runs-on: backend-node24
|
||||||
|
env:
|
||||||
|
DATABASE_URL: mysql://ci:ci@127.0.0.1:3306/recipe_app
|
||||||
|
JWT_SECRET: ci-test-secret
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Verify Node toolchain on runner
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Ensure workspace checkout
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ws="${GITHUB_WORKSPACE:-$PWD}"
|
||||||
|
if [ -d "$ws/backend" ]; then
|
||||||
|
echo "Workspace already prepared at $ws"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$ws"
|
||||||
|
repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||||
|
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||||
|
repo_url="$(echo "$repo_url" | sed "s#https://#https://oauth2:${GITHUB_TOKEN}@#")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git clone --depth 1 "$repo_url" "$ws"
|
||||||
|
if [ -n "${GITHUB_SHA:-}" ]; then
|
||||||
|
git -C "$ws" fetch --depth 1 origin "$GITHUB_SHA" || true
|
||||||
|
git -C "$ws" checkout -f "$GITHUB_SHA" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install dependencies (backend)
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Prisma schema validate
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run prisma:validate
|
||||||
|
|
||||||
|
- name: Generate Prisma Client
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run prisma:generate
|
||||||
|
|
||||||
|
- name: Verify generated Prisma client is typed
|
||||||
|
working-directory: ./backend
|
||||||
|
run: |
|
||||||
|
if ! grep -q "export \* from '.prisma/client/default'" node_modules/@prisma/client/index.d.ts; then
|
||||||
|
echo "Prisma client export is unexpected"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run receipt-import focused tests (PR quick)
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npx jest src/receipt-import/receipt-import.service.spec.ts src/receipt-import/receipt-import.parse-flow.spec.ts src/receipt-import/receipt-import.save.spec.ts --no-coverage
|
||||||
|
|
||||||
|
- name: Typecheck backend
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run typecheck
|
||||||
|
|
||||||
|
- name: Build NestJS app
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
quick-import-pr-quick:
|
||||||
|
if: gitea.event_name == 'pull_request'
|
||||||
|
runs-on: backend-node24
|
||||||
|
env:
|
||||||
|
DATABASE_URL: mysql://ci:ci@127.0.0.1:3306/recipe_app
|
||||||
|
JWT_SECRET: ci-test-secret
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Verify Node toolchain on runner
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Ensure workspace checkout
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ws="${GITHUB_WORKSPACE:-$PWD}"
|
||||||
|
if [ -d "$ws/backend" ]; then
|
||||||
|
echo "Workspace already prepared at $ws"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$ws"
|
||||||
|
repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||||
|
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||||
|
repo_url="$(echo "$repo_url" | sed "s#https://#https://oauth2:${GITHUB_TOKEN}@#")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git clone --depth 1 "$repo_url" "$ws"
|
||||||
|
if [ -n "${GITHUB_SHA:-}" ]; then
|
||||||
|
git -C "$ws" fetch --depth 1 origin "$GITHUB_SHA" || true
|
||||||
|
git -C "$ws" checkout -f "$GITHUB_SHA" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install dependencies (backend)
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run quick-import focused tests (PR quick)
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npx jest src/quick-import/quick-import.service.spec.ts --no-coverage
|
||||||
|
|
||||||
|
backend-full:
|
||||||
|
if: gitea.event_name == 'push'
|
||||||
|
runs-on: backend-node24
|
||||||
|
env:
|
||||||
|
DATABASE_URL: mysql://ci:ci@127.0.0.1:3306/recipe_app
|
||||||
|
JWT_SECRET: ci-test-secret
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Verify Node toolchain on runner
|
||||||
|
run: |
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Ensure workspace checkout
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ws="${GITHUB_WORKSPACE:-$PWD}"
|
||||||
|
if [ -d "$ws/backend" ]; then
|
||||||
|
echo "Workspace already prepared at $ws"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$ws"
|
||||||
|
repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||||
|
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||||
|
repo_url="$(echo "$repo_url" | sed "s#https://#https://oauth2:${GITHUB_TOKEN}@#")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git clone --depth 1 "$repo_url" "$ws"
|
||||||
|
if [ -n "${GITHUB_SHA:-}" ]; then
|
||||||
|
git -C "$ws" fetch --depth 1 origin "$GITHUB_SHA" || true
|
||||||
|
git -C "$ws" checkout -f "$GITHUB_SHA" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install dependencies (backend)
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Prisma schema validate
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run prisma:validate
|
||||||
|
|
||||||
|
- name: Generate Prisma Client
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run prisma:generate
|
||||||
|
|
||||||
|
- name: Verify generated Prisma client is typed
|
||||||
|
working-directory: ./backend
|
||||||
|
run: |
|
||||||
|
if ! grep -q "export \* from '.prisma/client/default'" node_modules/@prisma/client/index.d.ts; then
|
||||||
|
echo "Prisma client export is unexpected"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Dependency audit (high+critical)
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run audit:high
|
||||||
|
|
||||||
|
- name: Typecheck backend
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run typecheck
|
||||||
|
|
||||||
|
- name: Run tests (backend)
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Build NestJS app
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
flutter-quality:
|
||||||
|
runs-on: flutter-3-41
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Verify Flutter toolchain on runner
|
||||||
|
run: flutter --version
|
||||||
|
|
||||||
|
- name: Ensure workspace checkout
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
ws="${GITHUB_WORKSPACE:-$PWD}"
|
||||||
|
mkdir -p "$ws"
|
||||||
|
repo_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"
|
||||||
|
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||||
|
repo_url="$(echo "$repo_url" | sed "s#https://#https://oauth2:${GITHUB_TOKEN}@#")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$ws/.git" ]; then
|
||||||
|
git -C "$ws" init
|
||||||
|
git -C "$ws" remote add origin "$repo_url"
|
||||||
|
else
|
||||||
|
git -C "$ws" remote set-url origin "$repo_url"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ref="${GITHUB_SHA:-${GITHUB_REF_NAME:-main}}"
|
||||||
|
git -C "$ws" fetch --depth 1 origin "$ref"
|
||||||
|
git -C "$ws" checkout -f FETCH_HEAD
|
||||||
|
|
||||||
|
if [ ! -d "$ws/flutter" ]; then
|
||||||
|
echo "Flutter directory missing after checkout: $ws/flutter"
|
||||||
|
ls -la "$ws"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install dependencies (flutter)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cd "${GITHUB_WORKSPACE:-$PWD}/flutter"
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
- name: Analyze Flutter code
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cd "${GITHUB_WORKSPACE:-$PWD}/flutter"
|
||||||
|
flutter analyze
|
||||||
|
|
||||||
|
- name: Set Flutter test mode
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ gitea.event_name }}" = "pull_request" ]; then
|
||||||
|
echo "FLUTTER_TEST_CMD=flutter test --reporter=compact" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "FLUTTER_TEST_CMD=flutter test" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run Flutter tests
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
cd "${GITHUB_WORKSPACE:-$PWD}/flutter"
|
||||||
|
${FLUTTER_TEST_CMD}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Copilot Instructions
|
||||||
|
|
||||||
|
## Database Command Style
|
||||||
|
|
||||||
|
When suggesting database commands in this repository, always use credentials loaded from `.env` inline in the command.
|
||||||
|
|
||||||
|
Required pattern:
|
||||||
|
- Read `MARIADB_ROOT_PASSWORD` from `.env` with `grep` and sanitize quotes/CRLF.
|
||||||
|
- Read `MARIADB_DATABASE` from `.env` with `grep` and sanitize quotes/CRLF.
|
||||||
|
- Pass both values directly to `mariadb` command invocations.
|
||||||
|
|
||||||
|
Preferred one-liner style:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -i recipe-db mariadb -uroot -p"$(grep -E '^[[:space:]]*MARIADB_ROOT_PASSWORD[[:space:]]*=' .env | tail -n1 | sed -E 's/^[^=]*=[[:space:]]*//; s/[[:space:]]+$//; s/^["'\''']|["'\''']$//g' | tr -d '\r')" "$(grep -E '^[[:space:]]*MARIADB_DATABASE[[:space:]]*=' .env | tail -n1 | sed -E 's/^[^=]*=[[:space:]]*//; s/[[:space:]]+$//; s/^["'\''']|["'\''']$//g' | tr -d '\r')" -e "SELECT 1;"
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not suggest hardcoded passwords for MariaDB commands.
|
||||||
+116
-3
@@ -7,7 +7,8 @@ on:
|
|||||||
branches: [ main, develop ]
|
branches: [ main, develop ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
backend-pr-quick:
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@@ -26,12 +27,78 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies (backend)
|
- name: Install dependencies (backend)
|
||||||
working-directory: ./backend
|
working-directory: ./backend
|
||||||
run: npm install
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Prisma schema validate
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npx prisma validate --schema prisma/schema.prisma
|
||||||
|
|
||||||
- name: Generate Prisma Client
|
- name: Generate Prisma Client
|
||||||
working-directory: ./backend
|
working-directory: ./backend
|
||||||
run: npm run prisma:generate
|
run: npm run prisma:generate
|
||||||
|
|
||||||
|
- name: Verify generated Prisma client is typed
|
||||||
|
working-directory: ./backend
|
||||||
|
run: |
|
||||||
|
if ! grep -q "export \* from '.prisma/client/default'" node_modules/@prisma/client/index.d.ts; then
|
||||||
|
echo "Prisma client export is unexpected";
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Lint backend
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Build NestJS app
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
backend-full:
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [24.15.0]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies (backend)
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Prisma schema validate
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npx prisma validate --schema prisma/schema.prisma
|
||||||
|
|
||||||
|
- name: Generate Prisma Client
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run prisma:generate
|
||||||
|
|
||||||
|
- name: Verify generated Prisma client is typed
|
||||||
|
working-directory: ./backend
|
||||||
|
run: |
|
||||||
|
if ! grep -q "export \* from '.prisma/client/default'" node_modules/@prisma/client/index.d.ts; then
|
||||||
|
echo "Prisma client export is unexpected";
|
||||||
|
exit 1;
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Lint backend
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Dependency audit (high+critical)
|
||||||
|
working-directory: ./backend
|
||||||
|
run: npm audit --audit-level=high
|
||||||
|
|
||||||
- name: Run tests (backend)
|
- name: Run tests (backend)
|
||||||
working-directory: ./backend
|
working-directory: ./backend
|
||||||
run: npm test
|
run: npm test
|
||||||
@@ -39,4 +106,50 @@ jobs:
|
|||||||
- name: Build NestJS app
|
- name: Build NestJS app
|
||||||
working-directory: ./backend
|
working-directory: ./backend
|
||||||
run: npm run build
|
run: npm run build
|
||||||
continue-on-error: true
|
|
||||||
|
flutter-quality:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect Flutter-related changes
|
||||||
|
id: filter
|
||||||
|
uses: dorny/paths-filter@v3
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
flutter:
|
||||||
|
- 'flutter/**'
|
||||||
|
- '.github/workflows/test.yml'
|
||||||
|
|
||||||
|
- name: Set Flutter test mode
|
||||||
|
if: steps.filter.outputs.flutter == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
echo "FLUTTER_TEST_CMD=flutter test --reporter=compact" >> "$GITHUB_ENV"
|
||||||
|
else
|
||||||
|
echo "FLUTTER_TEST_CMD=flutter test" >> "$GITHUB_ENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
if: steps.filter.outputs.flutter == 'true'
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: '3.41.9'
|
||||||
|
|
||||||
|
- name: Install dependencies (flutter)
|
||||||
|
if: steps.filter.outputs.flutter == 'true'
|
||||||
|
working-directory: ./flutter
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Analyze Flutter code
|
||||||
|
if: steps.filter.outputs.flutter == 'true'
|
||||||
|
working-directory: ./flutter
|
||||||
|
run: flutter analyze
|
||||||
|
|
||||||
|
- name: Run Flutter tests
|
||||||
|
if: steps.filter.outputs.flutter == 'true'
|
||||||
|
working-directory: ./flutter
|
||||||
|
run: ${{ env.FLUTTER_TEST_CMD }}
|
||||||
|
|||||||
@@ -6,8 +6,16 @@ node_modules/
|
|||||||
!package-lock.json
|
!package-lock.json
|
||||||
!**/package-lock.json
|
!**/package-lock.json
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Compiled backend artifacts (built in Docker)
|
||||||
|
backend/dist/
|
||||||
|
backend/tsconfig.tsbuildinfo
|
||||||
|
|
||||||
# Dart/Flutter generated files with absolute host paths — must not be committed
|
# Dart/Flutter generated files with absolute host paths — must not be committed
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
*/.dart_tool/
|
*/.dart_tool/
|
||||||
|
flutter/build/
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Dokumentationsagent
|
||||||
|
**Roll**: Hjälper till att uppdatera och förbättra dokumentation, men **modifierar aldrig filer utan godkännande**.
|
||||||
|
|
||||||
|
## Regler
|
||||||
|
- **Läs endast**: Använd `read` och `grep` för att analysera dokumentation.
|
||||||
|
- **Föreslå ändringar**: Använd `suggest` för att visa förändringar innan de appliceras.
|
||||||
|
- **Förbjudna åtgärder**:
|
||||||
|
- Modifiera `teknisk_beskrivning.md` eller `docs/` direkt.
|
||||||
|
- Köra `bash`-kommandon som påverkar filer (t.ex. `rm`, `echo >`).
|
||||||
|
|
||||||
|
## Arbetsflöde
|
||||||
|
1. Läs befintlig dokumentation med `read`.
|
||||||
|
2. Föreslå ändringar via `/local-review-uncommitted`.
|
||||||
|
3. Vänta på explicit godkännande innan modifieringar görs.
|
||||||
|
4. Om användaren ber om en ändring, visa **alltid** en diff först.
|
||||||
|
|
||||||
|
## Exempel på tillåtna åtgärder
|
||||||
|
- Söka efter föråldrad information i `teknisk_beskrivning.md`.
|
||||||
|
- Föreslå nya avsnitt eller korrigeringar.
|
||||||
|
- Sammanfatta befintligt innehåll.
|
||||||
|
- Lägga till referenser eller länkar till relevant information.
|
||||||
|
- lägga till information i `docs/` efter godkännande.
|
||||||
|
|
||||||
|
## Exempel på förbjudna åtgärder
|
||||||
|
- Redigera `teknisk_beskrivning.md` direkt.
|
||||||
|
- Köra `git add` eller `git commit` utan begäran.
|
||||||
|
- Ta bort eller flytta filer i `docs/`.
|
||||||
@@ -0,0 +1,575 @@
|
|||||||
|
Du är en senior utvecklare och säkerhetsexpert. Analysera commit-kandidater i detta fullstack-projekt (backend: NestJS + Prisma, frontend: Next.js/Flutter, databas: MariaDB).
|
||||||
|
|
||||||
|
Syfte:
|
||||||
|
- Detta är en pre-commit quality gate innan commit.
|
||||||
|
- Leverera ett tydligt gate-beslut: `PASS`, `PASS_WITH_WARNINGS` eller `BLOCK`.
|
||||||
|
- Vid `BLOCK`: lista exakta blockerare och fixordning.
|
||||||
|
|
||||||
|
---
|
||||||
|
## 0. Deterministiska gate-regler (källa till sanning)
|
||||||
|
|
||||||
|
### 0.1 Filurval (delta-first)
|
||||||
|
1. Primärt: analysera alla staged filer.
|
||||||
|
2. Om inga staged filer finns: analysera commit-kandidater i working tree (modified + untracked).
|
||||||
|
3. Exkludera alltid: `node_modules`, `.git`, build/cache-artifacts, binärfiler, genererade filer som inte ska committas.
|
||||||
|
4. Fokusera blockerande bedömning på förändrad kod (delta). Legacy-problem i opåverkade delar rapporteras som teknisk skuld (ej blockerande i denna gate).
|
||||||
|
|
||||||
|
### 0.2 Severity och beslut
|
||||||
|
- **Critical**: säkerhetshål/scope-brist med hög impact (t.ex. IDOR, auth bypass, PII-läckage, injection).
|
||||||
|
- **High**: allvarlig korrektness-/driftsrisk i produktion.
|
||||||
|
- **Medium/Low**: informativa förbättringar (blockerar inte).
|
||||||
|
|
||||||
|
**Beslutslogik (deterministisk):**
|
||||||
|
- `BLOCK` om minst 1 `Critical`.
|
||||||
|
- `BLOCK` om 2 eller fler `High`.
|
||||||
|
- `PASS_WITH_WARNINGS` om exakt 1 `High` utan `Critical`.
|
||||||
|
- `PASS` om inga `Critical`/`High`.
|
||||||
|
|
||||||
|
### 0.3 Evidenskrav för blockerande fynd
|
||||||
|
Varje `Critical`/`High` måste ha:
|
||||||
|
- `Evidence`: `code`, `test`, eller `runtime`.
|
||||||
|
- Fil + radreferens.
|
||||||
|
- Konkreta fixsteg.
|
||||||
|
|
||||||
|
Fynd med endast antagande märks `Needs verification` och får inte ensamt orsaka `BLOCK`, om inte risken är uppenbart kritisk.
|
||||||
|
|
||||||
|
### 0.4 Stop-early-regel (effektivitet)
|
||||||
|
- Vid första tydliga `Critical`: sätt preliminärt `BLOCK`, identifiera max 3 ytterligare blockerare, avsluta sedan djupanalys.
|
||||||
|
|
||||||
|
### 0.5 Rapportbudget
|
||||||
|
- Rapportera max 5 informativa fynd (`Medium/Low`), prioriterade efter högst nytta/lägst kostnad.
|
||||||
|
|
||||||
|
---
|
||||||
|
## 1. Analysfokus
|
||||||
|
|
||||||
|
### 1.1 Allmän kodkvalitet
|
||||||
|
- Läsbarhet/underhållbarhet: namngivning, modularisering, komplexitet.
|
||||||
|
- TypeScript/Flutter best practices.
|
||||||
|
- Kommentarer för icke-obvious logik.
|
||||||
|
|
||||||
|
### 1.2 Performance-optimeringar (informational)
|
||||||
|
- Algoritmisk effektivitet.
|
||||||
|
- Onödiga kopior/serialiseringar.
|
||||||
|
- Databasfrågor, N+1-risk, ineffektiva `include/select`.
|
||||||
|
|
||||||
|
### 1.3 Säkerhetsanalys
|
||||||
|
Fokusera på **NestJS/Prisma/Next.js-specifika säkerhetsrisker** med automatiserad detektion.
|
||||||
|
|
||||||
|
#### 1.3.1 Vanliga säkerhetsrisker (med exempel)
|
||||||
|
| Risk | Exempel i kod | PowerShell för att hitta | Åtgärd |
|
||||||
|
|-------------------------------|-------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
|
||||||
|
| **SQL-injection (Prisma)** | `prisma.$queryRaw\`SELECT * FROM users WHERE name = ${userInput}\`` | `Select-String -Path "src\*" -Pattern "\$queryRaw.*\`\$\{"` | Använd Prisma-parametrar: `prisma.$queryRaw\`SELECT * FROM users WHERE name = ${userInput}\`` |
|
||||||
|
| **XSS (Next.js)** | `<div dangerouslySetInnerHTML={{ __html: userInput }} />` | `Select-String -Path "src\*" -Pattern "dangerouslySetInnerHTML"` | Använd `DOMPurify` eller `react-dom/server`. |
|
||||||
|
| **Secrets i kod** | `const apiKey = "sk_123456789";` | `Select-String -Path "src\*" -Pattern "apiKey|password|secret" -CaseSensitive` | Flytta till `.env` och använd `@nestjs/config`. |
|
||||||
|
| **CSRF (NestJS)** | Saknad `@UseGuards(CsrfGuard)` i `main.ts` | `Select-String -Path "src\main.ts" -Pattern "UseGuards.*Csrf" -NotMatch` | Lägg till `app.use(csurf())` i `main.ts`. |
|
||||||
|
| **Insecure Deserialization** | `JSON.parse(userInput)` utan validering | `Select-String -Path "src\*" -Pattern "JSON\.parse\("` | Använd `zod` eller `class-validator`. |
|
||||||
|
|
||||||
|
#### 1.3.2 Automatiserad säkerhetssökning (PowerShell)
|
||||||
|
Kör dessa kommandon för att hitta säkerhetsproblem:
|
||||||
|
```powershell
|
||||||
|
# 1. Sök efter hårdkodade secrets
|
||||||
|
Select-String -Path "src\*" -Pattern "apiKey|password|secret" -CaseSensitive |
|
||||||
|
ForEach-Object {
|
||||||
|
Write-Warning "Potentiell secret läcka i $($_.Path):$($_.LineNumber) - $($_.Line)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Sök efter SQL-injection-risker i Prisma
|
||||||
|
Select-String -Path "src\*" -Pattern "\$queryRaw.*\`\$\{" |
|
||||||
|
ForEach-Object {
|
||||||
|
Write-Warning "Potentiell SQL-injection i $($_.Path):$($_.LineNumber)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Kör npm audit för sårbara beroenden
|
||||||
|
npm audit --json | Out-File "npm_audit.json"
|
||||||
|
if ((Get-Content "npm_audit.json" | ConvertFrom-Json).metadata.vulnerabilities.high) {
|
||||||
|
Write-Error "Kritiska sårbarheter hittade! Kör `npm audit fix`."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3.3 Verktygsintegration
|
||||||
|
- **`npm audit`**: Upptäcker sårbara `node_modules`.
|
||||||
|
```powershell
|
||||||
|
npm audit --json | ConvertFrom-Json | Select-Object -ExpandProperty metadata
|
||||||
|
```
|
||||||
|
- **`eslint-plugin-security`**: Installera och kör:
|
||||||
|
```powershell
|
||||||
|
npm install --save-dev eslint-plugin-security
|
||||||
|
npx eslint --plugin security src/
|
||||||
|
```
|
||||||
|
- **`trivy`** (för container-säkerhet, om relevant):
|
||||||
|
```powershell
|
||||||
|
trivy fs --security-checks vuln .
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3.4 Prisma/NestJS-specifika risker
|
||||||
|
- **Prisma-raw-queries**: Undvik dynamiska SQL-strängar.
|
||||||
|
```ts
|
||||||
|
// OSAKER:
|
||||||
|
prisma.$queryRaw\`SELECT * FROM users WHERE name = ${userInput}\`;
|
||||||
|
|
||||||
|
// SÄKER:
|
||||||
|
prisma.$queryRaw\`SELECT * FROM users WHERE name = ${Prisma.sql\`${userInput}\`}\`;
|
||||||
|
```
|
||||||
|
- **NestJS-guards**: Validera alltid `roles` i `@UseGuards()`:
|
||||||
|
```ts
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('admin')
|
||||||
|
@Get('admin')
|
||||||
|
adminOnly() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 Backend-specifik kontroll (NestJS + Prisma)
|
||||||
|
Kontrollera **NestJS/Prisma-specifika mönster** med automatiserade verktyg.
|
||||||
|
|
||||||
|
#### 1.4.1 Vanliga backend-risker (med exempel)
|
||||||
|
| Risk | Exempel i kod | PowerShell för att hitta | Åtgärd |
|
||||||
|
|-------------------------------|-------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
|
||||||
|
| **Saknad DTO-validering** | `@Post() create(@Body() data: any)` | `Select-String -Path "src\*" -Pattern "@Body\(\)\.*any"` | Använd `class-validator`: `@Body() data: CreateUserDto` |
|
||||||
|
| **IDOR (Insecure Direct Object Reference)** | `@Get(':id') findOne(@Param('id') id: string)` utan scope-validering | `Select-String -Path "src\*" -Pattern "@Get\('.*'\)\.*@Param.*id.*string" -NotMatch "UseGuards"` | Lägg till `@UseGuards(OwnershipGuard)`. |
|
||||||
|
| **Saknad transaktion (Prisma)** | `await prisma.user.create({ data }); await prisma.log.create({ data })` | `Select-String -Path "src\*" -Pattern "await prisma\..*\.create.*await prisma"` | Använd `prisma.$transaction`: `await prisma.$transaction([...])` |
|
||||||
|
| **N+1-problem (Prisma)** | `users.map(user => prisma.posts.findMany({ where: { userId: user.id } }))` | `Select-String -Path "src\*" -Pattern "\.map.*prisma\..*\.findMany"` | Använd `include`: `prisma.user.findMany({ include: { posts: true } })` |
|
||||||
|
| **Saknad rate limiting** | `@Post('login')` utan begränsning | `Select-String -Path "src\*" -Pattern "@Post.*login" -NotMatch "ThrottlerGuard"` | Lägg till `@UseGuards(ThrottlerGuard)`. |
|
||||||
|
|
||||||
|
#### 1.4.2 Automatiserad validering (PowerShell)
|
||||||
|
Kör dessa kommandon för att hitta backend-problem:
|
||||||
|
```powershell
|
||||||
|
# 1. Sök efter saknad DTO-validering
|
||||||
|
Select-String -Path "src\*" -Pattern "@Body\(\)\.*any" |
|
||||||
|
ForEach-Object {
|
||||||
|
Write-Warning "Saknad DTO-validering i $($_.Path):$($_.LineNumber)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Sök efter IDOR-risker
|
||||||
|
Select-String -Path "src\*" -Pattern "@Get\('.*'\)\.*@Param.*id.*string" -NotMatch "UseGuards" |
|
||||||
|
ForEach-Object {
|
||||||
|
Write-Warning "Potentiell IDOR-risk i $($_.Path):$($_.LineNumber)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Sök efter saknade Prisma-transaktioner
|
||||||
|
Select-String -Path "src\*" -Pattern "await prisma\..*\.create.*await prisma" |
|
||||||
|
ForEach-Object {
|
||||||
|
Write-Warning "Saknad transaktion i $($_.Path):$($_.LineNumber)"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.4.3 Rekommenderade bibliotek
|
||||||
|
- **DTO-validering**: [`class-validator`](https://github.com/typestack/class-validator)
|
||||||
|
```powershell
|
||||||
|
npm install class-validator class-transformer
|
||||||
|
```
|
||||||
|
- **Auktorisation**: [`@nestjs/passport`](https://docs.nestjs.com/security/authentication)
|
||||||
|
```powershell
|
||||||
|
npm install @nestjs/passport passport passport-jwt
|
||||||
|
```
|
||||||
|
- **Rate limiting**: [`@nestjs/throttler`](https://docs.nestjs.com/security/rate-limiting)
|
||||||
|
```powershell
|
||||||
|
npm install @nestjs/throttler
|
||||||
|
```
|
||||||
|
- **Prisma-optimeringar**:
|
||||||
|
- Använd `include` för att undvika N+1:
|
||||||
|
```ts
|
||||||
|
prisma.user.findMany({ include: { posts: true } });
|
||||||
|
```
|
||||||
|
- Använd `select` för att minimera data:
|
||||||
|
```ts
|
||||||
|
prisma.user.findMany({ select: { id: true, name: true } });
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.4.4 Prisma-specifika optimeringar
|
||||||
|
- **Indexering**: Lägg till `@@index` i Prisma-schemat för frekventa frågor:
|
||||||
|
```prisma
|
||||||
|
model User {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
email String @unique
|
||||||
|
name String
|
||||||
|
|
||||||
|
@@index([email]) // Optimerar sökningar på email
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Batch-operations**: Använd `createMany` istället för loopar:
|
||||||
|
```ts
|
||||||
|
await prisma.user.createMany({ data: users });
|
||||||
|
```
|
||||||
|
- **Timeout-hantering**: Använd `PrismaClient.$extends` för att lägga till timeout:
|
||||||
|
```ts
|
||||||
|
const prisma = new PrismaClient().$extends({
|
||||||
|
query: {
|
||||||
|
async $allOperations({ operation, model, args, query }) {
|
||||||
|
return query({ timeout: 10000 }); // 10s timeout
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2. Krav på varje fynd
|
||||||
|
Använd följande mall:
|
||||||
|
- **Severity**: `Critical|High|Medium|Low`
|
||||||
|
- **Evidence**: `code|test|runtime|Needs verification`
|
||||||
|
- **Delsystem**: `backend|frontend|db|infra`
|
||||||
|
- **Fil**: `<path:line>`
|
||||||
|
- **Risk**: kort riskbeskrivning
|
||||||
|
- **Varför**: varför detta är ett problem
|
||||||
|
- **Åtgärd**: konkret, realistisk fix
|
||||||
|
- **Verifiering**: kommando/test för att bekräfta fix
|
||||||
|
|
||||||
|
Blocking-fynd (`Critical/High`) listas först, därefter informational (`Medium/Low`).
|
||||||
|
|
||||||
|
---
|
||||||
|
## 3. Obligatoriskt outputformat
|
||||||
|
Returnera exakt i denna ordning:
|
||||||
|
|
||||||
|
1. `Scope`
|
||||||
|
- Urvalsregel: `staged` eller `working-tree`
|
||||||
|
- Analyserade filer (exakt lista)
|
||||||
|
- Exkluderade filer (med orsak)
|
||||||
|
|
||||||
|
2. `Gate-beslut`
|
||||||
|
- `PASS|PASS_WITH_WARNINGS|BLOCK`
|
||||||
|
- Antal per severity: `Critical`, `High`, `Medium`, `Low`
|
||||||
|
- Kort motivering
|
||||||
|
|
||||||
|
3. `Blocking Findings (Critical/High)`
|
||||||
|
- Om inga finns: skriv `Inga blockerande fynd`.
|
||||||
|
|
||||||
|
4. `Informational Findings (Medium/Low)`
|
||||||
|
- Max 5 fynd.
|
||||||
|
|
||||||
|
5. `Fixplan (vid BLOCK eller PASS_WITH_WARNINGS)`
|
||||||
|
- Numrerad ordning, konkreta steg.
|
||||||
|
|
||||||
|
6. `Sammanfattning`
|
||||||
|
- Topp 3 åtgärder efter risk/vinst
|
||||||
|
- Tidsestimat
|
||||||
|
- Rekommenderade automatiserade kontroller
|
||||||
|
|
||||||
|
---
|
||||||
|
## 4. Konsistenskontroller (måste uppfyllas)
|
||||||
|
- Om `Gate-beslut=PASS` får inga `Critical/High` listas.
|
||||||
|
- Om `Gate-beslut=BLOCK` måste `Fixplan` innehålla minst 1 konkret blockerande åtgärd.
|
||||||
|
- Om `PASS_WITH_WARNINGS` används måste exakt 1 `High` finnas och 0 `Critical`.
|
||||||
|
|
||||||
|
---
|
||||||
|
## 5. Fallback: inget att analysera
|
||||||
|
Om inga relevanta filer hittas:
|
||||||
|
- Skriv `Inget att analysera` och varför (t.ex. tom staged + tom working tree).
|
||||||
|
- Ge nästa konkreta steg:
|
||||||
|
- `git add <filer>`
|
||||||
|
- `git diff --cached --name-only`
|
||||||
|
- Kör analysen igen.
|
||||||
|
|
||||||
|
---
|
||||||
|
## 6. Kontext för projektet
|
||||||
|
- Backend: NestJS + Prisma + MariaDB (Docker).
|
||||||
|
- Frontend: Next.js + TypeScript + Flutter.
|
||||||
|
- Mål: produktion, låg teknisk skuld, säkrad hantering av känslig data.
|
||||||
|
|
||||||
|
---
|
||||||
|
## 7. CI-koppling
|
||||||
|
- Detta är lokalt pre-commit-steg.
|
||||||
|
- Samma kvalitetskrav bör speglas i CI (push/PR) för att minska miljöskillnader.
|
||||||
|
|
||||||
|
---
|
||||||
|
## 8. TypeScript Syntaxanalys (PowerShell)
|
||||||
|
|
||||||
|
### 8.1 Automatiserad Felupptäckt
|
||||||
|
|
||||||
|
#### 8.1.1 Förberedelser
|
||||||
|
Kontrollera att TypeScript och Node.js är installerade:
|
||||||
|
```powershell
|
||||||
|
node --version
|
||||||
|
tsc --version
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.1.2 Hämta alla TypeScript-fel
|
||||||
|
Kör TypeScript-compilern i **noEmit-läge** för att lista fel **utan att generera filer**:
|
||||||
|
```powershell
|
||||||
|
tsc --noEmit --pretty | Out-File -FilePath "ts_errors.log" -Encoding utf8
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.1.3 Sök efter specifika felkoder (PowerShell)
|
||||||
|
Använd `Select-String` (PowerShells motsvarighet till `grep`) för att filtrera fel:
|
||||||
|
```powershell
|
||||||
|
# Sök efter "Cannot find name" (TS2304)
|
||||||
|
Select-String -Path "ts_errors.log" -Pattern "TS2304" | Format-Table -AutoSize
|
||||||
|
|
||||||
|
# Sök efter "Property does not exist" (TS2339/TS2551)
|
||||||
|
Select-String -Path "ts_errors.log" -Pattern "TS2339|TS2551" | Format-Table -AutoSize
|
||||||
|
|
||||||
|
# Sök efter "implicitly has 'any' type" (TS7006)
|
||||||
|
Select-String -Path "ts_errors.log" -Pattern "TS7006" | Format-Table -AutoSize
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.1.4 Analysera specifika filer
|
||||||
|
Fokusera på ändrade filer i `git` (pre-commit):
|
||||||
|
```powershell
|
||||||
|
# Hämta alla ändrade TypeScript-filer (staged + unstaged)
|
||||||
|
$changedFiles = git diff --name-only --diff-filter=d | Where-Object { $_ -match '\.tsx?$' }
|
||||||
|
if ($changedFiles) {
|
||||||
|
tsc --noEmit $changedFiles | Out-File -FilePath "ts_errors_staged.log" -Encoding utf8
|
||||||
|
} else {
|
||||||
|
Write-Host "Inga ändrade TypeScript-filer hittades."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Vanliga TypeScript-Syntaxfel (Windows)
|
||||||
|
| Felkod | Beskrivning | PowerShell för att hitta fel | Åtgärd |
|
||||||
|
|--------------|---------------------------------------|--------------------------------------------------|------------------------------------------------------------------------|
|
||||||
|
| **TS2304** | Okänd variabel/import | `Select-String -Pattern "TS2304"` | Lägg till import eller deklarera variabeln. |
|
||||||
|
| **TS2339** | Egenskap saknas på objekt | `Select-String -Pattern "TS2339"` | Utöka interfacet eller använd `as`-typning. |
|
||||||
|
| **TS2551** | Felaktig egenskapsåtkomst | `Select-String -Pattern "TS2551"` | Kontrollera typdefinitionen. |
|
||||||
|
| **TS1128** | Saknad deklaration eller statement | `Select-String -Pattern "TS1128"` | Lägg till kodblock eller semikolon. |
|
||||||
|
| **TS7006** | Parameter saknar typ | `Select-String -Pattern "TS7006"` | Lägg till typannotation (t.ex. `: string`). |
|
||||||
|
| **TS1005** | Saknad semikolon | `Select-String -Pattern "TS1005"` | Lägg till semikolon om `tsconfig.json` kräver det. |
|
||||||
|
|
||||||
|
### 8.3 Åtgärdsförslag (PowerShell-exempel)
|
||||||
|
|
||||||
|
#### 8.3.1 TS2304: "Cannot find name"
|
||||||
|
**Orsak**: Variabel, funktion eller import saknas.
|
||||||
|
**Lösningar**:
|
||||||
|
1. **Lägg till import**:
|
||||||
|
```powershell
|
||||||
|
# Exempel: Saknad import för `axios`
|
||||||
|
Get-Content "src\api\service.ts" | Select-String "axios"
|
||||||
|
```
|
||||||
|
```diff
|
||||||
|
- fetchData();
|
||||||
|
+ import axios from 'axios';
|
||||||
|
+ axios.get('/api/data');
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Installera saknade typer**:
|
||||||
|
```powershell
|
||||||
|
npm install --save-dev @types/node
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.3.2 TS2339/TS2551: "Property does not exist"
|
||||||
|
**Orsak**: Egenskap saknas i typdefinitionen.
|
||||||
|
**Lösningar**:
|
||||||
|
1. **Utöka interfacet**:
|
||||||
|
```diff
|
||||||
|
- interface User { name: string; }
|
||||||
|
+ interface User { name: string; address?: { city: string; }; }
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Använd `as`-typning (temporärt)**:
|
||||||
|
```ts
|
||||||
|
const city = (user as { address: { city: string } }).address.city;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.3.3 TS1128: "Declaration or statement expected"
|
||||||
|
**Orsak**: Ogiltig syntax (t.ex. saknat kodblock).
|
||||||
|
**Lösningar**:
|
||||||
|
1. **Lägg till kodblock**:
|
||||||
|
```diff
|
||||||
|
- function foo() return 5;
|
||||||
|
+ function foo() { return 5; }
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Kontrollera semikolon** (om `tsconfig.json` kräver det):
|
||||||
|
```diff
|
||||||
|
- const x = 5
|
||||||
|
+ const x = 5;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.3.4 TS7006: "Parameter implicitly has 'any' type"
|
||||||
|
**Orsak**: Parameter saknar typ.
|
||||||
|
**Lösningar**:
|
||||||
|
1. **Lägg till typannotation**:
|
||||||
|
```diff
|
||||||
|
- function greet(name) { ... }
|
||||||
|
+ function greet(name: string) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Stäng av `noImplicitAny`** (ej rekommenderat):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"noImplicitAny": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 Exempel på Korrigeringar (PowerShell)
|
||||||
|
|
||||||
|
#### 8.4.1 Fixa saknad import (TS2304)
|
||||||
|
**Felaktig kod**:
|
||||||
|
```ts
|
||||||
|
getUser(); // TS2304: Cannot find name 'getUser'
|
||||||
|
```
|
||||||
|
**Fix**:
|
||||||
|
```powershell
|
||||||
|
# Kontrollera om funktionen existerar i projektet
|
||||||
|
Get-ChildItem -Recurse -Include *.ts | Select-String -Pattern "function getUser"
|
||||||
|
```
|
||||||
|
```ts
|
||||||
|
import { getUser } from './api'; // Lägg till import
|
||||||
|
getUser();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.4.2 Fixa ogiltig egenskap (TS2551)
|
||||||
|
**Felaktig kod**:
|
||||||
|
```ts
|
||||||
|
interface User { name: string; }
|
||||||
|
const user: User = { name: 'Alice' };
|
||||||
|
console.log(user.age); // TS2551: Property 'age' does not exist
|
||||||
|
```
|
||||||
|
**Fix**:
|
||||||
|
```ts
|
||||||
|
interface User { name: string; age?: number; } // Gör 'age' valfritt
|
||||||
|
const user: User = { name: 'Alice' };
|
||||||
|
console.log(user.age); // OK (kan vara `undefined`)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.5 PowerShell-specifika Tips
|
||||||
|
|
||||||
|
#### 8.5.1 Sökvägar och Filtrering
|
||||||
|
- **Använd dubbla citattecken (`"`) för sökvägar**:
|
||||||
|
```powershell
|
||||||
|
Get-ChildItem -Path "C:\Users\Nils-JohanGynther\dev\recipe-app\src\*.ts"
|
||||||
|
```
|
||||||
|
- **Escape specialtecken** med backtick (`\`):
|
||||||
|
```powershell
|
||||||
|
Select-String -Pattern "Property\`|does not exist"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.5.2 Kör `tsc` med full sökväg (om nödvändigt)
|
||||||
|
```powershell
|
||||||
|
& "C:\Users\Nils-JohanGynther\dev\recipe-app\node_modules\.bin\tsc" --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.5.3 Spara felutdata till fil
|
||||||
|
```powershell
|
||||||
|
tsc --noEmit | Out-File -FilePath "ts_errors.log" -Encoding utf8
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.6 Rekommenderat Arbetsflöde (PowerShell)
|
||||||
|
1. **Lista alla TypeScript-fel**:
|
||||||
|
```powershell
|
||||||
|
tsc --noEmit --pretty | Out-File -FilePath "ts_errors.log" -Encoding utf8
|
||||||
|
```
|
||||||
|
2. **Filtrera efter felkod**:
|
||||||
|
```powershell
|
||||||
|
Select-String -Path "ts_errors.log" -Pattern "TS2304|TS2551" | Format-Table -AutoSize
|
||||||
|
```
|
||||||
|
3. **Åtgärda felen** enligt tabellen ovan.
|
||||||
|
4. **Validera fixar**:
|
||||||
|
```powershell
|
||||||
|
tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.7 Vanliga PowerShell-kommandon
|
||||||
|
| Syfte | PowerShell-kommando |
|
||||||
|
|--------------------------------|-------------------------------------------------------|
|
||||||
|
| Lista TypeScript-filer | `Get-ChildItem -Recurse -Include *.ts,*.tsx` |
|
||||||
|
| Sök efter text i filer | `Select-String -Path "src\*.ts" -Pattern "TS2304"` |
|
||||||
|
| Kör TypeScript-compilern | `tsc --noEmit` |
|
||||||
|
| Spara felutdata till fil | `tsc --noEmit | Out-File "errors.log"` |
|
||||||
|
| Visa innehåll i en fil | `Get-Content "src\app.ts"` |
|
||||||
|
| Kontrollera git-status | `git status` |
|
||||||
|
| Lista ändrade filer | `git diff --name-only --diff-filter=d` |
|
||||||
|
|
||||||
|
### 8.8 Output-format (för pre-commit)
|
||||||
|
När du kör analysen, returnera resultatet i detta format:
|
||||||
|
```markdown
|
||||||
|
### Scope
|
||||||
|
- **Analyserade filer**:
|
||||||
|
- `src\api\service.ts` (2 TS2304-fel)
|
||||||
|
- `src\models\user.ts` (1 TS2551-fel)
|
||||||
|
- **Exkluderade filer**:
|
||||||
|
- `node_modules\` (ignorerad)
|
||||||
|
- `dist\` (genererad kod)
|
||||||
|
|
||||||
|
### Gate-beslut: `PASS_WITH_WARNINGS`
|
||||||
|
- **Critical**: 0
|
||||||
|
- **High**: 1 (TS2551 i `user.ts`)
|
||||||
|
- **Medium**: 2
|
||||||
|
- **Low**: 0
|
||||||
|
|
||||||
|
### Blocking Findings (Critical/High)
|
||||||
|
1. **TS2551** i `src\models\user.ts:15`
|
||||||
|
- **Risk**: Egenskapen `address` saknas i `User`-interfacet.
|
||||||
|
- **Åtgärd**: Utöka interfacet eller använd `as`-typning.
|
||||||
|
- **Verifiering**: Kör `tsc --noEmit` efter fix.
|
||||||
|
|
||||||
|
### Informational Findings (Medium/Low)
|
||||||
|
1. **TS2304** i `src\api\service.ts:8`
|
||||||
|
- **Förbättring**: Lägg till import för `axios`.
|
||||||
|
- **Kommando**: `npm install axios --save`
|
||||||
|
|
||||||
|
2. **TS7006** i `src\utils\helpers.ts:3`
|
||||||
|
- **Förbättring**: Lägg till typ för parametern `data`.
|
||||||
|
- **Exempel**: `function parse(data: string) { ... }`
|
||||||
|
|
||||||
|
### Fixplan
|
||||||
|
1. Åtgärda `TS2551` i `user.ts` (blockerande).
|
||||||
|
2. Lägg till saknade imports i `service.ts`.
|
||||||
|
3. Typa parametrar i `helpers.ts`.
|
||||||
|
|
||||||
|
### Sammanfattning
|
||||||
|
- **Topp 3 åtgärder**:
|
||||||
|
1. Fixa `User`-interfacet (5 min).
|
||||||
|
2. Installera `axios` (2 min).
|
||||||
|
3. Lägg till typer i `helpers.ts` (3 min).
|
||||||
|
- **Tidsestimat**: 10 minuter.
|
||||||
|
- **Rekommenderade automatiserade kontroller**:
|
||||||
|
- Lägg till `tsc --noEmit` i `pre-commit`-hook.
|
||||||
|
- Konfigurera ESLint för TypeScript (`@typescript-eslint`).
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.9 Felhantering i PowerShell
|
||||||
|
Använd `try/catch` för att fånga fel i PowerShell-skript:
|
||||||
|
```powershell
|
||||||
|
try {
|
||||||
|
tsc --noEmit | Out-File -FilePath "ts_errors.log" -Encoding utf8 -ErrorAction Stop
|
||||||
|
} catch {
|
||||||
|
Write-Error "TypeScript-compilering misslyckades: $_"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.10 Dynamiska sökvägar och validering
|
||||||
|
|
||||||
|
#### 8.10.1 Använd dynamiska sökvägar
|
||||||
|
Ersätt hårdkodade sökvägar med variabler för bättre portabilitet:
|
||||||
|
```powershell
|
||||||
|
$projectRoot = Resolve-Path "."
|
||||||
|
$tsconfigPath = Join-Path $projectRoot "tsconfig.json"
|
||||||
|
tsc --project $tsconfigPath --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.10.2 Validera TypeScript-installation
|
||||||
|
Kontrollera att `tsc` är tillgängligt innan analys:
|
||||||
|
```powershell
|
||||||
|
if (-not (Get-Command tsc -ErrorAction SilentlyContinue)) {
|
||||||
|
Write-Error "TypeScript är inte installerat. Kör: npm install -g typescript"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.10.3 Förbättrad git-integration
|
||||||
|
Hämta ändrade filer med fullständiga sökvägar:
|
||||||
|
```powershell
|
||||||
|
$changedFiles = git diff --name-only --diff-filter=d | ForEach-Object {
|
||||||
|
$filePath = Resolve-Path $_ -ErrorAction SilentlyContinue
|
||||||
|
if ($filePath -and $filePath -match '\.tsx?$') { $filePath }
|
||||||
|
}
|
||||||
|
if ($changedFiles) {
|
||||||
|
tsc --noEmit $changedFiles | Out-File -FilePath "ts_errors_staged.log" -Encoding utf8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.11 Stöd för PowerShell Core (pwsh)
|
||||||
|
> **Notera**: Alla kommandon i denna guide är testade och fungerar i både:
|
||||||
|
> - **Windows PowerShell 5.1** (standard i Windows 10/11)
|
||||||
|
> - **PowerShell Core 7+** (`pwsh`)
|
||||||
|
|
||||||
|
För bästa resultat, använd PowerShell Core (`pwsh`) för:
|
||||||
|
- Snabbare exekvering.
|
||||||
|
- Bättre Unicode-stöd (t.ex. emojis i felmeddelanden).
|
||||||
|
- Kompatibilitet med multiplattforms-projekt.
|
||||||
|
|
||||||
|
Installera PowerShell Core:
|
||||||
|
```powershell
|
||||||
|
winget install --id Microsoft.Powershell --source winget
|
||||||
|
```
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://app.kilo.ai/config.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
# Plan: Fortsatt implementation av FlyerImport (autoflöde + UX i `/import` Flutter)
|
||||||
|
|
||||||
|
## Målbild
|
||||||
|
Implementera ett komplett flyer-flöde med så få klick som möjligt där:
|
||||||
|
- användaren markerar planerade inköp i `FlyerImportTab`
|
||||||
|
- kvittoimporten matchar automatiskt mot öppna `FlyerSelection` och sätter `status=bought`
|
||||||
|
- användaren bara behöver ingripa vid osäker matchning
|
||||||
|
|
||||||
|
Detta följer båda underlagen:
|
||||||
|
1) ingen befintlig flyer-UX i `/import` ännu (behöver byggas)
|
||||||
|
2) maximal automation i punkt 3 (sync med kvittoimport)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Principer för “så få klick som möjligt”
|
||||||
|
- Default är automation: matcha och uppdatera utan extra dialoger.
|
||||||
|
- UI ska vara “review first, edit only when needed”.
|
||||||
|
- Endast två manuella åtgärder i happy path:
|
||||||
|
1. markera planerade flyer-varor
|
||||||
|
2. importera kvitto
|
||||||
|
- Manuell override ska finnas men inte blockera flödet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 1: Backend-kontrakt för auto-sync
|
||||||
|
|
||||||
|
### 1.1 Nya/utökade endpoints
|
||||||
|
Implementera i `backend/src/flyer-selection`:
|
||||||
|
|
||||||
|
- `POST /flyer-selections/receipt-match-preview`
|
||||||
|
- Input: kvittorader (normaliserad struktur), `weekKey` (optional), `sessionId` (optional)
|
||||||
|
- Output: matchförslag per kvittorad + confidence + reasonCodes + kandidat-selection
|
||||||
|
- Används för transparent UI-annotering före commit
|
||||||
|
|
||||||
|
- `POST /flyer-selections/receipt-match-commit`
|
||||||
|
- Input: samma payload + optional overrides
|
||||||
|
- Output: antal uppdaterade selections, listor över `bought`, `unmatched`, `ambiguous`
|
||||||
|
- Utför transaktionell statusuppdatering (`planned -> bought`)
|
||||||
|
|
||||||
|
- `GET /flyer-selections/open`
|
||||||
|
- Query: `weekKey`, `retailer`, pagination
|
||||||
|
- Returnerar öppna selections (`status=planned`) för snabb klienthämtning
|
||||||
|
|
||||||
|
Notering: Behåll befintliga CRUD-rutter under `/flyer-sessions/:sessionId/selections`.
|
||||||
|
|
||||||
|
### 1.2 Matchningsmotor (service-nivå)
|
||||||
|
I `FlyerSelectionService` lägg till en intern matcher med prioriterad strategi:
|
||||||
|
1. `productId` exact (högsta prio)
|
||||||
|
2. normalized name exact
|
||||||
|
3. alias/ordöverlap (token)
|
||||||
|
4. quantity/unit-stöd som förstärkning (inte ensam källa)
|
||||||
|
|
||||||
|
Regler:
|
||||||
|
- En `FlyerSelection` kan bara konsumeras en gång per commit.
|
||||||
|
- Confidence-trösklar:
|
||||||
|
- `>=0.90`: auto-commit-kandidat
|
||||||
|
- `0.70-0.89`: ambiguous (kräver override för commit)
|
||||||
|
- `<0.70`: unmatched
|
||||||
|
- Returnera alltid `matchedVia`, `confidence`, `reasonCodes`.
|
||||||
|
|
||||||
|
### 1.3 Datamodell-justeringar (om behövs)
|
||||||
|
Nuvarande schema räcker i stort, men planera följande icke-blockerande förbättringar:
|
||||||
|
- `FlyerSelection`:
|
||||||
|
- `boughtAt DateTime?`
|
||||||
|
- `boughtSource String?` (t.ex. `receipt_auto`, `receipt_manual`)
|
||||||
|
- `receiptImportBatchId String?` för spårbarhet
|
||||||
|
- Index:
|
||||||
|
- `(userId, status, updatedAt)` för snabb hämtning av öppna poster
|
||||||
|
|
||||||
|
Migration i separat steg efter kod.
|
||||||
|
|
||||||
|
### 1.4 Säkerhet och robusthet
|
||||||
|
- Validera alltid user-scope i alla nya endpoints.
|
||||||
|
- Rate-limit på match-endpoints (liknande befintlig throttle-nivå).
|
||||||
|
- Transaktion (`prisma.$transaction`) för commit så statusuppdatering blir atomisk.
|
||||||
|
- Idempotens: commit med samma `receiptImportBatchId` ska inte dubbeluppdatera.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 2: Flutter UX i `/import` (ny flyer-tab)
|
||||||
|
|
||||||
|
### 2.1 Lägg till `FlyerImportTab`
|
||||||
|
Uppdatera `ImportScreen` så tabbar blir:
|
||||||
|
1. Recept
|
||||||
|
2. Kvitto
|
||||||
|
3. Flyer
|
||||||
|
|
||||||
|
Skapa:
|
||||||
|
- `flutter/lib/features/import/presentation/flyer_import_tab.dart`
|
||||||
|
- `flutter/lib/features/import/data/flyer_import_repository.dart`
|
||||||
|
- `flutter/lib/features/import/data/flyer_import_providers.dart`
|
||||||
|
- `flutter/lib/features/import/domain/flyer_item.dart`
|
||||||
|
- `flutter/lib/features/import/domain/flyer_selection.dart`
|
||||||
|
|
||||||
|
### 2.2 Flyer-tabens minimalklick-flöde
|
||||||
|
Steg i UI:
|
||||||
|
1. Upload flyerfil
|
||||||
|
2. Visa parserader med förvald checkbox för matchade varor
|
||||||
|
3. Primär CTA: `Planera markerade` (bulk-create/upsert)
|
||||||
|
4. Direkt visning av statuschips (`planned/bought/skipped`)
|
||||||
|
|
||||||
|
Klickoptimering:
|
||||||
|
- Förifyll `plannedQuantity/plannedUnit` från flyerdata.
|
||||||
|
- Batch-upsert selections i ett enda API-anrop.
|
||||||
|
- Visa inline varningar istället för modaler där möjligt.
|
||||||
|
|
||||||
|
### 2.3 Statusöversikt
|
||||||
|
I flyer-tabben visa sektioner:
|
||||||
|
- `Planerade` (öppna)
|
||||||
|
- `Nyligen köpta` (autouppdaterade från kvitto)
|
||||||
|
- `Ej matchade vid senaste kvitto` (för snabb manuell hantering)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 3: Integrera auto-sync i `ReceiptImportTab`
|
||||||
|
|
||||||
|
### 3.1 Hook efter receipt parse
|
||||||
|
I befintlig `_submit()` i `receipt_import_tab.dart`:
|
||||||
|
1. importera kvitto som idag
|
||||||
|
2. anropa `receipt-match-preview`
|
||||||
|
3. annotera UI-rader med matchstatus (icon/chip)
|
||||||
|
4. vid `Lägg till markerade`: anropa `receipt-match-commit` parallellt/sekventiellt med saveReceipt
|
||||||
|
|
||||||
|
### 3.2 Zero-click commit i happy path
|
||||||
|
Defaultbeteende vid `Lägg till markerade`:
|
||||||
|
- auto-committa alla matcher med confidence `>=0.90`
|
||||||
|
- lämna ambiguous som `planned`
|
||||||
|
- visa en kompakt snackbar:
|
||||||
|
- `2 planerade markerades som köpta`
|
||||||
|
- `1 kräver manuell kontroll`
|
||||||
|
|
||||||
|
Ingen extra dialog i standardfall.
|
||||||
|
|
||||||
|
### 3.3 Manuell override (endast vid behov)
|
||||||
|
Lägg till valfri expandrad i resultatlistan:
|
||||||
|
- “Föreslagen flyer-match” + knapp `Bekräfta ändå`
|
||||||
|
- används endast för ambiguous fall
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 4: API- och UI-detaljer för låg friktion
|
||||||
|
|
||||||
|
### 4.1 Payload-standard (receipt -> matcher)
|
||||||
|
Standardisera kvittorad till:
|
||||||
|
- `rowId` (lokalt index eller UUID)
|
||||||
|
- `rawName`
|
||||||
|
- `normalizedName`
|
||||||
|
- `productId` (om redan mappad)
|
||||||
|
- `quantity`
|
||||||
|
- `unit`
|
||||||
|
- `price`
|
||||||
|
|
||||||
|
### 4.2 UX-copy
|
||||||
|
Konsekventa texter i UI:
|
||||||
|
- `Automatchad mot flyer`
|
||||||
|
- `Osäker matchning`
|
||||||
|
- `Ej matchad`
|
||||||
|
|
||||||
|
### 4.3 WeekKey-hantering
|
||||||
|
Fallback-ordning vid matchning:
|
||||||
|
1. explicit `sessionId`
|
||||||
|
2. explicit `weekKey`
|
||||||
|
3. server beräknar aktuell `weekKey`
|
||||||
|
|
||||||
|
Detta minimerar klientlogik och fel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 5: Test och kvalitetssäkring
|
||||||
|
|
||||||
|
### 5.1 Backend
|
||||||
|
- Unit-tester för matcher-regler och confidence-nivåer
|
||||||
|
- Service-tester för commit-idempotens
|
||||||
|
- Controller-e2e för user-scope + throttling + felkoder
|
||||||
|
- Prisma-transaktionsscenarion (dubbelklick/duplicerat commit)
|
||||||
|
|
||||||
|
### 5.2 Flutter
|
||||||
|
- Widget-tester för:
|
||||||
|
- `FlyerImportTab` listning/bulk-planering
|
||||||
|
- kvitto-rad med automatch-chip
|
||||||
|
- Integrationstester för `ReceiptImportTab` + auto-sync callback
|
||||||
|
|
||||||
|
### 5.3 Acceptanskriterier (måste uppfyllas)
|
||||||
|
- Happy path kräver max 2 aktiva klick från planering till auto-bought.
|
||||||
|
- Minst 90% av high-confidence-matchningar autouppdateras korrekt i test-fixtures.
|
||||||
|
- Inga writes till `Product/Inventory` sker i flyer-planeringsfas.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 6: Gradvis lansering
|
||||||
|
|
||||||
|
1. Backend-endpoints bakom feature flag: `flyerReceiptAutoSyncEnabled`
|
||||||
|
2. Aktivera för intern/test-användare först
|
||||||
|
3. Mät:
|
||||||
|
- andel auto-match
|
||||||
|
- andel ambiguous
|
||||||
|
- manuell override-frekvens
|
||||||
|
4. Finjustera thresholds innan full rollout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementeringsordning (konkret)
|
||||||
|
1. Backend: matcher + preview/commit endpoints
|
||||||
|
2. Backend: idempotens + spårbarhetsfält + migration
|
||||||
|
3. Flutter: ny `FlyerImportTab` + repository/providers
|
||||||
|
4. Flutter: integrera preview/commit i `ReceiptImportTab`
|
||||||
|
5. Tester backend + Flutter
|
||||||
|
6. Feature flag rollout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risker och mitigering
|
||||||
|
- Felmatchningar: håll konservativ tröskel och auto-commit bara vid hög confidence.
|
||||||
|
- Dubbla commits: idempotensnyckel + transaktion.
|
||||||
|
- UX-brus: visa detaljer först vid ambiguous, inte i happy path.
|
||||||
|
- Prestanda: batcha matchning och undvik N+1-frågor via prefetch av öppna selections.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- `/import` har en fungerande `FlyerImportTab`.
|
||||||
|
- Kvittoimport auto-synkar mot `FlyerSelection` med minimal friktion.
|
||||||
|
- `planned -> bought` uppdateras automatiskt för high-confidence.
|
||||||
|
- Ambiguous fall kan hanteras manuellt utan att blockera flödet.
|
||||||
|
- Tester gröna och feature flag klar för kontrollerad utrullning.
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Plan: Omgjord flyerimport (pdf-parse + tesseract + Mistral Tiny)
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
Ersätta nuvarande flyerimportflöde (som idag delegerar till `importer-api`) med en robust pipeline som:
|
||||||
|
1) extraherar text från flyer-PDF (primärt `pdf-parse`, fallback OCR via Tesseract),
|
||||||
|
2) skickar normaliserad text till Mistral Tiny,
|
||||||
|
3) returnerar strikt strukturerad JSON,
|
||||||
|
4) behåller befintlig matchning/planeringsflöde i backend + Flutter men förbättrar UX kring importresultat och fel.
|
||||||
|
|
||||||
|
## Nulägesanalys (projektanpassad)
|
||||||
|
- Backend endpoint finns: `POST /flyer-import/parse` i `backend/src/flyer-import/flyer-import.controller.ts`.
|
||||||
|
- Nuvarande backendlogik i `backend/src/flyer-import/flyer-import.service.ts` anropar extern tjänst via `IMPORTER_SERVICE_URL` (`/api/flyer/parse`).
|
||||||
|
- Flutter har redan komplett flyerflik i `flutter/lib/features/import/presentation/flyer_import_tab.dart`:
|
||||||
|
- filval, importknapp, preview, radrendering med checkboxar, bulk-planering.
|
||||||
|
- Datamodell för sessions/items finns redan i Prisma (`FlyerSession`, `FlyerItem`, `FlyerSelection`) och stödjer parse+match-metadata.
|
||||||
|
- `flyerimporter.md` beskriver rätt riktning men är generisk; projektet behöver NestJS-integration och kompatibilitet med befintliga DTO/Flutter-modeller.
|
||||||
|
|
||||||
|
## Föreslagen arkitektur (ersättning av dagens lösning)
|
||||||
|
|
||||||
|
### 1) Ny intern parser i recipe-api (NestJS)
|
||||||
|
- Ersätt `parseViaImporter(...)` i `FlyerImportService` med lokal pipeline:
|
||||||
|
- `extractFlyerText(file)`
|
||||||
|
- PDF/text-extraktion via `pdf-parse`.
|
||||||
|
- Fallback OCR via Tesseract för sidor/underlag utan användbar text.
|
||||||
|
- `parseFlyerWithMistral(text)`
|
||||||
|
- Mistral Tiny-anrop med strikt JSON-schema-prompt.
|
||||||
|
- `normalizeFlyerItems(aiJson)`
|
||||||
|
- validering, typkonvertering, enhetsnormalisering, confidence/reasonCodes.
|
||||||
|
- Behåll resten av tjänsten intakt (matchning, sessionpersistens, selections-kompatibilitet).
|
||||||
|
|
||||||
|
### 2) AI-kontrakt (strikt JSON)
|
||||||
|
- Introducera explicit schema för AI-svar (intern typ + runtime-validering):
|
||||||
|
- `rawName`, `normalizedName`, `category`, `price`, `priceUnit`, `comparisonPrice`, `comparisonUnit`, `offerText`, `confidence`, `reasonCodes`.
|
||||||
|
- Promptdesign:
|
||||||
|
- svensk flyer-kontext,
|
||||||
|
- tydlig enhets- och prisnormalisering,
|
||||||
|
- "returnera ENDAST JSON" + exempel,
|
||||||
|
- fallback vid saknade fält (`null`, tomma listor).
|
||||||
|
- Robust parsing av modelloutput:
|
||||||
|
- ta bort ev. markdown fences,
|
||||||
|
- fail-fast med tydligt felmeddelande om ogiltigt JSON.
|
||||||
|
|
||||||
|
### 3) OCR-strategi
|
||||||
|
- Primärväg: `pdf-parse` (snabb, billig).
|
||||||
|
- OCR-fallback: bara när extraherad text är tom/under tröskel.
|
||||||
|
- Preprocess för OCR (vid behov): sidvis rasterisering + språk `swe` (ev. `swe+eng`).
|
||||||
|
- Timeout/guardrails per steg för att undvika låsta importer.
|
||||||
|
|
||||||
|
### 4) API/infra-anpassning
|
||||||
|
- Controller (`flyer-import.controller.ts`):
|
||||||
|
- uppdatera tillåtna MIME-typer så de matchar Flutter-filtyper (PDF + bilder om vi ska stödja bildflyers).
|
||||||
|
- `compose.yml`/env:
|
||||||
|
- gör `IMPORTER_SERVICE_URL` optional eller avveckla för flyerflödet.
|
||||||
|
- säkerställ `MISTRAL_API_KEY` används av `recipe-api` för flyer.
|
||||||
|
- Dokumentation:
|
||||||
|
- uppdatera teknisk beskrivning så flyerimport inte längre kräver extern flyer-parser.
|
||||||
|
|
||||||
|
## UX-analys Flutter (nuvarande) och planerade förbättringar
|
||||||
|
|
||||||
|
### Nuvarande UX (bra att bygga vidare på)
|
||||||
|
- Enkel 3-stegsinteraktion: välj fil -> importera -> markera/planera.
|
||||||
|
- Förhandsvisning finns och passar arbetsflödet.
|
||||||
|
- Offer-badge + pris/jämförpris + matchvisning ger snabb scanning.
|
||||||
|
|
||||||
|
### UX-gap att täppa till i denna implementation
|
||||||
|
- Ingen tydlig visning av parserwarnings från backend (fältet `warnings` finns i modellen).
|
||||||
|
- Ingen kvalitetssignal i UI trots att `parseConfidence/matchConfidence` finns.
|
||||||
|
- Felmeddelanden är relativt råa; saknar råd per feltyp (timeout, ogiltig fil, AI-svar oformaterat).
|
||||||
|
|
||||||
|
### Föreslagna UX-förbättringar (inkrementella, kompatibla)
|
||||||
|
1. Visa `warnings` över resultatlistan i en kompakt varningspanel.
|
||||||
|
2. Lägg till "kvalitetsindikator" per rad (t.ex. låg/medel/hög) baserat på `parseConfidence` + `matchConfidence`.
|
||||||
|
3. Lägg till filterchips: `Endast erbjudanden`, `Saknar matchning`, `Låg kvalitet`.
|
||||||
|
4. Förbättra loading-state med stegnära text ("Extraherar text", "Tolkar med AI", "Matchar produkter").
|
||||||
|
5. Felmappning till användarvänliga meddelanden i `showErrorDialog` (teknisk detalj i kopierbar sekundärtext).
|
||||||
|
|
||||||
|
## Implementationsplan (ordning)
|
||||||
|
|
||||||
|
### Fas A - Backend kärna
|
||||||
|
1. Lägg till dependencies i `backend/package.json` för PDF/OCR/Mistral-klient.
|
||||||
|
2. Skapa intern flyer-parser service i `backend/src/flyer-import/` (text extraction + AI parse).
|
||||||
|
3. Byt `parseViaImporter` till intern implementation i `FlyerImportService`.
|
||||||
|
4. Lägg till runtime-validering och normalisering av AI-svar.
|
||||||
|
|
||||||
|
### Fas B - Kontrakt och robusthet
|
||||||
|
5. Säkerställ att response-format fortsatt matchar `FlyerImportResponse` (ingen breaking change mot Flutter).
|
||||||
|
6. Förbättra controller MIME-regler så de stämmer med faktiska stödda format.
|
||||||
|
7. Lägg till tydliga felkoder/meddelanden för:
|
||||||
|
- tom/oläsbar flyer,
|
||||||
|
- AI-parsefel,
|
||||||
|
- timeout/service unavailable.
|
||||||
|
|
||||||
|
### Fas C - Flutter UX på befintlig skärm
|
||||||
|
8. Visa backend `warnings` i `flyer_import_tab.dart`.
|
||||||
|
9. Lägg till kvalitetsindikator + minimala filterchips.
|
||||||
|
10. Förfina loading/feltexter utan att ändra grundlayouten.
|
||||||
|
|
||||||
|
### Fas D - Verifiering
|
||||||
|
11. Backendtester för intern flyer-parser (happy path + fallback + felbanor).
|
||||||
|
12. Uppdatera/addera Flutter widgettester för warnings/indikator/filter.
|
||||||
|
13. Manuell E2E: PDF med text, PDF med skannade sidor, bildflyer, trasig fil.
|
||||||
|
|
||||||
|
## Filer som sannolikt berörs vid implementation
|
||||||
|
- `backend/src/flyer-import/flyer-import.service.ts`
|
||||||
|
- `backend/src/flyer-import/flyer-import.controller.ts`
|
||||||
|
- `backend/src/flyer-import/dto/flyer-import.response.ts` (endast om extra metadata behövs)
|
||||||
|
- `backend/package.json`
|
||||||
|
- `flutter/lib/features/import/presentation/flyer_import_tab.dart`
|
||||||
|
- Ev. `flutter/lib/features/import/domain/flyer_import_item.dart` (om ny UI-metadata exponeras)
|
||||||
|
- Dokumentation: `TEKNISK_BESKRIVNING.md` (kort uppdatering av arkitektur)
|
||||||
|
|
||||||
|
## Risker och mitigering
|
||||||
|
- OCR-prestanda/latens: använd fallback-only och timeout.
|
||||||
|
- Mistral kan ge semistrukturerat svar: strikt schema + robust JSON-sanitizing + validering.
|
||||||
|
- Kostnad/kvot på AI-anrop: minimera promptstorlek, trunkera brus, återanvänd normalisering.
|
||||||
|
- Driftöverraskningar: behåll endpoint-kontrakt oförändrat mot Flutter.
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
- Flyerimport fungerar utan beroende av extern `/api/flyer/parse` i importer-api.
|
||||||
|
- Minst en PDF med inbäddad text och en skannad PDF importeras framgångsrikt.
|
||||||
|
- Backend returnerar valid `FlyerImportResponse` och befintlig planeringsfunktion fortsätter fungera.
|
||||||
|
- Flutter visar warnings och gör det tydligare vilka rader som behöver manuell granskning.
|
||||||
|
|
||||||
|
## Fastställt beslut
|
||||||
|
- Första leveransen ska stödja **PDF + bildfiler** (`png/jpg/webp`) fullt ut.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# Åtgärdsplan utifrån E2E-fynd (Flyer + Inventory)
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
- Göra flyerflödet praktiskt användbart för jämförelse, redigering och planering till inköpslista.
|
||||||
|
- Säkerställa att inventory/pantry visar korrekta kategorier i stället för att allt hamnar i `Övrigt`.
|
||||||
|
|
||||||
|
## Scope (utifrån dina punkter)
|
||||||
|
1. Flyer-vy:
|
||||||
|
- Visa importerad flyer-PDF för jämförelse mot extraherade rader.
|
||||||
|
- Redigera poster (namn + kategori) med samma kategorikälla som products/pantry.
|
||||||
|
- `Planera X markerade` ska skapa en faktisk inköpslista i en egen flik.
|
||||||
|
2. Inventory:
|
||||||
|
- Felsöka och åtgärda varför poster visas under `Övrigt`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nulägesbild (från kodbasen)
|
||||||
|
- Flutter har redan `FlyerImportTab` med:
|
||||||
|
- filval/import,
|
||||||
|
- checkboxar,
|
||||||
|
- `Planera X markerade` -> `POST /flyer-sessions/:id/selections/bulk`.
|
||||||
|
- PDF-visning finns idag bara för aktuell uppladdad fil i minnet (`_pickedFile.bytes`) och kan inte återöppnas säkert vid återställd session/app-omstart.
|
||||||
|
- Flyer-rader kan ännu inte redigeras i UI (ingen inline edit-dialog för flyer-item).
|
||||||
|
- Meal Plan har en shopping-sektion, men ingen dedikerad flik för flyer-planerade köp.
|
||||||
|
- Kategori-träd finns redan och används i flera vyer via `/categories/tree`.
|
||||||
|
- Inventory läser kategori via `product.categoryRef`; null blir `Övrigt` i UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Genomförandeplan
|
||||||
|
|
||||||
|
### 1) Flyer-PDF: beständig förhandsvisning per session
|
||||||
|
**Backend**
|
||||||
|
- Lägg till lagring av originalfil för flyer-session (MVP: lokal filstore eller DB blob beroende på befintligt mönster i projektet).
|
||||||
|
- Utöka `flyer_session` metadata med filreferens (filnamn, mime, storlek, storageKey).
|
||||||
|
- Ny endpoint: `GET /flyer-import/sessions/:sessionId/source` (auth + ägarskap) som streamar PDF/bild.
|
||||||
|
|
||||||
|
**Flutter**
|
||||||
|
- I `FlyerImportTab`:
|
||||||
|
- använd befintlig local preview direkt efter uppladdning,
|
||||||
|
- när session återställs: hämta source-endpoint och visa `Visa flyer` även då.
|
||||||
|
- Behåll fallback-meddelande för plattformar som inte kan öppna PDF direkt.
|
||||||
|
|
||||||
|
**Klart-kriterium**
|
||||||
|
- Samma importerade flyer kan öppnas efter tab-byte och app-omstart för samma användare.
|
||||||
|
|
||||||
|
### 2) Redigering av flyer-poster (namn + kategori)
|
||||||
|
**Backend**
|
||||||
|
- Lägg till endpoint för uppdatering av flyer-item i session, t.ex.
|
||||||
|
- `PATCH /flyer-import/sessions/:sessionId/items/:itemId`
|
||||||
|
- fält: `rawName` (eller `displayName`) och `categoryId` (ev. `categoryHintPath` för visning).
|
||||||
|
- Validera att kategori finns i samma kategoriträd (`categories`).
|
||||||
|
- Ägarskapskontroll via sessionens `userId`.
|
||||||
|
|
||||||
|
**Flutter**
|
||||||
|
- I listan i `FlyerImportTab`: lägg till `Redigera`-action per rad.
|
||||||
|
- Edit-dialog:
|
||||||
|
- textfält för namn,
|
||||||
|
- kategori-väljare baserad på samma träd/komponentmönster som inventory/pantry/admin.
|
||||||
|
- Spara uppdatering till backend och uppdatera lokal/session state.
|
||||||
|
|
||||||
|
**Klart-kriterium**
|
||||||
|
- Användaren kan ändra namn och kategori på en flyer-rad och ser ändringen direkt i listan.
|
||||||
|
|
||||||
|
### 3) Flyer -> Inköpslista i egen flik
|
||||||
|
**Backend**
|
||||||
|
- Definiera enkel shopping-list-resurs för MVP (user-scoped):
|
||||||
|
- tabell t.ex. `shopping_list_item` (name/productId/categoryId/quantity/unit/source/status/userId).
|
||||||
|
- Ny endpoint för att skapa inköpsrader från flyer-selections:
|
||||||
|
- `POST /flyer-sessions/:sessionId/selections/plan-to-shopping-list`
|
||||||
|
- alternativt återanvänd bulk-create i shopping-modul.
|
||||||
|
- Deduplicering/regler:
|
||||||
|
- om samma `productId+unit` finns öppet: summera eller hoppa över (bestäms i implementation; rekommenderat: summera).
|
||||||
|
|
||||||
|
**Flutter**
|
||||||
|
- Lägg till ny flik/skärm `Inköpslista` i app-shell.
|
||||||
|
- `Planera X markerade` i flyer-vyn ska:
|
||||||
|
1) skapa/uppdatera flyer selections,
|
||||||
|
2) trigga backend-mappning till shopping-list,
|
||||||
|
3) visa snackbar med antal tillagda/uppdaterade rader.
|
||||||
|
- Inköpslista-vyn (MVP): lista rader + enkel check/avprickning.
|
||||||
|
|
||||||
|
**Klart-kriterium**
|
||||||
|
- Klick på `Planera X markerade` flyttar markerade flyer-produkter till Inköpslista-fliken.
|
||||||
|
|
||||||
|
### 4) Inventory-fel: allt i `Övrigt`
|
||||||
|
**Felsökning**
|
||||||
|
- Verifiera varför `product.categoryId/categoryRef` blir null i aktuella poster:
|
||||||
|
- skapade produkter utan kategori,
|
||||||
|
- importflöden som inte persistar vald kategori,
|
||||||
|
- äldre data utan backfill.
|
||||||
|
|
||||||
|
**Åtgärd**
|
||||||
|
- Säkerställ att produktskapande från import/edit alltid skickar/sätter kategori när sådan är vald.
|
||||||
|
- Lägg skydd i backend så kategori inte tappas vid update-flöden.
|
||||||
|
- Engångs-backfill för befintliga produkter utan kategori:
|
||||||
|
- använd befintlig kategoriseringslogik (regel/AI) + fallback till rimlig underkategori.
|
||||||
|
- Kör re-fetch/invalidations i Flutter inventory/pantry efter backfill.
|
||||||
|
|
||||||
|
**Klart-kriterium**
|
||||||
|
- Inventory/pantry visar blandade korrekta kategorier; endast okända poster ligger kvar i `Övrigt`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testplan
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- Nya tester för:
|
||||||
|
- auth/ownership på source-endpoint och item-edit endpoint,
|
||||||
|
- validering av kategori-id,
|
||||||
|
- plan-to-shopping-list (antal skapade, dedupe, idempotens).
|
||||||
|
|
||||||
|
### Flutter widget/integration
|
||||||
|
- Flyer:
|
||||||
|
- render av `Visa flyer` efter restore,
|
||||||
|
- edit-dialog uppdaterar rad,
|
||||||
|
- `Planera X markerade` ger förväntad feedback.
|
||||||
|
- Inköpslista:
|
||||||
|
- ny flik syns,
|
||||||
|
- mottar rader från flyer.
|
||||||
|
- Inventory regression:
|
||||||
|
- kategori visas från product category path när satt,
|
||||||
|
- `Övrigt` endast fallback.
|
||||||
|
|
||||||
|
### E2E-checklista
|
||||||
|
- Importera flyer PDF -> öppna PDF -> redigera 2 rader -> planera markerade -> verifiera Inköpslista-fliken.
|
||||||
|
- Starta om app -> återöppna samma flyer-PDF -> verifiera ändringar kvar.
|
||||||
|
- Kontrollera inventory/pantry-kategorier efter backfill.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Leveransordning (rekommenderad)
|
||||||
|
1. Inventory-kategori bugfix + backfill (snabbt värde, hög påverkan).
|
||||||
|
2. Flyer item-redigering (namn/kategori).
|
||||||
|
3. Inköpslista-flik + backend-mappning från flyer.
|
||||||
|
4. Beständig flyer-PDF source-visning (kan byggas parallellt med 2/3 om backendkapacitet finns).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risker och mitigering
|
||||||
|
- **Datamigrering/backfill-risk**: kör först mot staging + logga träffsäkerhet och antal fallback till `Övrigt`.
|
||||||
|
- **Dubbelposter i inköpslista**: inför tydlig dedupe-regel och testfall.
|
||||||
|
- **PDF-hantering per plattform**: behåll web-first öppning och tydligt fallback-meddelande där native viewer saknas.
|
||||||
|
- **Prestanda vid stora flyers**: paginera/virtuallista om UI blir tungt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- Flyer-vyn har fungerande: PDF-visning, redigering av namn/kategori, planering till inköpslista.
|
||||||
|
- Inköpslista finns som separat flik och visar planerade flyer-rader.
|
||||||
|
- Inventory/pantry kategoriserar korrekt och `Övrigt` används endast som verklig fallback.
|
||||||
|
- Nya backend- och Flutter-tester gröna.
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
# Plan: Förbättrad dokumentationsstruktur för Recipe-app
|
||||||
|
|
||||||
|
## Bakgrund
|
||||||
|
Projektet har idag **20+ .md-filer** spridda över olika mappar, inklusive:
|
||||||
|
- **Aktiva filer** i rotmappen (t.ex. `TEKNISK_BESKRIVNING.md`, `NEXT_STEPS.md`).
|
||||||
|
- **Arkiverade filer** i `_archive/docs/` (t.ex. session-specifika anteckningar, gamla refaktoringsplaner).
|
||||||
|
- **Fragmenterade guider** (t.ex. `flyerimporter.md`, `filanalys.md`).
|
||||||
|
- **GitHub-specifika filer** (t.ex. `.github/copilot-instructions.md`).
|
||||||
|
|
||||||
|
**Problem**:
|
||||||
|
- **Svårt att hitta information**: Relaterat innehåll är splittrat (t.ex. Flutter-dokumentation i `_archive/docs/flutter/` vs. backend-dokumentation i rotmappen).
|
||||||
|
- **Föråldrat innehåll**: Arkiverade filer blandas med aktiva (t.ex. `SESSION_CHECKPOINT_2026-05-12.md`).
|
||||||
|
- **Icke-optimerat för språkmodeller**: Saknar tydlig hierarki och kontextuella länkar.
|
||||||
|
- **Duplicering**: Samma koncept (t.ex. "kategoriträd") dokumenteras på flera ställen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mål med ny struktur
|
||||||
|
1. **Användarvänligt för utvecklare**:
|
||||||
|
- Logisk gruppering av innehåll (t.ex. "Backend", "Flutter", "Deploy").
|
||||||
|
- Tydliga **steg-för-steg-guider** för vanliga arbetsflöden.
|
||||||
|
- **Sökbart** med tydliga rubriker och nyckelord.
|
||||||
|
|
||||||
|
2. **Optimerat för språkmodeller (LLMs):
|
||||||
|
- **Kontextuell sammanhang**: Varje fil innehåller tillräcklig bakgrund för att förstå dess syfte.
|
||||||
|
- **Länkat innehåll**: Korsreferenser mellan filer för att ge fullständig bild.
|
||||||
|
- **Strukturera data**: Använd tabeller, listor och diagram för att göra informationen maskinläsbar.
|
||||||
|
|
||||||
|
3. **Underhållbart**:
|
||||||
|
- **Versionerat**: Tydlig separation mellan aktiv dokumentation och arkiv.
|
||||||
|
- **Modulärt**: Uppdateringar i en modul (t.ex. "Databas") påverkar inte andra.
|
||||||
|
- **Automatiseringsvänligt**: Filer som `CONTRIBUTING.md` och `README.md` kan genereras delvis från andra källor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Föreslagen struktur
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── docs/ # **Huvudkatalog för ALL aktiv dokumentation**
|
||||||
|
│ ├── 01-overview/ # Övergripande projektbeskrivning
|
||||||
|
│ │ ├── README.md # Huvudsaklig README (ersätter rot-README.md)
|
||||||
|
│ │ ├── ARCHITECTURE.md # Systemarkitektur (flytta från TEKNISK_BESKRIVNING.md)
|
||||||
|
│ │ └── GLOSSARY.md # Termer och definitioner (t.ex. "kategoriträd", "flyer session")
|
||||||
|
│ │
|
||||||
|
│ ├── 02-setup/ # Installation och konfiguration
|
||||||
|
│ │ ├── INSTALL.md # Miljökrav, beroenden, första uppstart
|
||||||
|
│ │ ├── CONFIG.md # Konfigurationsfiler (.env, Docker, etc.)
|
||||||
|
│ │ └── TROUBLESHOOTING.md # Vanliga problem och lösningar
|
||||||
|
│ │
|
||||||
|
│ ├── 03-development/ # Utvecklingsguider
|
||||||
|
│ │ ├── CONTRIBUTING.md # Bidragsregler, kodstandard, PR-process
|
||||||
|
│ │ ├── WORKFLOWS.md # Git-flöden, branch-strategi, CI/CD
|
||||||
|
│ │ ├── DATABASE.md # Schema, migrationer, seedning (flytta från TEKNISK_BESKRIVNING.md)
|
||||||
|
│ │ ├── API.md # Backend-API:er, Swagger-länkar, exempelanrop
|
||||||
|
│ │ ├── FLUTTER.md # Flutter-specifik dokumentation (flytta från _archive/docs/flutter/)
|
||||||
|
│ │ └── MICROSERVICES.md # Importer-AI, Todo-microservice, etc.
|
||||||
|
│ │
|
||||||
|
│ ├── 04-deploy/ # Driftsättning och underhåll
|
||||||
|
│ │ ├── DEPLOY.md # Steg-för-steg deploy (ersätter delar av TEKNISK_BESKRIVNING.md)
|
||||||
|
│ │ ├── MAINTENANCE.md # Underhållsskript, backup, monitorering
|
||||||
|
│ │ └── SCALING.md # Prestanda, skalning, lasttest
|
||||||
|
│ │
|
||||||
|
│ ├── 05-features/ # Djupdyk i funktioner
|
||||||
|
│ │ ├── RECIPE_IMPORT.md # Kvittosimport (ersätter flyerimporter.md)
|
||||||
|
│ │ ├── CATEGORY_TREE.md # Kategorihantering och L3-integration
|
||||||
|
│ │ ├── SHOPPING_LIST.md # Inköpslistor och flyer-integration
|
||||||
|
│ │ └── ... # Övriga funktioner (t.ex. måltidsplanering)
|
||||||
|
│ │
|
||||||
|
│ └── 06-archive/ # **Arkiverade dokument** (flytta hit från _archive/)
|
||||||
|
│ ├── sessions/ # Gamla sessionsanteckningar
|
||||||
|
│ ├── legacy/ # Föråldrade planer (t.ex. RECIPE_IMPORT_REFACTOR_PLAN.md)
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
├── .github/ # GitHub-specifika filer
|
||||||
|
│ ├── COPILOT_INSTRUCTIONS.md # Flytta hit från .github/copilot-instructions.md
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
└── ... # Övriga projektfiler (backend/, flutter/, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detaljerad filstruktur och innehåll
|
||||||
|
|
||||||
|
### 1. `docs/01-overview/`
|
||||||
|
| Fil | Syfte | Källor (befintliga filer) |
|
||||||
|
|-------------------|-----------------------------------------------------------------------|---------------------------------------------------|
|
||||||
|
| `README.md` | **Huvudsaklig ingress**: Projektbeskrivning, mål, snabbstart, länkar. | `README.md`, delar av `TEKNISK_BESKRIVNING.md` |
|
||||||
|
| `ARCHITECTURE.md` | **Systemarkitektur**: Komponentdiagram, databasrelationer, flöden. | `TEKNISK_BESKRIVNING.md` (avsnitt om arkitektur) |
|
||||||
|
| `GLOSSARY.md` | **Ordförklaringar**: Definitioner av projekt-specifika termer. | Ny fil |
|
||||||
|
|
||||||
|
**Exempelinnehåll för `ARCHITECTURE.md`**:
|
||||||
|
```markdown
|
||||||
|
# Systemarkitektur
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Flutter App] -->|HTTP/REST| B[Backend API]
|
||||||
|
B -->|Prisma Client| C[MariaDB]
|
||||||
|
B -->|gRPC| D[Importer Microservice]
|
||||||
|
D -->|HTTP| E[Externa API:er]
|
||||||
|
C -->|Seed| F[Initial Data]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Komponenter
|
||||||
|
1. **Flutter App**:
|
||||||
|
- State management: Riverpod
|
||||||
|
- UI: Material Design + anpassade widgets
|
||||||
|
- Integreringar: Kamera (kvittoscan), PDF-rendering
|
||||||
|
|
||||||
|
2. **Backend API**:
|
||||||
|
- Ramverk: NestJS
|
||||||
|
- Databas: Prisma + MariaDB
|
||||||
|
- Autentisering: JWT
|
||||||
|
|
||||||
|
3. **Microservices**:
|
||||||
|
- Importer: Node.js + Puppeteer (för kvittosimport)
|
||||||
|
- AI: Python + Mistral (för kategorisering)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `docs/02-setup/`
|
||||||
|
| Fil | Syfte | Källor |
|
||||||
|
|-------------------|-----------------------------------------------------------------------|----------------------------------------------|
|
||||||
|
| `INSTALL.md` | **Miljösetup**: Krav, beroenden, första körning. | Delar av `README.md` och `TEKNISK_BESKRIVNING.md` |
|
||||||
|
| `CONFIG.md` | **Konfiguration**: `.env`-variabler, Docker-compose, nätverk. | `TEKNISK_BESKRIVNING.md` (avsnitt om miljö) |
|
||||||
|
| `TROUBLESHOOTING.md` | **Felsökning**: Vanliga fel, lösningar, debug-tips. | Ny fil |
|
||||||
|
|
||||||
|
**Exempelinnehåll för `INSTALL.md`**:
|
||||||
|
```markdown
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
## Krav
|
||||||
|
| Komponent | Version | Notering |
|
||||||
|
|-----------------|---------------|-----------------------------------|
|
||||||
|
| Node.js | 24.15.0 | LTS-version rekommenderas |
|
||||||
|
| Docker | 24.x | Krävs för databas och microservices|
|
||||||
|
| Flutter | 3.41.9 | Kanal: stable |
|
||||||
|
| MariaDB | 11.x | Inkluderas via Docker-compose |
|
||||||
|
|
||||||
|
## Steg-för-steg
|
||||||
|
1. **Klona repo**:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/recipe-app/recipe-app.git
|
||||||
|
cd recipe-app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Konfigurera miljö**:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Redigera .env (se CONFIG.md för detaljer)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Starta tjänster**:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. `docs/03-development/`
|
||||||
|
| Fil | Syfte | Källor |
|
||||||
|
|-------------------|-----------------------------------------------------------------------|----------------------------------------------|
|
||||||
|
| `CONTRIBUTING.md` | **Bidragsregler**: Kodstandard, PR-process, testkrav. | Ny fil (inspirerad av GitHub-standard) |
|
||||||
|
| `WORKFLOWS.md` | **Arbetsflöden**: Git-strategi, CI/CD, release-process. | `TEKNISK_BESKRIVNING.md` (avsnitt om Git) |
|
||||||
|
| `DATABASE.md` | **Databas**: Schema, migrationer, seedning, underhållsskript. | `TEKNISK_BESKRIVNING.md` + nya avsnitt |
|
||||||
|
| `API.md` | **Backend-API**: Endpoints, autentisering, exempelanrop. | Ny fil |
|
||||||
|
| `FLUTTER.md` | **Flutter-utveckling**: Widget-träd, state management, teman. | `_archive/docs/flutter/` |
|
||||||
|
| `MICROSERVICES.md`| **Microservices**: Importer, AI, kommunikation med backend. | Ny fil |
|
||||||
|
|
||||||
|
**Exempelinnehåll för `DATABASE.md`**:
|
||||||
|
```markdown
|
||||||
|
# Databas
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
User ||--o{ Recipe : creates
|
||||||
|
User ||--o{ InventoryItem : owns
|
||||||
|
Category ||--o{ Product : "L3"
|
||||||
|
FlyerSession ||--o{ FlyerItem : contains
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migrationer
|
||||||
|
### Standardflöde
|
||||||
|
1. Uppdatera `prisma/schema.prisma`.
|
||||||
|
2. Skapa migration:
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev --name add_feature_x
|
||||||
|
```
|
||||||
|
3. Testa lokalt:
|
||||||
|
```bash
|
||||||
|
npx prisma migrate reset
|
||||||
|
npx prisma db seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Underhållsskript
|
||||||
|
- **Rensa databas** (behåll kategorier):
|
||||||
|
```bash
|
||||||
|
./deploy.sh --clean-database
|
||||||
|
```
|
||||||
|
> Obs! Uppdatera `prisma/maintenance/clean-database.sql` när nya tabeller läggs till.
|
||||||
|
|
||||||
|
## Seedning
|
||||||
|
- **Initial data**: Laddas från `db/seeds/seed_all.sql`.
|
||||||
|
- **Kör seed**:
|
||||||
|
```bash
|
||||||
|
./deploy.sh --seed
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. `docs/04-deploy/`
|
||||||
|
| Fil | Syfte | Källor |
|
||||||
|
|-------------------|-----------------------------------------------------------------------|----------------------------------------------|
|
||||||
|
| `DEPLOY.md` | **Deploy-guider**: Steg-för-steg för staging/produktion. | `TEKNISK_BESKRIVNING.md` (avsnitt om deploy) |
|
||||||
|
| `MAINTENANCE.md` | **Underhåll**: Backup, monitorering, logghantering. | Ny fil |
|
||||||
|
| `SCALING.md` | **Skalning**: Prestandaoptimering, lasttest, caching. | Ny fil |
|
||||||
|
|
||||||
|
**Exempelinnehåll för `DEPLOY.md`**:
|
||||||
|
```markdown
|
||||||
|
# Driftsättning
|
||||||
|
|
||||||
|
## Miljöer
|
||||||
|
| Miljö | Domän | Syfte |
|
||||||
|
|-------------|---------------------|--------------------------------|
|
||||||
|
| Lokal | localhost:8080 | Utveckling |
|
||||||
|
| Staging | staging.app.com | Test перед prod |
|
||||||
|
| Produktion | app.recipe.com | Live-trafik |
|
||||||
|
|
||||||
|
## Steg-för-steg
|
||||||
|
1. **Bygg och pusha images**:
|
||||||
|
```bash
|
||||||
|
docker compose build
|
||||||
|
docker compose push
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Kör migrationer**:
|
||||||
|
```bash
|
||||||
|
./deploy.sh --backend --migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Starta tjänster**:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vanliga flaggor
|
||||||
|
| Flagga | Beskrivning |
|
||||||
|
|-------------------|----------------------------------------------|
|
||||||
|
| `--migrate` | Kör Prisma-migrationer. |
|
||||||
|
| `--clean-database`| Rensar data (behåller kategorier). |
|
||||||
|
| `--seed` | Laddar initial data. |
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. `docs/05-features/`
|
||||||
|
| Fil | Syfte | Källor |
|
||||||
|
|-------------------|-----------------------------------------------------------------------|----------------------------------------------|
|
||||||
|
| `RECIPE_IMPORT.md`| **Kvittosimport**: Flyer-parsing, AI-kategorisering, lagring. | `flyerimporter.md` |
|
||||||
|
| `CATEGORY_TREE.md`| **Kategoriträd**: L3-integration, hierarki, synkronisering. | Delar av `TEKNISK_BESKRIVNING.md` |
|
||||||
|
| `SHOPPING_LIST.md`| **Inköpslistor**: Flyer-integration, kvantitetsberäkning, delning. | Ny fil |
|
||||||
|
|
||||||
|
**Exempelinnehåll för `RECIPE_IMPORT.md`**:
|
||||||
|
```markdown
|
||||||
|
# Kvittosimport
|
||||||
|
|
||||||
|
## Flöde
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[Ladda upp PDF] --> B[Extrahera text]
|
||||||
|
B --> C[AI-kategorisering]
|
||||||
|
C --> D[Spara som FlyerSession]
|
||||||
|
D --> E[Mappa till inköpslista]
|
||||||
|
```
|
||||||
|
|
||||||
|
## API-endpoints
|
||||||
|
| Endpoint | Metod | Beskrivning |
|
||||||
|
|-----------------------------------|-------|--------------------------------------|
|
||||||
|
| `/api/flyer-sessions` | POST | Ladda upp och parsa PDF. |
|
||||||
|
| `/api/flyer-sessions/:id/items` | PATCH | Uppdatera produktnamn/kategori. |
|
||||||
|
| `/api/shopping-list/from-flyer` | POST | Konvertera flyer till inköpslista. |
|
||||||
|
|
||||||
|
## Underhåll
|
||||||
|
- **Uppdatera AI-modell**: Se `MICROSERVICES.md`.
|
||||||
|
- **Lägg till nya butiker**: Uppdatera `src/flyer-import/parsers/`.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. `docs/06-archive/`
|
||||||
|
- **Syfte**: Flytta alla föråldrade filer hit, organiserade efter typ:
|
||||||
|
```
|
||||||
|
docs/06-archive/
|
||||||
|
├── sessions/ # Gamla sessionscheckpoints
|
||||||
|
│ ├── SESSION_2026-05-09_RECEIPT_IMPORT.md
|
||||||
|
│ └── ...
|
||||||
|
├── legacy/ # Föråldrade planer
|
||||||
|
│ ├── RECIPE_IMPORT_REFACTOR_PLAN.md
|
||||||
|
│ └── ...
|
||||||
|
└── flutter_legacy/ # Gamla Flutter-dokument
|
||||||
|
├── teknisk_beskrivning_flutter.md
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migrationsplan
|
||||||
|
|
||||||
|
### Steg 1: Rensa och organisera befintliga filer
|
||||||
|
| Åtgärd | Källfil(er) | Målfil |
|
||||||
|
|---------------------------------|---------------------------------------------|-----------------------------------------|
|
||||||
|
| Flytta och uppdatera | `TEKNISK_BESKRIVNING.md` | `docs/01-overview/ARCHITECTURE.md` |
|
||||||
|
| | `TEKNISK_BESKRIVNING.md` (deploy-avsnitt) | `docs/04-deploy/DEPLOY.md` |
|
||||||
|
| | `TEKNISK_BESKRIVNING.md` (databas-avsnitt) | `docs/03-development/DATABASE.md` |
|
||||||
|
| Flytta och slå samman | `flyerimporter.md` | `docs/05-features/RECIPE_IMPORT.md` |
|
||||||
|
| | `_archive/docs/flutter/*` | `docs/03-development/FLUTTER.md` |
|
||||||
|
| Arkivera | `_archive/docs/SESSION_*.md` | `docs/06-archive/sessions/` |
|
||||||
|
| | `MVP_CHECKLISTA.md` | `docs/06-archive/legacy/` |
|
||||||
|
| Uppdatera och flytta | `README.md` | `docs/01-overview/README.md` |
|
||||||
|
| | `.github/copilot-instructions.md` | `.github/COPILOT_INSTRUCTIONS.md` |
|
||||||
|
|
||||||
|
### Steg 2: Skapa nya filer
|
||||||
|
| Fil | Syfte |
|
||||||
|
|-----------------------------|-----------------------------------------------------------------------|
|
||||||
|
| `docs/03-development/CONTRIBUTING.md` | Standardiserade bidragsregler (branches, PR, code review). |
|
||||||
|
| `docs/03-development/API.md` | Dokumentation av backend-API:er (OpenAPI/Swagger-länkar). |
|
||||||
|
| `docs/03-development/MICROSERVICES.md` | Beskrivning av microservices (Importer, AI). |
|
||||||
|
| `docs/05-features/CATEGORY_TREE.md` | Djupdyk i kategorihantering och L3-integration. |
|
||||||
|
| `docs/04-deploy/MAINTENANCE.md` | Backup, monitorering, logghantering. |
|
||||||
|
|
||||||
|
### Steg 3: Lägg till kontext för språkmodeller
|
||||||
|
- **Länka filer**: Korsreferenser mellan relaterade ämnen (t.ex. länka från `DATABASE.md` till `DEPLOY.md` för migrationssteg).
|
||||||
|
- **Metadatarubriker**: Lägg till `## Syfte` och `## Målgrupp` i varje fil.
|
||||||
|
- **Diagram**: Använd Mermaid för att visualisera flöden och relationer.
|
||||||
|
- **Exempel**: Inkludera kopierbara kodblock för vanliga operationer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fördelar med den nya strukturen
|
||||||
|
|
||||||
|
### För utvecklare
|
||||||
|
✅ **Lätt att hitta**: Logisk gruppering (t.ex. all Flutter-dokumentation på ett ställe).
|
||||||
|
✅ **Uppdaterad**: Arkiverade filer separerade från aktiv dokumentation.
|
||||||
|
✅ **Steg-för-steg**: Guider med tydliga exempel (t.ex. `INSTALL.md`).
|
||||||
|
|
||||||
|
### För språkmodeller (LLMs)
|
||||||
|
🤖 **Kontextuell förståelse**:
|
||||||
|
- Varje fil har ett tydligt syfte och målgrupp.
|
||||||
|
- Korsreferenser ger fullständig bild (t.ex. länka från `ARCHITECTURE.md` till `DATABASE.md`).
|
||||||
|
- Diagram och tabeller gör informationen maskinläsbar.
|
||||||
|
|
||||||
|
📚 **Sökbarhet**:
|
||||||
|
- Hierarkisk struktur (`01-overview/`, `02-setup/`, etc.) underlättar navigering.
|
||||||
|
- Nyckelord i rubriker (t.ex. "Flutter State Management" i `FLUTTER.md`).
|
||||||
|
|
||||||
|
🔗 **Länkat innehåll**:
|
||||||
|
- Relaterade ämnen länkas explicit (t.ex. "Se [DATABASE.md](../03-development/DATABASE.md) för schema").
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nästa steg
|
||||||
|
1. **Godkänn planen**: Bekräfta att den föreslagna strukturen uppfyller kraven.
|
||||||
|
2. **Prioritera filer**: Vilka filer ska migreras först? (t.ex. `TEKNISK_BESKRIVNING.md` → flera nya filer).
|
||||||
|
3. **Implementera**: Skapa den nya strukturen och flytta innehåll stegvis.
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Plan: Separat Admin-AI huvudtabb + AI-insyn för Kvitto/Flyer
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
- Flytta ut `AI` från `Admin > Databas` till en egen huvudtabb i admin (samma nivå som `Users` och `Database`).
|
||||||
|
- På nya `AI`-fliken visa två underflikar:
|
||||||
|
- `Kvitto` (AI-funktioner för receipt-import)
|
||||||
|
- `Flyer` (AI-funktioner för flyer-import)
|
||||||
|
- Ge admin insyn i:
|
||||||
|
- Prompt (vad modellen fick)
|
||||||
|
- Output (vad modellen returnerade)
|
||||||
|
- Per import/sessionshistorik
|
||||||
|
|
||||||
|
## Nuvarande läge (verifierat)
|
||||||
|
- Huvudtabbar i admin definieras i:
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_screen.dart`
|
||||||
|
- `flutter/lib/core/ui/app_shell.dart`
|
||||||
|
- `AI` ligger idag som intern databas-tab i:
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_database_panel.dart`
|
||||||
|
- Nuvarande `AdminAiPanel` visar bara modellinfo:
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_ai_panel.dart`
|
||||||
|
- Flyer har sessions-API och sparar parsat resultat, men ingen API-yta för prompt/output:
|
||||||
|
- `backend/src/flyer-import/flyer-import.controller.ts`
|
||||||
|
- `backend/src/flyer-import/dto/flyer-import.response.ts`
|
||||||
|
- Receipt-import har ingen sessionhistorik i recipe-api och ingen prompt/output i svar:
|
||||||
|
- `backend/src/receipt-import/receipt-import.controller.ts`
|
||||||
|
- `backend/src/receipt-import/dto/parsed-receipt-item.dto.ts`
|
||||||
|
|
||||||
|
## UX-förslag (enkelt och snyggt)
|
||||||
|
- Ny huvudflik `AI` i admin med tydlig struktur:
|
||||||
|
- Topp: två chips/segmenterade knappar `Kvitto` och `Flyer`
|
||||||
|
- Innehåll: 2-kolumnslayout på desktop, enkel stack på mobil
|
||||||
|
- Vänster: lista med senaste importer (tid, användare, fil, status)
|
||||||
|
- Höger: detaljer för vald import
|
||||||
|
- Detaljvyn har tre kort:
|
||||||
|
1. **Prompt** (monospace, copy-knapp, expand/collapse)
|
||||||
|
2. **Model Output** (formatterad JSON, copy-knapp)
|
||||||
|
3. **Sammanfattning** (modell, duration, chunk/retry, warnings)
|
||||||
|
- Filter högst upp:
|
||||||
|
- Källa: Kvitto/Flyer
|
||||||
|
- Period: senaste 24h / 7d / 30d
|
||||||
|
- Endast fel
|
||||||
|
- Default-beteende:
|
||||||
|
- Välj senaste import automatiskt
|
||||||
|
- Visa läs-only data (ingen redigering av prompt i denna iteration)
|
||||||
|
|
||||||
|
## Föreslagen implementation
|
||||||
|
|
||||||
|
### 1) Flutter: ny Admin-huvudtabb AI
|
||||||
|
1. Utöka enum och query-hantering:
|
||||||
|
- `AdminViewTab` får `ai`
|
||||||
|
- query-stöd: `?tab=ai`
|
||||||
|
2. Uppdatera admin-title chips i `AppShell`:
|
||||||
|
- Lägg till `AI` bredvid `Användare` och `Databas`
|
||||||
|
3. Rendra ny panel i `AdminScreen`:
|
||||||
|
- `AdminAiPanel` blir panel för huvudtabben
|
||||||
|
4. Ta bort AI från `AdminDatabasePanel` interna tabs
|
||||||
|
|
||||||
|
Berörda filer:
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_screen.dart`
|
||||||
|
- `flutter/lib/core/ui/app_shell.dart`
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_database_panel.dart`
|
||||||
|
|
||||||
|
### 2) Flutter: bygg om `AdminAiPanel` till AI-observability
|
||||||
|
1. Inför underflikar `Kvitto` / `Flyer`
|
||||||
|
2. Lägg till vänster lista + höger detalj
|
||||||
|
3. Lägg till komponenter:
|
||||||
|
- `PromptCard` (text/copy)
|
||||||
|
- `OutputJsonCard` (pretty JSON/copy)
|
||||||
|
- `TraceMetaCard` (modell, tid, status)
|
||||||
|
4. Lägg till `adminRepository`-metoder för att hämta traces
|
||||||
|
|
||||||
|
Berörda filer:
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_ai_panel.dart`
|
||||||
|
- `flutter/lib/features/admin/data/admin_repository.dart`
|
||||||
|
- `flutter/lib/core/api/api_paths.dart`
|
||||||
|
- ev. nya domänmodeller under `flutter/lib/features/admin/domain/`
|
||||||
|
|
||||||
|
### 3) Backend: exponera AI trace-data (admin-only)
|
||||||
|
1. Lägg till admin-endpoints för trace-lista + detalj
|
||||||
|
2. Returnera prompt/output samt metadata
|
||||||
|
3. Begränsa åtkomst till admin
|
||||||
|
|
||||||
|
Föreslagen API-yta:
|
||||||
|
- `GET /ai/traces?source=receipt|flyer&limit=...&cursor=...`
|
||||||
|
- `GET /ai/traces/:id`
|
||||||
|
|
||||||
|
### 4) Datalagring för prompt/output
|
||||||
|
För stabil UX behövs persistens, inte bara loggar.
|
||||||
|
|
||||||
|
Föreslagen modell:
|
||||||
|
- Ny Prisma-tabell `AiTrace`
|
||||||
|
- `id`, `source` (`receipt`/`flyer`), `userId`, `sessionId?`, `model`, `prompt`, `rawOutput`, `normalizedOutput`, `status`, `error`, `durationMs`, `createdAt`
|
||||||
|
|
||||||
|
Integration:
|
||||||
|
- Flyer: skapa trace i `AiFlyerParserService` vid varje AI-anrop/chunk (sammanfatta till en sessionsrad eller flera child-rader)
|
||||||
|
- Receipt: recipe-api behöver trace från importer-service eller egen instrumentation av prompt/output
|
||||||
|
|
||||||
|
## Viktig teknisk avgränsning (receipt)
|
||||||
|
- I nuvarande repo byggs flyer-prompt i `recipe-api` och är enkel att visa.
|
||||||
|
- Receipt-AI sker i importer-flöde; prompt/output finns sannolikt inte i recipe-api idag.
|
||||||
|
- Därför två realistiska steg:
|
||||||
|
1. **Steg 1 (snabbt):** full trace-visning för Flyer + modellinfo för Kvitto
|
||||||
|
2. **Steg 2:** utöka receipt/importer så prompt/output skickas eller lagras som trace och visas i samma UI
|
||||||
|
|
||||||
|
## Säkerhet
|
||||||
|
- Endast admin får läsa traces.
|
||||||
|
- Prompt/output kan innehålla känslig text från uppladdade filer:
|
||||||
|
- visa som read-only
|
||||||
|
- möjlighet till maskning av persondata i senare steg
|
||||||
|
- paginering + kort retention (t.ex. 30 dagar) rekommenderas
|
||||||
|
|
||||||
|
## Testplan
|
||||||
|
- Flutter widget-test:
|
||||||
|
- Ny huvudtabb `AI` syns och route `?tab=ai` fungerar
|
||||||
|
- Underflikar `Kvitto`/`Flyer` växlar korrekt
|
||||||
|
- Prompt/output renderas och copy-knappar fungerar
|
||||||
|
- Backend tester:
|
||||||
|
- admin-only behörighet på trace-endpoints
|
||||||
|
- lista/detalj svarar korrekt
|
||||||
|
- trace skapas för flyer parse-flöde
|
||||||
|
- Regression:
|
||||||
|
- Admin `Users`/`Database` fungerar oförändrat
|
||||||
|
|
||||||
|
## Genomförandeordning
|
||||||
|
1. Flytta AI till huvudtabb i Flutter (utan backend-ändring först)
|
||||||
|
2. Bygg ny AI-panelstruktur med underflikar
|
||||||
|
3. Inför backend trace-endpoints + Prisma-migration
|
||||||
|
4. Koppla Flyer trace end-to-end
|
||||||
|
5. Koppla Receipt trace (beroende på importer-instrumentation)
|
||||||
|
6. Sluttest + docs
|
||||||
|
|
||||||
|
## Rekommenderat beslut inför implementation
|
||||||
|
- Implementera i två faser för låg risk:
|
||||||
|
- Fas A: Ny UX + full Flyer-insyn direkt
|
||||||
|
- Fas B: Receipt prompt/output när importer-trace är tillgänglig
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
# Plan: Implementera automatiserad datarensning för AiTrace
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
Implementera en automatiserad rensning av gamla `AiTrace`-poster för att säkerställa att känsliga data inte lagras längre än nödvändigt och för att följa GDPR-krav.
|
||||||
|
|
||||||
|
## Bakgrund
|
||||||
|
- `AiTrace`-tabellen lagrar känsliga data (t.ex. maskerade promptar och AI-svar) utan någon retention-policy.
|
||||||
|
- Risk för att data ackumuleras obehindrat, vilket kan leda till lagringsproblem och GDPR-brott.
|
||||||
|
- Enligt GDPR bör känsliga data rensas efter en viss tid om de inte längre behövs för felsökning eller analys.
|
||||||
|
|
||||||
|
## Krav
|
||||||
|
1. Rensa `AiTrace`-poster äldre än 30 dagar.
|
||||||
|
2. Schemalägg rensningen att köra dagligen vid midnatt.
|
||||||
|
3. Logga antalet rensade poster för övervakning.
|
||||||
|
4. Säkerställa att rensningen inte påverkar systemets prestanda.
|
||||||
|
|
||||||
|
## Implementeringsplan
|
||||||
|
|
||||||
|
### Steg 1: Installera beroenden
|
||||||
|
Installera `NestJS Schedule`-modulen för att möjliggöra schemalagda jobb.
|
||||||
|
|
||||||
|
**Kommando:**
|
||||||
|
```bash
|
||||||
|
npm install @nestjs/schedule
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 2: Konfigurera ScheduleModule
|
||||||
|
Lägg till `ScheduleModule` i `AppModule` för att aktivera schemalagda jobb.
|
||||||
|
|
||||||
|
**Fil:** `backend/src/app.module.ts`
|
||||||
|
**Ändring:**
|
||||||
|
```typescript
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ScheduleModule.forRoot()],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 3: Skapa AiTraceCleanupService
|
||||||
|
Skapa en ny tjänst för att hantera rensningen av `AiTrace`-poster.
|
||||||
|
|
||||||
|
**Fil:** `backend/src/ai/ai-trace-cleanup.service.ts`
|
||||||
|
**Innehåll:**
|
||||||
|
```typescript
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AiTraceCleanupService {
|
||||||
|
private readonly logger = new Logger(AiTraceCleanupService.name);
|
||||||
|
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
||||||
|
async cleanupOldTraces() {
|
||||||
|
this.logger.log('Starting cleanup of old AiTrace records...');
|
||||||
|
const thirtyDaysAgo = new Date();
|
||||||
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
|
|
||||||
|
const result = await this.prisma.aiTrace.deleteMany({
|
||||||
|
where: {
|
||||||
|
createdAt: {
|
||||||
|
lt: thirtyDaysAgo,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log(`Cleaned up ${result.count} old AiTrace records.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 4: Registrera AiTraceCleanupService
|
||||||
|
Lägg till `AiTraceCleanupService` i `AiTraceModule` för att aktivera tjänsten.
|
||||||
|
|
||||||
|
**Fil:** `backend/src/ai/ai-trace.module.ts`
|
||||||
|
**Ändring:**
|
||||||
|
```typescript
|
||||||
|
import { AiTraceCleanupService } from './ai-trace-cleanup.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [AiTraceService, AiTraceCleanupService],
|
||||||
|
})
|
||||||
|
export class AiTraceModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 5: Testa rensningen manuellt
|
||||||
|
Skapa en testmetod för att manuellt köra rensningen och verifiera att den fungerar som förväntat.
|
||||||
|
|
||||||
|
**Fil:** `backend/src/ai/ai-trace.controller.ts`
|
||||||
|
**Ändring:**
|
||||||
|
```typescript
|
||||||
|
import { AiTraceCleanupService } from './ai-trace-cleanup.service';
|
||||||
|
|
||||||
|
@Controller('ai/traces')
|
||||||
|
export class AiTraceController {
|
||||||
|
constructor(
|
||||||
|
private readonly aiTraceService: AiTraceService,
|
||||||
|
private readonly aiTraceCleanupService: AiTraceCleanupService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('cleanup')
|
||||||
|
@Roles('admin')
|
||||||
|
async manualCleanup() {
|
||||||
|
await this.aiTraceCleanupService.cleanupOldTraces();
|
||||||
|
return { success: true, message: 'Manual cleanup completed.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 6: Verifiera implementationen
|
||||||
|
1. Skapa några testposter i `AiTrace`-tabellen med olika `createdAt`-datum.
|
||||||
|
2. Kör den manuella rensningen via `POST /api/ai/traces/cleanup`.
|
||||||
|
3. Verifiera att endast poster äldre än 30 dagar har rensats.
|
||||||
|
4. Kontrollera loggarna för att säkerställa att antalet rensade poster loggas korrekt.
|
||||||
|
|
||||||
|
### Steg 7: Dokumentera ändringarna
|
||||||
|
Uppdatera `TEKNISK_BESKRIVNING.md` med information om den automatiserade rensningen.
|
||||||
|
|
||||||
|
**Fil:** `TEKNISK_BESKRIVNING.md`
|
||||||
|
**Ändring:**
|
||||||
|
```markdown
|
||||||
|
## Automatiserad datarensning för AiTrace
|
||||||
|
|
||||||
|
För att säkerställa att känsliga data inte lagras längre än nödvändigt, har en automatiserad rensning implementerats:
|
||||||
|
|
||||||
|
- **Rensningsintervall**: Dagligen vid midnatt.
|
||||||
|
- **Retention-period**: 30 dagar.
|
||||||
|
- **Loggning**: Antalet rensade poster loggas för övervakning.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- **Tjänst**: `AiTraceCleanupService` i `backend/src/ai/ai-trace-cleanup.service.ts`.
|
||||||
|
- **Schemaläggning**: Använder `@nestjs/schedule` för att köra rensningen dagligen.
|
||||||
|
- **Manuell rensning**: Tillgänglig via `POST /api/ai/traces/cleanup` (kräver admin-behörighet).
|
||||||
|
|
||||||
|
### GDPR-efterlevnad
|
||||||
|
- Rensningen säkerställer att känsliga data raderas efter 30 dagar, vilket uppfyller GDPR-krav på dataminimering.
|
||||||
|
- Användare kan begära att deras data raderas tidigare via admin-panelen.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frågor och överväganden
|
||||||
|
1. **Retention-period**: Är 30 dagar en lämplig period, eller bör den justeras baserat påelsökningsbehov?
|
||||||
|
2. **Loggning**: Bör loggarna för rensningen sparas längre för revisionsändamål?
|
||||||
|
3. **Prestanda**: Bör rensningen köra under lågtrafikperioder för att minimera påverkan på systemet?
|
||||||
|
|
||||||
|
## Nästa steg
|
||||||
|
1. Implementera planen enligt ovan.
|
||||||
|
2. Testa rensningen i en testmiljö.
|
||||||
|
3. Verifiera att loggarna är korrekta och att rensningen fungerar som förväntat.
|
||||||
|
4. Dokumentera ändringarna i `TEKNISK_BESKRIVNING.md`.
|
||||||
|
5. Informera teamet om den nya funktionaliteten och dess påverkan på GDPR-efterlevnaden.
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
# Plan: Implementera användarinitierad radering av personuppgifter
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
Skapa en tydlig och stegvis plan för hur användare kan ta bort sina personuppgifter från sin profil på plattformen, inklusive teknisk implementation och användarflöde.
|
||||||
|
|
||||||
|
## Bakgrund
|
||||||
|
- GDPR kräver att användare ska kunna begära radering av sina personuppgifter.
|
||||||
|
- Nuvarande plattform saknar ett tydligt användarflöde för att initiera radering av personuppgifter.
|
||||||
|
- Användare bör kunna ta bort sin profil och associerade data på ett säkert och transparent sätt.
|
||||||
|
|
||||||
|
## Krav
|
||||||
|
1. Lägg till en "Ta bort min profil"-knapp i användarens profilinställningar.
|
||||||
|
2. Implementera en bekräftelsedialog för att förhindra oavsiktlig radering.
|
||||||
|
3. Skapa en backend-endpoint för att hantera raderingsbegäran.
|
||||||
|
4. Se till att all användardata (profil, produkter, recept, etc.) raderas eller anonymiseras.
|
||||||
|
5. Logga raderingsbegäran för revisionsändamål.
|
||||||
|
6. Skicka ett bekräftelsemeddelande till användaren efter radering.
|
||||||
|
|
||||||
|
## Implementeringsplan
|
||||||
|
|
||||||
|
### Steg 1: Lägg till "Ta bort min profil"-knapp i Flutter UI
|
||||||
|
Lägg till en knapp i användarens profilinställningar som låter användaren initiera radering av sin profil.
|
||||||
|
|
||||||
|
**Fil:** `flutter/lib/features/profile/presentation/profile_screen.dart`
|
||||||
|
**Ändring:**
|
||||||
|
```dart
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
_showDeleteProfileConfirmation();
|
||||||
|
},
|
||||||
|
child: Text('Ta bort min profil'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 2: Implementera bekräftelsedialog
|
||||||
|
Skapa en dialog som kräver att användaren bekräftar raderingen.
|
||||||
|
|
||||||
|
**Fil:** `flutter/lib/features/profile/presentation/profile_screen.dart`
|
||||||
|
**Ändring:**
|
||||||
|
```dart
|
||||||
|
Future<void> _showDeleteProfileConfirmation() async {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Bekräfta radering'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: ListBody(
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Är du säker på att du vill ta bort din profil?'),
|
||||||
|
Text('Alla dina data kommer att raderas permanent.'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
child: Text('Avbryt'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: Text('Ta bort'),
|
||||||
|
onPressed: () {
|
||||||
|
_deleteProfile();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 3: Skapa backend-endpoint för radering
|
||||||
|
Implementera en endpoint som hanterar raderingsbegäran och raderar all associerad data.
|
||||||
|
|
||||||
|
**Fil:** `backend/src/users/users.controller.ts`
|
||||||
|
**Ändring:**
|
||||||
|
```typescript
|
||||||
|
@Delete('me')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
async deleteProfile(@CurrentUser() user: UserEntity) {
|
||||||
|
await this.usersService.deleteUserAndData(user.id);
|
||||||
|
return { success: true, message: 'Din profil och data har tagits bort.' };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 4: Implementera raderingslogik i UsersService
|
||||||
|
Skapa en metod som raderar användaren och all associerad data.
|
||||||
|
|
||||||
|
**Fil:** `backend/src/users/users.service.ts`
|
||||||
|
**Ändring:**
|
||||||
|
```typescript
|
||||||
|
async deleteUserAndData(userId: number) {
|
||||||
|
await this.prisma.$transaction([
|
||||||
|
this.prisma.product.deleteMany({ where: { ownerId: userId } }),
|
||||||
|
this.prisma.recipe.deleteMany({ where: { ownerId: userId } }),
|
||||||
|
this.prisma.inventoryItem.deleteMany({ where: { userId } }),
|
||||||
|
this.prisma.mealPlanEntry.deleteMany({ where: { userId } }),
|
||||||
|
this.prisma.user.delete({ where: { id: userId } }),
|
||||||
|
]);
|
||||||
|
this.logger.log(`User ${userId} and associated data deleted.`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 5: Logga raderingsbegäran
|
||||||
|
Lägg till loggning för att spåra raderingsbegäran för revisionsändamål.
|
||||||
|
|
||||||
|
**Fil:** `backend/src/users/users.service.ts`
|
||||||
|
**Ändring:**
|
||||||
|
```typescript
|
||||||
|
async logDeletionRequest(userId: number, userEmail: string) {
|
||||||
|
await this.prisma.auditLog.create({
|
||||||
|
data: {
|
||||||
|
action: 'USER_DELETION',
|
||||||
|
userId,
|
||||||
|
email: userEmail,
|
||||||
|
metadata: { message: 'User initiated deletion of their profile and data.' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 6: Skicka bekräftelsemeddelande
|
||||||
|
Skicka ett e-postmeddelande till användaren för att bekräfta raderingen.
|
||||||
|
|
||||||
|
**Fil:** `backend/src/users/users.service.ts`
|
||||||
|
**Ändring:**
|
||||||
|
```typescript
|
||||||
|
async sendDeletionConfirmationEmail(email: string) {
|
||||||
|
await this.emailService.sendEmail({
|
||||||
|
to: email,
|
||||||
|
subject: 'Bekräftelse på radering av din profil',
|
||||||
|
text: 'Din profil och alla associerade data har tagits bort från vår plattform.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 7: Uppdatera Flutter för att anropa backend-endpoint
|
||||||
|
Implementera metoden för att anropa backend-endpoint för radering.
|
||||||
|
|
||||||
|
**Fil:** `flutter/lib/features/profile/presentation/profile_screen.dart`
|
||||||
|
**Ändring:**
|
||||||
|
```dart
|
||||||
|
Future<void> _deleteProfile() async {
|
||||||
|
try {
|
||||||
|
final response = await ApiService.delete('/users/me');
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Din profil har tagits bort.'));
|
||||||
|
);
|
||||||
|
Navigator.of(context).pushNamedAndRemoveUntil('/login', (route) => false);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Ett fel uppstod vid radering av din profil.'));
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 8: Testa implementeringen
|
||||||
|
1. Skapa en testanvändare i systemet.
|
||||||
|
2. Navigera till profilinställningar och klicka på "Ta bort min profil".
|
||||||
|
3. Bekräfta raderingen i dialogrutan.
|
||||||
|
4. Verifiera att användaren och all associerad data har tagits bort från databasen.
|
||||||
|
5. Kontrollera att ett bekräftelsemeddelande har skickats till användarens e-post.
|
||||||
|
6. Verifiera att raderingsbegäran har loggats i `AuditLog`.
|
||||||
|
|
||||||
|
### Steg 9: Dokumentera ändringarna
|
||||||
|
Uppdatera `TEKNISK_BESKRIVNING.md` med information om den nya funktionaliteten.
|
||||||
|
|
||||||
|
**Fil:** `TEKNISK_BESKRIVNING.md`
|
||||||
|
**Ändring:**
|
||||||
|
```markdown
|
||||||
|
## Användarinitierad radering av personuppgifter
|
||||||
|
|
||||||
|
För att uppfylla GDPR-krav har en funktion implementerats som låter användare ta bort sin profil och associerade data:
|
||||||
|
|
||||||
|
- **Användarflöde**: Användaren kan initiera radering via en knapp i profilinställningarna.
|
||||||
|
- **Bekräftelse**: En dialog kräver bekräftelse för att förhindra oavsiktlig radering.
|
||||||
|
- **Backend-endpoint**: `DELETE /users/me` hanterar raderingsbegäran.
|
||||||
|
- **Data som raderas**: Profil, produkter, recept, inventarieposter och matplaner.
|
||||||
|
- **Loggning**: Raderingsbegäran loggas i `AuditLog` för revisionsändamål.
|
||||||
|
- **Bekräftelse**: Ett e-postmeddelande skickas till användaren efter radering.
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
- **Frontend**: `flutter/lib/features/profile/presentation/profile_screen.dart`
|
||||||
|
- **Backend**: `backend/src/users/users.controller.ts` och `backend/src/users/users.service.ts`
|
||||||
|
- **Loggning**: `AuditLog`-poster skapas för varje raderingsbegäran.
|
||||||
|
- **E-postbekräftelse**: Skickas via `EmailService`.
|
||||||
|
|
||||||
|
### GDPR-efterlevnad
|
||||||
|
- Användare har full kontroll över sina personuppgifter.
|
||||||
|
- Raderingsprocessen är transparent och dokumenterad.
|
||||||
|
- All data raderas eller anonymiseras enligt GDPR-krav.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frågor och överväganden
|
||||||
|
1. **Dataretention**: Bör vissa data (t.ex. transaktionshistorik) sparas för revisionsändamål även efter radering?
|
||||||
|
2. **Anonymisering**: Bör vi anonymisera data istället för att radera dem helt?
|
||||||
|
3. **Ångerperiod**: Bör vi implementera en ångerperiod där användaren kan återställa sin profil inom en viss tid?
|
||||||
|
|
||||||
|
## Nästa steg
|
||||||
|
1. Implementera planen enligt ovan.
|
||||||
|
2. Testa funktionaliteten i en testmiljö.
|
||||||
|
3. Verifiera att all data raderas korrekt och att loggning fungerar.
|
||||||
|
4. Dokumentera ändringarna i `TEKNISK_BESKRIVNING.md`.
|
||||||
|
5. Informera teamet om den nya funktionaliteten och dess påverkan på GDPR-efterlevnaden.
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# Plan: Projektanpassad Lighthouse-plan for Flutter-web
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
Höja Lighthouse-resultaten för Flutter-webklienten i detta repo utan att bryta befintlig Docker/Caddy-deploy, med fokus på mätbar förbättring av prestanda, tillgänglighet och grundläggande SEO.
|
||||||
|
|
||||||
|
## Kontext och nuläge (verifierat i projektet)
|
||||||
|
- Flutter-web byggs redan i release-läge i `flutter/Dockerfile` via `flutter build web --release`.
|
||||||
|
- Webb klient körs via Caddy i `flutter/Caddyfile` och har redan `encode gzip`.
|
||||||
|
- `flutter/web/index.html` är minimal och innehåller redan korrekt viewport utan `user-scalable=no`.
|
||||||
|
- `flutter/web/` innehåller idag endast `index.html` (ingen `robots.txt`/`sitemap.xml` i Flutter-mappen).
|
||||||
|
- API-basurl injiceras redan korrekt med `--dart-define=API_BASE_URL=/api` via `compose.flutter.yml`.
|
||||||
|
|
||||||
|
## Problem i befintlig flutter-lighthouse.md som inte passar repo exakt
|
||||||
|
- Planen utgår delvis från Nginx/Apache, men projektet använder Caddy för Flutter-spåret.
|
||||||
|
- Påståendet om `user-scalable=no` gäller inte nuvarande `flutter/web/index.html`.
|
||||||
|
- Påståendet om ogiltig `robots.txt` kan inte verifieras i nuvarande Flutter-webmapp.
|
||||||
|
- Förslag om tvingad HTML-renderer är för grovt; bör vara experiment med mätning och rollback-kriterier.
|
||||||
|
|
||||||
|
## Strategi
|
||||||
|
Arbeta i tre iterationer med baseline -> lågriskoptimeringar -> tyngre optimeringar, och låt varje steg vara datadrivet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 1: Baseline och mätprotokoll (dag 1)
|
||||||
|
1. Skapa en reproducerbar mätbaseline för både lokal container och produktionsdomän.
|
||||||
|
2. Kör Lighthouse minst 3 gånger per miljö och ta median för:
|
||||||
|
- Performance score
|
||||||
|
- LCP
|
||||||
|
- TBT
|
||||||
|
- INP (om rapporteras)
|
||||||
|
- Transfer size / antal requests
|
||||||
|
3. Dokumentera nuläge och tröskelvärden före ändringar.
|
||||||
|
|
||||||
|
### Acceptanskriterier Fas 1
|
||||||
|
- Baseline-tabell finns med mätvärden från 3 körningar per miljö.
|
||||||
|
- Samma URL, samma nätprofil och samma emulering används konsekvent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 2: Lågriskfixar med hög nytta (dag 1-2)
|
||||||
|
|
||||||
|
### 2.1 Index och metadata (`flutter/web/index.html`)
|
||||||
|
- Lägg till `lang="sv"` på `<html>`.
|
||||||
|
- Lägg till relevant `meta description` för appens huvudsakliga nytta.
|
||||||
|
- Behåll `viewport` som den är (ingen ändring behövs kring zoom-blockering).
|
||||||
|
|
||||||
|
### 2.2 Caddy-headerhygien (`flutter/Caddyfile`)
|
||||||
|
- Behåll `encode gzip`.
|
||||||
|
- Lägg till explicita cache-headers för hashade statiska assets (js/wasm/fonts) med lång TTL och immutable.
|
||||||
|
- Lägg till kortare/konservativ cache för `index.html` så nya deploys slår igenom snabbt.
|
||||||
|
- Lägg till säkerhetsheaders som är kompatibla med Flutter-web (minst grundnivå: `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`).
|
||||||
|
|
||||||
|
### 2.3 Byggoptimering (`flutter/Dockerfile`)
|
||||||
|
- Utvärdera `--no-source-maps` i produktionsbuild för mindre artifact-storlek.
|
||||||
|
- Säkerställ att eventuella ändringar inte påverkar felsökning i miljö där sourcemaps behövs.
|
||||||
|
|
||||||
|
### Acceptanskriterier Fas 2
|
||||||
|
- Lighthouse visar förbättring i minst två nyckelmått (t.ex. LCP/TBT).
|
||||||
|
- Ingen regress i appstart, routning eller API-proxy `/api/*`.
|
||||||
|
- Build pipeline passerar oförändrat i Docker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 3: Prestandaexperiment med tydlig rollback (dag 2-4)
|
||||||
|
|
||||||
|
### 3.1 Rendering-strategi (CanvasKit vs HTML/Skwasm)
|
||||||
|
- Kör A/B-test av rendererstrategi i en separat branch/buildvariant.
|
||||||
|
- Mät skillnad i initial transfer size, LCP och renderingkvalitet på kritiska vyer.
|
||||||
|
- Beslut endast baserat på mätdata + visuell/regressionskontroll.
|
||||||
|
|
||||||
|
### 3.2 Deferred loading i tunga featureflöden
|
||||||
|
- Identifiera kandidater för deferred imports (exempel: import/admin-vyer med hög kodvikt).
|
||||||
|
- Introducera gradvis och validera att navigation inte blir ryckig.
|
||||||
|
|
||||||
|
### 3.3 Bootstrap-laddning
|
||||||
|
- Behåll asynkron laddning i `index.html` om mätning visar bäst resultat.
|
||||||
|
- Undvik manuella hacks som injicerar canvaskit-script ad hoc utan evidens i mätning.
|
||||||
|
|
||||||
|
### Acceptanskriterier Fas 3
|
||||||
|
- Minst 15-25% förbättring i TBT eller tydlig minskning i JS-exekveringstid jämfört med baseline.
|
||||||
|
- Ingen funktionell regress i kärnflöden (login, inventarie, recept, import).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 4: Tillgänglighet (A11y) med repo-fokus (dag 3-5)
|
||||||
|
1. Inventera ikonknappar och actions i Flutter-kod och säkra `tooltip`/`semanticsLabel` där det saknas.
|
||||||
|
2. Lägg till/justera `Semantics` för centrala actions (import, spara, ta bort, navigering).
|
||||||
|
3. Verifiera tangentbordsnavigering i webbläsare för huvudflöden.
|
||||||
|
|
||||||
|
### Acceptanskriterier Fas 4
|
||||||
|
- Lighthouse Accessibility förbättras mätbart.
|
||||||
|
- Inga nya fokusfällor eller förlorad keyboard-navigering introduceras.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 5: SEO-minimum för app-shell (dag 4-5)
|
||||||
|
1. Säkerställ att titel och meta description är korrekta för startsidan.
|
||||||
|
2. Besluta om `robots.txt` och `sitemap.xml` ska hanteras i Flutter-web, Caddy eller upstream-domänkonfiguration (inte antagande).
|
||||||
|
3. Implementera endast den väg som matchar faktisk domänrouting i drift.
|
||||||
|
|
||||||
|
### Acceptanskriterier Fas 5
|
||||||
|
- Lighthouse SEO får förbättring från baseline.
|
||||||
|
- Robots/sitemap-lösning är verifierad mot faktisk driftarkitektur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas 6: Säkerhet och CSP (dag 5)
|
||||||
|
1. Introducera en pragmatisk CSP för Flutter-web i Caddy med minsta nödvändiga undantag.
|
||||||
|
2. Testa särskilt att Flutter bootstrap, API-anrop och ev. externa resurser fungerar.
|
||||||
|
3. Strama åt policyn iterativt istället för en aggressiv engångspolicy.
|
||||||
|
|
||||||
|
### Acceptanskriterier Fas 6
|
||||||
|
- Säkerhetsheaders levereras korrekt från Flutter-Caddy.
|
||||||
|
- Ingen blockerad kärnfunktion pga CSP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prioriterad genomförandeordning
|
||||||
|
1. Fas 1 (baseline)
|
||||||
|
2. Fas 2 (lågriskfixar)
|
||||||
|
3. Re-mätning
|
||||||
|
4. Fas 3 (prestandaexperiment)
|
||||||
|
5. Fas 4 (tillgänglighet)
|
||||||
|
6. Fas 5 (SEO-minimum)
|
||||||
|
7. Fas 6 (CSP hardening)
|
||||||
|
8. Slutlig Lighthouse-jämförelse och dokumentation
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- Reproducerbar före/efter-mätning finns dokumenterad.
|
||||||
|
- Performance, Accessibility och SEO har förbättrats jämfört med baseline.
|
||||||
|
- Inga regressioner i Docker/Caddy-flödet eller appens kärnflöden.
|
||||||
|
- Åtgärderna är anpassade till faktisk stack (Flutter + Caddy), inte generiska Nginx-råd.
|
||||||
|
|
||||||
|
## Konkreta filer som sannolikt berörs vid implementation
|
||||||
|
- `flutter/web/index.html`
|
||||||
|
- `flutter/Caddyfile`
|
||||||
|
- `flutter/Dockerfile`
|
||||||
|
- ev. flera Flutter-vyer med knapp-/ikonsemantik under `flutter/lib/**`
|
||||||
|
|
||||||
|
## Risker och motåtgärder
|
||||||
|
- **Risk:** För aggressiv CSP bryter Flutter-bootstrap.
|
||||||
|
**Motåtgärd:** Iterativ policy + verifiering efter varje ändring.
|
||||||
|
- **Risk:** Rendererbyte förbättrar vikt men försämrar visual fidelity.
|
||||||
|
**Motåtgärd:** A/B-test med tydliga rollback-kriterier.
|
||||||
|
- **Risk:** Cache-policy gör deploys "stale".
|
||||||
|
**Motåtgärd:** Lång cache endast för fingerprintade assets, kort cache för `index.html`.
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
# Plan: Flyerimport - specialtecken, beskrivande felmeddelanden, synlig prompt
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
Tre sammanhängande förbättringar av flyerimport-pipelinen så att användaren får begripligt feedback och korrekt data:
|
||||||
|
|
||||||
|
1. Säkerställ att svenska tecken (ä, å, ö, é) bevaras i produktnamn som "Prästost", "Herrgårdsost", "Grevéost".
|
||||||
|
2. Ersätt opaka koder som `parse:ai_parsed` och `match:no_match` med människovänliga svenska förklaringar som beskriver "vad" som hände och "var" det hände.
|
||||||
|
3. Visa promptens innehåll (inte bara output) för operatörer/admins och vid behov i importflödet vid varningar.
|
||||||
|
|
||||||
|
Allt arbete ska respektera nuvarande arkitektur: NestJS backend (`backend/src/flyer-import`, `backend/src/ai`), Flutter web frontend (`flutter/lib/features/import`, `flutter/lib/features/admin`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verifierad nulägesbild (källkodsbevis)
|
||||||
|
|
||||||
|
### Specialtecken
|
||||||
|
- `ai-flyer-parser.service.ts:189-293` skickar prompt utan diakritiska tecken (skriver "Prastost", "Herrgardsost", "Greveost", "ARLA KO", "ä" undviks medvetet i prompt-instruktionerna). Detta gör att modellen tränas/uppmuntras att returnera namnen utan ä/å/é.
|
||||||
|
- `ai-flyer-parser.service.ts:358-364` (`normalizeName`) tar bort allt som inte är `[a-zåäö0-9\s]` men `rawName` skickas vidare som det är, så ä/å bevaras tekniskt om AI returnerar dem.
|
||||||
|
- `flyer-normalizer.service.ts:26-30` har en hårdkodad mappning för cheese variants:
|
||||||
|
- `prast: 'Prästost'`
|
||||||
|
- `herrgard: 'Herrgårdsost'`
|
||||||
|
- `greve: 'Greveost'` (saknar `é`! Bör vara `Grevéost`)
|
||||||
|
- `flyer-normalizer.service.ts:196-227` (`expandCheeseVariants`) gör `stripDiacritics` på rawName innan den jämför med token-list. Detta funkar för matchning men producerar diakrit-fria varianter om mappningen inte är korrekt.
|
||||||
|
- `flyer-normalizer.service.ts:140-146` (`normalizeName`) fungerar med `[a-zåäö0-9\s]` så svenska tecken behålls i normaliserat namn när inputen har dem.
|
||||||
|
|
||||||
|
### "parse:ai_parsed" och "match:no_match"
|
||||||
|
- Konstanter genereras som "reason codes" i koden:
|
||||||
|
- `ai-flyer-parser.service.ts:354`: `reasonCodes: ['ai_parsed']` - sätts på alla AI-parsade items.
|
||||||
|
- `flyer-import.service.ts:496`: `reasons: ['no_match']` - sätts när inget produktnamn matchar.
|
||||||
|
- Reason codes prefixas med `parse:` eller `match:` av `ai-trace.service.ts:435-452` (`collectWarnings`):
|
||||||
|
```ts
|
||||||
|
warnings.add(`parse:${text}`);
|
||||||
|
warnings.add(`match:${text}`);
|
||||||
|
```
|
||||||
|
- Dessa visas i admin AI-traces-vyn (`admin_ai_panel.dart:355-362`, `_WarningsCard`) och möjligen i import-UI:t via `_buildWarningsPanel` (`flyer_import_tab.dart:497-539`) om `_result.warnings` innehåller dem.
|
||||||
|
- Slutsats: koderna är inte fel - de är obegripliga utan översättning.
|
||||||
|
|
||||||
|
### Promptens synlighet
|
||||||
|
- Admin AI-panelen (`admin_ai_panel.dart:444-508`, `_PromptCard`) visar redan prompt med expand/copy-knappar.
|
||||||
|
- Importflödet (`flyer_import_tab.dart`, `receipt_import_tab.dart`) visar idag bara `warnings`-listan. Ingen promptvisning, ingen mappning från reason codes till begripligt språk, ingen länk till AI-trace.
|
||||||
|
- AI-trace lagras alltid i DB (`flyer-import.service.ts:148-164`, `persistFlyerTrace`). Promptens innehåll finns alltså redan tillgängligt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Strategi och faser
|
||||||
|
|
||||||
|
Tre paralleliserbara delprojekt med varsin egen fas. Slutlig sammanflätning verifieras med befintliga tester och ny smoke-test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas A: Specialtecken i produktnamn (lågriskfix, hög nytta)
|
||||||
|
|
||||||
|
### A1. Korrigera hårdkodad cheese-mappning
|
||||||
|
- `backend/src/flyer-import/services/flyer-normalizer.service.ts:26-30`
|
||||||
|
- Ändra `greve: 'Greveost'` till `greve: 'Grevéost'`.
|
||||||
|
- Behåll `prast: 'Prästost'` och `herrgard: 'Herrgårdsost'`.
|
||||||
|
|
||||||
|
### A2. Skydda diakritiska tecken hela vägen från AI-svar till klient
|
||||||
|
- `backend/src/flyer-import/services/ai-flyer-parser.service.ts`
|
||||||
|
- Säkerställ att Mistralklientens response.choices[0].message.content tolkas som UTF-8. Logga hex-dump i debug-läge om tecken förvanskas.
|
||||||
|
- I `sanitizeJsonResponse`/`normalizeAiItem`: säkerställ att `rawName` inte normaliseras eller stripps innan `normalize`-steget.
|
||||||
|
- Uppdatera prompten:
|
||||||
|
- Lägg till explicit instruktion: `Behåll svenska diakritiska tecken (ä, å, ö, é) i produktnamn. Returnera "Prästost", "Herrgårdsost", "Grevéost" - inte ASCII-versioner.`
|
||||||
|
- Uppdatera exempel-utdata i prompten (rad 256-286) från "Prastost"/"Herrgardsost" till "Prästost"/"Herrgårdsost"/"Grevéost" så modellen tränas att returnera korrekt.
|
||||||
|
- Behåll fortfarande `prast/herrgard/greve` som tokens i instruktion 9 men kräv normalisering till diakrit-versionen i utdatan.
|
||||||
|
- `backend/src/flyer-import/services/flyer-normalizer.service.ts`
|
||||||
|
- I `expandCheeseVariants` (rad 196-227): efter `stripDiacritics`-tokenisering, mappa via `CHEESE_VARIANT_TO_NAME` så slutnamnet alltid har korrekta tecken.
|
||||||
|
- I `fixKnownOcrTypos` (rad 250-262): lägg till regel som korrigerar "Greveost" -> "Grevéost" (men bara när det är klart att det är ostnamnet, inte personnamn). Använd kontext: bara om `category` antyder hårdost/ost eller `rawName` slutar på `ost`.
|
||||||
|
|
||||||
|
### A3. Säkra Caddy/HTTP-respons för UTF-8
|
||||||
|
- Verifiera att `Content-Type: application/json; charset=utf-8` returneras från NestJS (default i `@nestjs/platform-express`, men kontrollera att ingen middleware skriver om).
|
||||||
|
- Caddy gör inte byteomvandling som standard, men dubbelkolla att `flutter/Caddyfile` inte har någon `replace`-direktiv (det har det inte i nuläget).
|
||||||
|
|
||||||
|
### A4. Test
|
||||||
|
- Utöka `flyer-normalizer.service.spec.ts` med:
|
||||||
|
- Test som matar in rawName "PRAST" och kontrollerar att outputens rawName blir "Prästost" (inte "Prastost").
|
||||||
|
- Test som matar in rawName "GREVE" och kontrollerar att outputens rawName blir "Grevéost".
|
||||||
|
- Test som verifierar att `é`-tecken bevaras i hela pipen.
|
||||||
|
- Snapshot-test för prompten i `ai-flyer-parser.service.spec.ts` så att ändringar i prompten är medvetna.
|
||||||
|
|
||||||
|
### Acceptanskriterier Fas A
|
||||||
|
- En flyer som innehåller "PRAST, HERRGARD, GREVE" producerar rader med rawName `Prästost`, `Herrgårdsost`, `Grevéost`.
|
||||||
|
- Ingen befintlig test går sönder.
|
||||||
|
- UI:t (Flutter) visar de korrekta tecknen utan encoding-artifacts.
|
||||||
|
|
||||||
|
### Risker
|
||||||
|
- Mistral kan ignorera diakritiska tecken i instruktioner. Motåtgärd: post-normalisering i `flyer-normalizer.service.ts` är hårda fallback-regeln.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas B: Beskrivande felmeddelanden ersätter `parse:ai_parsed` och `match:no_match`
|
||||||
|
|
||||||
|
### B1. Centraliserad reason-code-katalog (backend)
|
||||||
|
- Skapa `backend/src/flyer-import/services/reason-codes.ts` (eller `backend/src/ai/reason-codes.ts` om det är delat med receipt) som exporterar:
|
||||||
|
- `ParseReasonCode = 'ai_parsed' | 'split_cheese_variants' | 'normalized' | 'low_confidence' | ...`
|
||||||
|
- `MatchReasonCode = 'no_match' | 'alias_exact' | 'normalized_exact' | 'token_overlap' | 'alias_points_to_missing_product' | 'empty_name'`
|
||||||
|
- Funktion `describeParseReason(code, context?): { title, message, severity, location? }`
|
||||||
|
- Funktion `describeMatchReason(code, context?): { title, message, severity }`
|
||||||
|
- Exempelmappning:
|
||||||
|
- `ai_parsed` -> `severity: 'info', title: 'AI-tolkad rad', message: 'Raden tolkades av AI utan att en deterministisk regel matchade.'`
|
||||||
|
- `split_cheese_variants` -> `severity: 'info', title: 'Variant-split', message: 'AI-svarade en gruppannons som expanderades till individuella ostvarianter.'`
|
||||||
|
- `low_confidence` -> `severity: 'warning', title: 'Låg parsningskvalitet', message: 'Modellens säkerhet är låg, granska raden manuellt.'`
|
||||||
|
- `no_match` -> `severity: 'warning', title: 'Ingen produktmatchning', message: 'Vi kunde inte hitta någon befintlig produkt som matchar texten på flyern.'`
|
||||||
|
- `alias_points_to_missing_product` -> `severity: 'error', title: 'Trasig alias-koppling', message: 'Ett alias pekar på en produkt som inte längre finns.'`
|
||||||
|
- `empty_name` -> `severity: 'error', title: 'Tomt produktnamn', message: 'Raden saknar tolkbart produktnamn.'`
|
||||||
|
- Inkludera `location` när relevant: t ex `'Steg: AI-parser, chunk N/M'` eller `'Steg: matchning mot dina produkter'`.
|
||||||
|
|
||||||
|
### B2. Returnera strukturerade reasons i API
|
||||||
|
- Utöka `backend/src/flyer-import/dto/flyer-import.response.ts`:
|
||||||
|
```ts
|
||||||
|
export type FlyerReasonDescriptor = {
|
||||||
|
code: string; // 'ai_parsed', 'no_match', ...
|
||||||
|
kind: 'parse' | 'match';
|
||||||
|
title: string; // Människovänlig titel
|
||||||
|
message: string; // Förklarande text
|
||||||
|
severity: 'info' | 'warning' | 'error';
|
||||||
|
location: string | null; // T ex 'Steg: matchning mot dina produkter'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
- Lägg till på `FlyerImportItem`:
|
||||||
|
- `parseReasonsDetailed: FlyerReasonDescriptor[]`
|
||||||
|
- `matchReasonsDetailed: FlyerReasonDescriptor[]`
|
||||||
|
- Behåll befintliga `parseReasons: string[]` och `matchReasons: string[]` för bakåtkompatibilitet.
|
||||||
|
- I `flyer-import.service.ts:110-144` (där `FlyerImportItem` byggs): mappa via `describeParseReason`/`describeMatchReason`.
|
||||||
|
- Detsamma för session-läs-paths (`toFlyerImportItem` rad 721-799 och `toFlyerImportResponseFromSession`).
|
||||||
|
|
||||||
|
### B3. Uppdatera AI-trace warnings (admin)
|
||||||
|
- `backend/src/ai/ai-trace.service.ts:435-452` (`collectWarnings`):
|
||||||
|
- Ändra till att returnera strukturerade objekt istället för `parse:xxx`/`match:xxx`-strängar:
|
||||||
|
```ts
|
||||||
|
type AdminAiWarning = {
|
||||||
|
code: string;
|
||||||
|
kind: 'parse' | 'match';
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
severity: 'info' | 'warning' | 'error';
|
||||||
|
itemIndex?: number; // Pekar på vilken rad i sessionen
|
||||||
|
};
|
||||||
|
```
|
||||||
|
- Uppdatera `AdminAiTraceDetail.warnings` schema till strukturerat format.
|
||||||
|
- Bibehåll en `legacyWarnings: string[]` med gamla formatet ifall någon klient ännu inte uppdaterats.
|
||||||
|
|
||||||
|
### B4. Uppdatera Flutter-modeller och vyer
|
||||||
|
- `flutter/lib/features/import/domain/flyer_import_item.dart`:
|
||||||
|
- Lägg till `parseReasonsDetailed: List<FlyerReasonDescriptor>` och `matchReasonsDetailed: List<FlyerReasonDescriptor>` med `fromJson`/`toJson`.
|
||||||
|
- Skapa `flutter/lib/features/import/domain/flyer_reason_descriptor.dart`:
|
||||||
|
- `class FlyerReasonDescriptor { final String code; final String kind; final String title; final String message; final String severity; final String? location; ... }`
|
||||||
|
- `flutter/lib/features/import/presentation/flyer_import_tab.dart`:
|
||||||
|
- I `_buildWarningsPanel` (rad 497-539): visa enbart sessionens egentliga warnings (existerande) men gör dem klickbara så de kan kopieras.
|
||||||
|
- Per rad: visa en summerande badge "X varningar" som expanderar till en lista med titel + message istället för tekniska koder. Använd ikoner per severity (info/warning/error).
|
||||||
|
- Behåll befintlig kvalitetsbadge (Hög/Medel/Låg).
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_ai_panel.dart`:
|
||||||
|
- `_WarningsCard` (rad 583+): byt ut SelectableText med rå-strängar mot strukturerad rendering: titel (fet), message (vanlig), severity-färg, eventuellt itemIndex som länk.
|
||||||
|
- Behåll copy-funktion - kopierar då en formaterad sträng `"[severity] title: message"`.
|
||||||
|
|
||||||
|
### B5. Test
|
||||||
|
- Backend: enhetstest för `describeParseReason`/`describeMatchReason` som täcker alla codes.
|
||||||
|
- Backend: integrationstest som verifierar att `FlyerImportResponse` innehåller `parseReasonsDetailed` med rätt fält.
|
||||||
|
- Flutter: widget-test för warnings-panel som verifierar att `parse:ai_parsed` ALDRIG visas, utan ersätts av "AI-tolkad rad".
|
||||||
|
- Uppdatera `flutter/test/features/admin/presentation/admin_ai_panel_test.dart` som idag verifierar `find.text('parse:low_confidence')` - testet ska istället leta efter "Låg parsningskvalitet".
|
||||||
|
|
||||||
|
### Acceptanskriterier Fas B
|
||||||
|
- Inga `parse:xxx`- eller `match:xxx`-strängar visas i UI:t (varken admin eller import).
|
||||||
|
- Varje warning har: titel, beskrivande text, severity-ikon, och om relevant en location ("Steg: ...").
|
||||||
|
- API:et returnerar både legacy- och nytt format, så ingen klient bryts.
|
||||||
|
|
||||||
|
### Risker
|
||||||
|
- Översättning av code -> human text måste hållas i sync mellan backend och Flutter. Motåtgärd: sätt textmappningen ENBART i backend och returnera färdig string. Flutter renderar bara.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fas C: Synlig prompt i import-flödet och vid varningar
|
||||||
|
|
||||||
|
### C1. Bestäm synlighetspolicy
|
||||||
|
- Operatörer/admins ska kunna se prompten direkt i admin AI-panelen (finns redan, fortsätter fungera).
|
||||||
|
- Vanliga användare ska kunna se prompten **efter behov** för att felsöka eller rapportera fel - men inte by default eftersom prompten är teknisk.
|
||||||
|
- Föreslagen UX: när en rad har varningar (parse eller match), visa knapp "Visa AI-detaljer" som öppnar modal med:
|
||||||
|
- Använd modell + retry/chunk-info
|
||||||
|
- Prompten (expanderbar)
|
||||||
|
- AI-svar (raw output, expanderbar)
|
||||||
|
- Lista över alla parse/match-reasons med deras `describeXxxReason`-output
|
||||||
|
- Promptvisning i adminpanelen redan komplett (`_PromptCard`) - bara verifiera att det fortsätter funka efter Fas B.
|
||||||
|
|
||||||
|
### C2. Backend-ändringar
|
||||||
|
- Ny endpoint `GET /api/flyer-import/sessions/:sessionId/ai-trace` som returnerar:
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
sessionId: number;
|
||||||
|
model: string;
|
||||||
|
prompt: string;
|
||||||
|
rawOutput: string;
|
||||||
|
chunkCount: number | null;
|
||||||
|
retryCount: number | null;
|
||||||
|
durationMs: number | null;
|
||||||
|
status: 'success' | 'warning' | 'error';
|
||||||
|
warnings: AdminAiWarning[];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Återanvänd existerande `aiTrace`-tabellen. Auth: kräv att `userId` ägar sessionen (samma policy som `getSessionSource`).
|
||||||
|
- Eventuellt feature-flagga visa-prompt-för-användare (`FLYER_AI_USER_PROMPT_VISIBLE` env). Default: `true` för admin, `true` för användare som äger sessionen.
|
||||||
|
|
||||||
|
### C3. Flutter-ändringar
|
||||||
|
- `flutter/lib/features/import/data/import_repository.dart`: ny metod `getFlyerSessionAiTrace(sessionId, token)`.
|
||||||
|
- `flutter/lib/features/import/domain/`: ny modell `FlyerAiTrace`.
|
||||||
|
- `flutter/lib/features/import/presentation/flyer_import_tab.dart`:
|
||||||
|
- Lägg till en "AI-detaljer"-knapp i headern (efter Importera-knappen) eller i varje rads expanderingspanel.
|
||||||
|
- Visa prompt + rawOutput i en modal/expanderbar Card med samma look-and-feel som admin (`_PromptCard` + `_OutputJsonCard` kan extraheras till delad widget under `flutter/lib/features/import/presentation/widgets/ai_trace_view.dart`).
|
||||||
|
- Lägg till samma åtgärd för `receipt_import_tab.dart` om motsvarande backend-stöd finns (det finns - receipts har också AI-trace).
|
||||||
|
|
||||||
|
### C4. Säkerhet och PII
|
||||||
|
- Innan prompt visas till slutanvändare: kör `maskSensitiveText` (finns redan i `ai-trace.service.ts`).
|
||||||
|
- Alla loggade prompts/rawOutputs ska redan vara maskerade i nuvarande pipeline. Verifiera att det fortsätter gälla.
|
||||||
|
|
||||||
|
### C5. Test
|
||||||
|
- Backend: e2e-test för nya endpointen, inklusive 403 för icke-ägare och 200 för ägare.
|
||||||
|
- Flutter: widget-test för att modalen öppnas och visar prompten.
|
||||||
|
- Manuell QA: Importera en flyer, klicka "AI-detaljer", verifiera att prompten visas och att kopiera-knappen fungerar.
|
||||||
|
|
||||||
|
### Acceptanskriterier Fas C
|
||||||
|
- Användare kan se prompten som skickades vid sin egen flyerimport.
|
||||||
|
- Adminpanelen visar fortfarande prompten oförändrat.
|
||||||
|
- PII-mask appliceras innan prompten skickas till klienten.
|
||||||
|
|
||||||
|
### Risker
|
||||||
|
- Prompten är lång (>5000 tecken). Motåtgärd: använd existerande expand/collapse-mönster (`_PromptCard.expanded`).
|
||||||
|
- Prompten kan innehålla användardata. Motåtgärd: maskning + tydlig "Detta innehåller text från din flyer"-disclaimer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prioriterad genomförandeordning
|
||||||
|
1. **Fas A** (specialtecken) - lågrisk, snabb seger för UX.
|
||||||
|
2. **Fas B** (mänskliga felmeddelanden) - medelarbete, hög UX-impact.
|
||||||
|
3. **Fas C** (prompt-synlighet) - mer komplex pga ny endpoint + UI, men oberoende av A och B.
|
||||||
|
|
||||||
|
Faserna kan implementeras i parallella PR:er om så önskas; Fas A och B berör delvis samma kodvägar i `flyer-normalizer.service.ts` och `flyer-import.service.ts`, och bör samordnas så att en PR mergas före nästa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- "Prästost", "Herrgårdsost", "Grevéost" och `é`/`å`/`ä` syns korrekt i flyerimport-UI.
|
||||||
|
- Inga råa `parse:ai_parsed`/`match:no_match`-strängar visas för användare eller admin.
|
||||||
|
- Användare och admin kan se vilken prompt som skickades till AI.
|
||||||
|
- Befintliga tester passerar; nya tester täcker varje fas separat.
|
||||||
|
- Inga regressioner i Docker-bygget (`backend/Dockerfile` kör `npm test -- --runInBand`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Konkreta filer som berörs
|
||||||
|
|
||||||
|
Backend:
|
||||||
|
- `backend/src/flyer-import/services/flyer-normalizer.service.ts` (Fas A)
|
||||||
|
- `backend/src/flyer-import/services/ai-flyer-parser.service.ts` (Fas A, eventuellt B)
|
||||||
|
- `backend/src/flyer-import/services/reason-codes.ts` (ny, Fas B)
|
||||||
|
- `backend/src/flyer-import/dto/flyer-import.response.ts` (Fas B)
|
||||||
|
- `backend/src/flyer-import/flyer-import.service.ts` (Fas B, Fas C)
|
||||||
|
- `backend/src/flyer-import/flyer-import.controller.ts` (Fas C, ny endpoint)
|
||||||
|
- `backend/src/ai/ai-trace.service.ts` (Fas B)
|
||||||
|
- `backend/src/flyer-import/services/flyer-normalizer.service.spec.ts` (Fas A)
|
||||||
|
- `backend/src/flyer-import/services/ai-flyer-parser.service.spec.ts` (Fas A, B)
|
||||||
|
|
||||||
|
Flutter:
|
||||||
|
- `flutter/lib/features/import/domain/flyer_import_item.dart` (Fas B)
|
||||||
|
- `flutter/lib/features/import/domain/flyer_reason_descriptor.dart` (ny, Fas B)
|
||||||
|
- `flutter/lib/features/import/domain/flyer_ai_trace.dart` (ny, Fas C)
|
||||||
|
- `flutter/lib/features/import/data/import_repository.dart` (Fas C)
|
||||||
|
- `flutter/lib/features/import/presentation/flyer_import_tab.dart` (Fas A-C)
|
||||||
|
- `flutter/lib/features/import/presentation/widgets/ai_trace_view.dart` (ny, Fas C)
|
||||||
|
- `flutter/lib/features/admin/domain/admin_ai_trace_detail.dart` (Fas B)
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_ai_panel.dart` (Fas B)
|
||||||
|
- `flutter/test/features/admin/presentation/admin_ai_panel_test.dart` (Fas B)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Riskanalys och rollback
|
||||||
|
|
||||||
|
| Risk | Sannolikhet | Motåtgärd |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Mistral returnerar fortfarande ASCII-versioner | Medel | Hård post-normalisering i `flyer-normalizer.service.ts` (Fas A2) |
|
||||||
|
| Strukturerade reasons bryter befintlig admin-vy | Låg | Behåll legacy-format parallellt under en release |
|
||||||
|
| Promptens längd försämrar mobil-UX | Låg | Default collapsed med expand-knapp |
|
||||||
|
| AI-trace exponerar PII oavsiktligt | Medel | Återanvänd befintlig `maskSensitiveText` + tydlig disclaimer |
|
||||||
|
| Ändrade reason codes bryter andra konsumenter (t ex flyer-selection-matcher) | Låg | Sökning visar att `reasonCodes` används endast i flyer-import + ai-trace; uppdatera samtliga callsites i samma PR |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Beslut tagna med dig
|
||||||
|
|
||||||
|
1. **Promptens synlighet**: endast admin. Vanliga användare ser inte prompten - bara översatta reasons. Detta förenklar Fas C avsevärt: ingen ny användarendpoint, ingen PII-mask för slutanvändare, ingen ny användar-UI.
|
||||||
|
2. **Reason-codes översätts i backend**: backend returnerar färdig svensk text (`title`, `message`) i `FlyerReasonDescriptor`. Frontend renderar bara strängarna utan översättning. Lang-parameter förbereds för framtida flerspråksstöd.
|
||||||
|
3. **Kopiera felrapport-knapp**: ja. Lägg till "Kopiera felrapport"-knapp i admin AI-panelens detail-vy som producerar formaterad text:
|
||||||
|
```
|
||||||
|
[AI-trace flyer-123]
|
||||||
|
Modell: ministral-8b-2512
|
||||||
|
Status: warning (3 varningar)
|
||||||
|
Tid: 2026-05-23T20:12:00
|
||||||
|
|
||||||
|
Varningar:
|
||||||
|
- [warning] Ingen produktmatchning (rad 5): Vi kunde inte hitta...
|
||||||
|
- [info] AI-tolkad rad (rad 7): Raden tolkades av AI...
|
||||||
|
|
||||||
|
Prompt:
|
||||||
|
...
|
||||||
|
|
||||||
|
Raw output:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
4. **Stavning**: `Grevéost` med `é` (Arlas officiella stavning).
|
||||||
|
|
||||||
|
## Konsekvenser av besluten på faserna
|
||||||
|
|
||||||
|
### Fas A (oförändrad)
|
||||||
|
- `Grevéost` används i mappningen.
|
||||||
|
|
||||||
|
### Fas B (oförändrad)
|
||||||
|
- Backend översätter och returnerar färdig svensk text i `title`/`message`.
|
||||||
|
- Behåll `code`-fältet så frontend kan filtrera/rendera olika per typ.
|
||||||
|
|
||||||
|
### Fas C (förenklad)
|
||||||
|
- **Tas bort**: Ny användarendpoint `GET /api/flyer-import/sessions/:id/ai-trace`.
|
||||||
|
- **Tas bort**: Ny Flutter-modell `FlyerAiTrace` och repository-metod för användarvy.
|
||||||
|
- **Tas bort**: Ny widget `ai_trace_view.dart` för importflödet.
|
||||||
|
- **Behålls**: Adminpanelens promptvisning (`_PromptCard`) - finns redan, fungerar.
|
||||||
|
- **Läggs till**: "Kopiera felrapport"-knapp i adminpanelens detail-vy. Genererar formaterad text enligt mallen ovan.
|
||||||
|
- **Användarflöde uppdateras**: i flyer/receipt-import-tabben visa endast översatta reasons via Fas B; ingen prompt-knapp för användare.
|
||||||
|
|
||||||
|
## Uppdaterade konkreta filer (efter beslut)
|
||||||
|
|
||||||
|
Backend:
|
||||||
|
- `backend/src/flyer-import/services/flyer-normalizer.service.ts` (Fas A)
|
||||||
|
- `backend/src/flyer-import/services/ai-flyer-parser.service.ts` (Fas A)
|
||||||
|
- `backend/src/flyer-import/services/reason-codes.ts` (ny, Fas B)
|
||||||
|
- `backend/src/flyer-import/dto/flyer-import.response.ts` (Fas B)
|
||||||
|
- `backend/src/flyer-import/flyer-import.service.ts` (Fas B)
|
||||||
|
- `backend/src/ai/ai-trace.service.ts` (Fas B)
|
||||||
|
- `backend/src/flyer-import/services/flyer-normalizer.service.spec.ts` (Fas A)
|
||||||
|
- `backend/src/flyer-import/services/ai-flyer-parser.service.spec.ts` (Fas A)
|
||||||
|
- *Inte längre*: ny endpoint i `flyer-import.controller.ts` (utgår med beslut 1).
|
||||||
|
|
||||||
|
Flutter:
|
||||||
|
- `flutter/lib/features/import/domain/flyer_import_item.dart` (Fas B)
|
||||||
|
- `flutter/lib/features/import/domain/flyer_reason_descriptor.dart` (ny, Fas B)
|
||||||
|
- `flutter/lib/features/import/presentation/flyer_import_tab.dart` (Fas A-B)
|
||||||
|
- `flutter/lib/features/admin/domain/admin_ai_trace_detail.dart` (Fas B)
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_ai_panel.dart` (Fas B + felrapportknapp)
|
||||||
|
- `flutter/test/features/admin/presentation/admin_ai_panel_test.dart` (Fas B)
|
||||||
|
- *Inte längre*: `flyer_ai_trace.dart`, `ai_trace_view.dart`, repository-metod (utgår med beslut 1).
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# Plan: Harmonisera flyer-import och kvitto-import
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
Implementera en gemensam importmodell och matchningspipeline så att flyer-import och kvitto-import beter sig så likt som möjligt, med fokus på:
|
||||||
|
- Automatisk strukturering av namn/brand/vikt samt bundle-detaljer
|
||||||
|
- Automatiskt kategoriupplösning (`categoryHint -> categoryId`)
|
||||||
|
- Matchning mot befintliga produkter via normaliserade namn + signaler
|
||||||
|
- Ingen automatisk skapning av produkter
|
||||||
|
- Förberedelse för framtida automation via strukturerade signaler (`signals` JSON)
|
||||||
|
|
||||||
|
## Icke-mål (denna implementation)
|
||||||
|
- Ingen auto-create av produkter i produktkatalog
|
||||||
|
- Ingen ändring av övergripande UI-flöde (manuell import/validering kvar)
|
||||||
|
- Ingen full omskrivning av receipt-import; vi extraherar och återanvänder delar stegvis
|
||||||
|
|
||||||
|
## Nuvarande gap (från kodbasen)
|
||||||
|
1. `FlyerItem.categoryId` sätts till `null` i parse-flödet trots `categoryHint`.
|
||||||
|
2. Flyer-matchning använder enklare strategi än receipt-import (färre regler/signalvikter).
|
||||||
|
3. Ingen strukturerad lagring av ursprung/etiketter (t.ex. Sverige, Eko) i flyer.
|
||||||
|
4. Bundleinformation finns men exponeras inte tydligt som detaljnamn i payload.
|
||||||
|
5. Receipt och flyer använder olika “kontrakt” för mellanrepresentation.
|
||||||
|
|
||||||
|
## Övergripande design
|
||||||
|
Inför en gemensam intern domänmodell för importerade rader (backend), och låt både flyer- och kvittoflöde mappa till den innan kategori/matchning.
|
||||||
|
|
||||||
|
### Gemensam intern modell (ny)
|
||||||
|
`ImportedItemCandidate` (internt, ej API-brytande initialt):
|
||||||
|
- `rawName`, `normalizedName`, `brand`
|
||||||
|
- `weight`, `bundleWeight`, `isBundle`, `bundleItems`
|
||||||
|
- `price`, `priceUnit`, `comparisonPrice`, `comparisonUnit`
|
||||||
|
- `categoryHint`, `categoryId`
|
||||||
|
- `matchedProductId`, `matchedProductName`, `matchedVia`, `matchConfidence`, `matchReasons`
|
||||||
|
- `signals` (JSON):
|
||||||
|
- `originCountries: string[]`
|
||||||
|
- `labels: string[]` (ekologisk, laktosfri, etc)
|
||||||
|
- `qualityFlags: string[]` (normaliserade flaggor, ex `eco`)
|
||||||
|
- `variant: string | null`
|
||||||
|
- `packaging: string | null`
|
||||||
|
- `displayNameDetailed` (beräknat fält, kan persistas eller beräknas vid response)
|
||||||
|
|
||||||
|
## Faser och implementation
|
||||||
|
|
||||||
|
## Fas 1: Datamodell och migration
|
||||||
|
1. Uppdatera `backend/prisma/schema.prisma`:
|
||||||
|
- Lägg till `signals Json?` på `FlyerItem`
|
||||||
|
- Lägg till `displayNameDetailed String?` på `FlyerItem`
|
||||||
|
2. Skapa Prisma-migration.
|
||||||
|
3. Säkerställ bakåtkompatibilitet:
|
||||||
|
- Nullabla fält
|
||||||
|
- Ingen ändring av befintliga constraints/index som bryter drift
|
||||||
|
4. (Valfritt i samma fas) indexera vanligt använda JSON-signaler senare först efter verifierad nytta.
|
||||||
|
|
||||||
|
### Acceptanskriterier fas 1
|
||||||
|
- Migration appliceras lokalt utan dataförlust.
|
||||||
|
- Befintliga endpoints fungerar med gamla rader (`signals = null`).
|
||||||
|
|
||||||
|
## Fas 2: Gemensamma normaliserings-/signalverktyg
|
||||||
|
1. Skapa gemensam utility-modul i backend, exempel:
|
||||||
|
- `backend/src/import-common/import-item.types.ts`
|
||||||
|
- `backend/src/import-common/import-signals.util.ts`
|
||||||
|
- `backend/src/import-common/import-display-name.util.ts`
|
||||||
|
2. Implementera signal-extraktion från textfält (`rawName`, `brand`, `offerText`):
|
||||||
|
- Ursprungsländer till `originCountries`
|
||||||
|
- Etiketter/märkningar till `labels`/`qualityFlags`
|
||||||
|
- Pack-format till `packaging`
|
||||||
|
3. Normalisera utan att förlora information:
|
||||||
|
- Ta bort signalord från primär matchsträng men spara i `signals`
|
||||||
|
- Ex: `Fläskytterfilé (Sverige)` -> matchsträng `flaskytterfile`, `signals.originCountries=["Sverige"]`
|
||||||
|
4. Implementera `displayNameDetailed`:
|
||||||
|
- Bundle: inkludera `bundleItems` i visningsnamn
|
||||||
|
- Ex: `Kaptenens Favoriter (Chumlax 3x100g + Alaska pollock 3x100g)`
|
||||||
|
|
||||||
|
### Acceptanskriterier fas 2
|
||||||
|
- Signals extraheras deterministiskt för kända mönster (Sverige/Tyskland/Eko/Ekologiskt).
|
||||||
|
- `displayNameDetailed` genereras för bundles.
|
||||||
|
|
||||||
|
## Fas 3: Kategoriupplösning i flyer (paritet med kvitto)
|
||||||
|
1. Extrahera/återanvänd kategori-regelmotorn från receipt-import till gemensam tjänst:
|
||||||
|
- Ex: `backend/src/import-common/category-resolver.service.ts`
|
||||||
|
2. Använd den i flyer-import efter normalisering:
|
||||||
|
- `categoryHint` + signaltext + regler -> `categoryId`
|
||||||
|
3. Prioritet:
|
||||||
|
- Produktmatchad kategori (om säkert matchad produkt har kategori) kan väga högst
|
||||||
|
- Annars regelbaserad kategori
|
||||||
|
- Annars behåll `categoryHint` utan `categoryId`
|
||||||
|
4. Specifika regler för kött/fläskytterfilé verifieras.
|
||||||
|
|
||||||
|
### Acceptanskriterier fas 3
|
||||||
|
- `Fläskytterfilé` får korrekt `categoryId` i flyer-session.
|
||||||
|
- `categoryId` sätts automatiskt för en betydande andel rader med tydlig signal.
|
||||||
|
|
||||||
|
## Fas 4: Matchningsparitet flyer <-> kvitto
|
||||||
|
1. Bryt ut matchning till gemensam matcher (eller harmonisera algoritm):
|
||||||
|
- alias exact
|
||||||
|
- canonical/normalized exact
|
||||||
|
- token/fuzzy
|
||||||
|
- bonus för brand/weight/signalträffar
|
||||||
|
2. Matchning ska använda signalrensad namnsträng + metadata:
|
||||||
|
- Länder och eco-etiketter ska inte sabotera namnmatch
|
||||||
|
3. Standardisera reason codes mellan flöden (så långt möjligt utan brytande API):
|
||||||
|
- `alias_exact`, `normalized_exact`, `token_overlap:*`, `no_match`
|
||||||
|
4. Behåll strikt policy: ingen auto-create produkt.
|
||||||
|
|
||||||
|
### Acceptanskriterier fas 4
|
||||||
|
- Färre `no_match` på samma flyer-input jämfört med baseline.
|
||||||
|
- Matchningsorsaker blir mer förklarbara och konsekventa.
|
||||||
|
|
||||||
|
## Fas 5: API/DTO och persistens
|
||||||
|
1. Uppdatera flyer DTO:
|
||||||
|
- `backend/src/flyer-import/dto/flyer-import.response.ts`
|
||||||
|
- Lägg till `signals` och `displayNameDetailed`.
|
||||||
|
2. Uppdatera persistens i `flyer-import.service.ts`:
|
||||||
|
- Spara `signals`, `displayNameDetailed`, `categoryId`.
|
||||||
|
3. Säkerställ att `getSession`, `getLatestSession`, `updateSessionItem` returnerar nya fält.
|
||||||
|
4. Behåll kompatibilitet mot klient:
|
||||||
|
- Nya fält adderas utan att ta bort befintliga.
|
||||||
|
|
||||||
|
### Acceptanskriterier fas 5
|
||||||
|
- Response innehåller tydlig bundle-info och signaler per rad.
|
||||||
|
- Inga regressions i existerande frontend-parsing.
|
||||||
|
|
||||||
|
## Fas 6: Frontend (flyer import-tab)
|
||||||
|
1. Uppdatera domänmodeller i Flutter:
|
||||||
|
- `flutter/lib/features/import/domain/flyer_import_item.dart`
|
||||||
|
- ev. session/result-objekt
|
||||||
|
2. Visa `displayNameDetailed` där tillgängligt, annars fallback `rawName`.
|
||||||
|
3. Visa `bundleItems` tydligt i list-/detaljrad.
|
||||||
|
4. Visa badge/metadata för signaler (`Sverige`, `Ekologisk`) utan att skriva över produktnamn.
|
||||||
|
5. Säkerställ att manuellt urval till inköpslista fortsätter fungera.
|
||||||
|
|
||||||
|
### Acceptanskriterier fas 6
|
||||||
|
- Bundle-rader är tydligare i UI.
|
||||||
|
- Ursprung/eko syns som metadata.
|
||||||
|
|
||||||
|
## Fas 7: Teststrategi
|
||||||
|
|
||||||
|
### Backend enhetstester
|
||||||
|
- `flyer-normalizer.service.spec.ts`
|
||||||
|
- extraktion av `signals` (origin/labels)
|
||||||
|
- bundle-detaljnamn
|
||||||
|
- Ny kategori-resolver-spec
|
||||||
|
- `Fläskytterfilé` -> köttkategori
|
||||||
|
- `flyer-import.service.spec.ts`
|
||||||
|
- `categoryId` sätts vid tydlig signal
|
||||||
|
- `signals` och `displayNameDetailed` persisteras/returneras
|
||||||
|
- Matchningstester
|
||||||
|
- namn med land/eko matchar korrekt produkt
|
||||||
|
|
||||||
|
### Integrationstester
|
||||||
|
- End-to-end parseAndMatch med representativ flyer-fixture.
|
||||||
|
- Verifiera att inga produkter auto-skaps.
|
||||||
|
- Verifiera att shopping-list insertion fungerar med/utan `matchedProductId`.
|
||||||
|
|
||||||
|
### Frontendtester
|
||||||
|
- Serialisering av nya fält i import-session.
|
||||||
|
- Rendering av `displayNameDetailed` + `bundleItems`.
|
||||||
|
|
||||||
|
## Fas 8: Mätning och rollout
|
||||||
|
1. Lägg till enkel före/efter-mätning i logg/trace:
|
||||||
|
- andel `no_match`
|
||||||
|
- andel med satt `categoryId`
|
||||||
|
2. Soft rollout via feature flag (om möjligt), annars stegvis release.
|
||||||
|
3. Utvärdera verkliga flyer-sessioner innan vidare automatisering.
|
||||||
|
|
||||||
|
## Konkreta filer att ändra (planerad)
|
||||||
|
- `backend/prisma/schema.prisma`
|
||||||
|
- `backend/src/flyer-import/flyer-import.service.ts`
|
||||||
|
- `backend/src/flyer-import/services/flyer-normalizer.service.ts`
|
||||||
|
- `backend/src/flyer-import/dto/flyer-import.response.ts`
|
||||||
|
- `backend/src/receipt-import/receipt-import.service.ts` (endast för extraktion/återanvändning av gemensamma delar)
|
||||||
|
- Nya gemensamma filer under `backend/src/import-common/*`
|
||||||
|
- `flutter/lib/features/import/domain/flyer_import_item.dart`
|
||||||
|
- `flutter/lib/features/import/data/flyer_import_session.dart`
|
||||||
|
- `flutter/lib/features/import/presentation/flyer_import_tab.dart`
|
||||||
|
- Relevanta spec/test-filer i backend + flutter
|
||||||
|
|
||||||
|
## Risker och mitigering
|
||||||
|
- Risk: API-kontraktsändringar bryter klient.
|
||||||
|
- Mitigering: endast additive fält, fallback på gamla fält.
|
||||||
|
- Risk: Felkategori vid aggressiva regler.
|
||||||
|
- Mitigering: regelprioritet + reason-codes + tester för edge cases.
|
||||||
|
- Risk: Övermatchning av produkter.
|
||||||
|
- Mitigering: tröskelvärden + konservativ confidence för fuzzy.
|
||||||
|
|
||||||
|
## Leveransordning (rekommenderad)
|
||||||
|
1. Fas 1–2 (schema + signals + utilities)
|
||||||
|
2. Fas 3 (kategoriupplösning flyer)
|
||||||
|
3. Fas 4 (matchningsparitet)
|
||||||
|
4. Fas 5 (DTO/persistens)
|
||||||
|
5. Fas 6 (frontend)
|
||||||
|
6. Fas 7–8 (tester + mätning/rollout)
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- Flyer och kvitto använder samma centrala regler för kategorisering/matchning där möjligt.
|
||||||
|
- Flyer-rader innehåller `signals` och tydligare produktrepresentation (`displayNameDetailed`, bundle-innehåll).
|
||||||
|
- `categoryId` sätts automatiskt i flyer när tillräcklig signal finns (inkl. fläskytterfilé-fall).
|
||||||
|
- Ingen automatisk produktskapning sker.
|
||||||
|
- Tester uppdaterade och gröna.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Plan: Hantera deprecated dependencies (`inflight` / `glob`) i backend
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
Minska säkerhets- och stabilitetsrisk från deprecated/transitiva paket i `backend` genom kontrollerad uppgradering, verifiering och CI-skydd utan att bryta befintliga flöden.
|
||||||
|
|
||||||
|
## Bakgrund
|
||||||
|
`npm ci` i `backend` visar varningar för:
|
||||||
|
- `inflight@1.0.6` (ej underhållen, memory leak)
|
||||||
|
- `glob@7.2.3` (föråldrad med kända sårbarheter)
|
||||||
|
|
||||||
|
Detta är normalt transitiva beroenden, men bör ändå adresseras systematiskt.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Endast Node-backend (`backend/`)
|
||||||
|
- Inga funktionella ändringar i app-logik
|
||||||
|
- Fokus: dependency graph, lockfile, CI-kontroller, dokumentation
|
||||||
|
|
||||||
|
## Implementationsplan
|
||||||
|
|
||||||
|
1. **Kartlägg källan till transitiva paket**
|
||||||
|
- Kör i `backend/`:
|
||||||
|
- `npm ls inflight`
|
||||||
|
- `npm ls glob`
|
||||||
|
- Dokumentera exakt vilka toppnivåpaket som drar in versionerna.
|
||||||
|
- Syfte: avgöra om problemet löses via toppnivåuppdatering eller `overrides`.
|
||||||
|
|
||||||
|
2. **Uppdatera direkta beroenden först (minst invasivt)**
|
||||||
|
- Kör riktad uppdatering av paket som identifieras i steg 1 (t.ex. test-/build-verktyg först, runtime efteråt).
|
||||||
|
- Kör därefter:
|
||||||
|
- `npm ci`
|
||||||
|
- `npm ls inflight glob`
|
||||||
|
- Beslutspunkt:
|
||||||
|
- Om `glob@7`/`inflight` försvinner: gå vidare till verifiering.
|
||||||
|
- Om kvarstår: gå till steg 3.
|
||||||
|
|
||||||
|
3. **Inför `overrides` i `backend/package.json` vid behov**
|
||||||
|
- Lägg till kontrollerade `overrides` för att styra bort sårbara/föråldrade versioner.
|
||||||
|
- Prioritet:
|
||||||
|
- `glob` till modern, kompatibel version i aktiv support.
|
||||||
|
- Undvik tvingad ersättning av `inflight` med inkompatibla alias om konsumentpaket inte stödjer det.
|
||||||
|
- Regenerera lockfile via normal npm-process och verifiera installationsflöde.
|
||||||
|
|
||||||
|
4. **Säkerhetsverifiera dependency-trädet**
|
||||||
|
- Kör:
|
||||||
|
- `npm audit --audit-level=high`
|
||||||
|
- `npm audit` (för full kontext)
|
||||||
|
- Klassificera återstående fynd:
|
||||||
|
- fixbara nu
|
||||||
|
- accepterad residualrisk (med motivering)
|
||||||
|
|
||||||
|
5. **Regressionstest av backend efter dependency-ändringar**
|
||||||
|
- Kör samma kvalitetskedja som används i projektet:
|
||||||
|
- `npm run prisma:validate`
|
||||||
|
- `npm run prisma:generate`
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npm run lint`
|
||||||
|
- `npm test`
|
||||||
|
- `npm run build`
|
||||||
|
- Syfte: säkerställa att dependency-upgrade inte skapar drift-/build-regressioner.
|
||||||
|
|
||||||
|
6. **Skärp CI-policy (om inte redan tillräcklig)**
|
||||||
|
- Verifiera att `.github/workflows/test.yml` fortsatt kör `npm audit --audit-level=high` för backend-push.
|
||||||
|
- Om önskat: höj till `critical` eller behåll `high` enligt teamets riskprofil.
|
||||||
|
- Rekommendation: behåll `high` i nuläget för bättre tidig signal i ett aktivt projekt.
|
||||||
|
|
||||||
|
7. **Dokumentera beslut och operativ hantering**
|
||||||
|
- Uppdatera `README.md` kort med:
|
||||||
|
- att deprecated-varningar hanterats
|
||||||
|
- hur man felsöker nya transitiva varningar (`npm ls <paket>`)
|
||||||
|
- policy för hur snabbt dependencies ska uppdateras
|
||||||
|
|
||||||
|
## Risker och mitigering
|
||||||
|
- **Risk: Breaking changes vid dependency bump**
|
||||||
|
- Mitigering: uppgradera stegvis + full kvalitetskedja i steg 5.
|
||||||
|
- **Risk: `overrides` maskerar underliggande kompatibilitetsproblem**
|
||||||
|
- Mitigering: använd `overrides` endast när toppnivåuppdatering inte räcker; dokumentera tydligt varför.
|
||||||
|
- **Risk: Kvarvarande audit-fynd blockerar release**
|
||||||
|
- Mitigering: klassificera residualrisk och skapa uppföljningsärende med ägare och deadline.
|
||||||
|
|
||||||
|
## Leverabler
|
||||||
|
- Uppdaterat `backend/package.json` (ev. `overrides` + uppdaterade versionsintervall)
|
||||||
|
- Uppdaterad `backend/package-lock.json`
|
||||||
|
- Eventuell liten CI-justering i `.github/workflows/test.yml`
|
||||||
|
- Kort dokumentationsnotis i `README.md`
|
||||||
|
|
||||||
|
## Acceptanskriterier
|
||||||
|
- `npm ls inflight` visar inte problematisk kedja, eller tydligt dokumenterad residualrisk med motivering.
|
||||||
|
- `npm ls glob` visar ingen osäker/föråldrad kedja (eller dokumenterad temporär avvikelse med plan).
|
||||||
|
- `npm audit --audit-level=high` passerar, eller kvarvarande fynd är explicit riskaccepterade.
|
||||||
|
- Backend-kvalitetskedjan passerar utan regression.
|
||||||
|
- CI för backend fortsätter passera.
|
||||||
|
|
||||||
|
## Rekommenderad genomförandeordning
|
||||||
|
1) Kartlägg (`npm ls`)
|
||||||
|
2) Riktade uppdateringar
|
||||||
|
3) `overrides` endast vid behov
|
||||||
|
4) Audit + full testkedja
|
||||||
|
5) CI/dokumentation
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Plan: Harmonisering av importfält baserat på inventory-tabellen
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
Skapa konsistens mellan kvitto-import, flyer-import och inventory-tabellen genom att anpassa fältnamn, datatyper och struktur. Detta kommer att förenkla integrationen och minska risken för fel.
|
||||||
|
|
||||||
|
## Bakgrund
|
||||||
|
- `inventory`-tabellen är central och har en väletablerad struktur.
|
||||||
|
- Kvitto-import och flyer-import använder olika fältnamn och datatyper, vilket skapar inkonsistenser.
|
||||||
|
- Flyer-import använder `signals.originCountries` (array), medan `inventory` använder `origin` (string).
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Uppdatera `ParsedReceiptItem` och `FlyerImportItem` för att matcha `inventory`-tabellen.
|
||||||
|
- Uppdatera mappningslogiken i importfunktionerna.
|
||||||
|
- Uppdatera databasen för att stödja `originCountries` som en array (lång sikt).
|
||||||
|
|
||||||
|
## Implementationsplan
|
||||||
|
|
||||||
|
### 1. Uppdatera `ParsedReceiptItem` (kvitto-import)
|
||||||
|
- **Mål**: Anpassa fältnamn och datatyper för att matcha `inventory`-tabellen.
|
||||||
|
- **Åtgärder**:
|
||||||
|
- Lägg till `categoryId` för att möjliggöra kategorisättning.
|
||||||
|
- Använd `rawName` istället för `receiptName` för konsistens.
|
||||||
|
- Mappa `origin` till `inventory.origin`.
|
||||||
|
|
||||||
|
### 2. Uppdatera `FlyerImportItem` (flyer-import)
|
||||||
|
- **Mål**: Anpassa fältnamn och datatyper för att matcha `inventory`-tabellen.
|
||||||
|
- **Åtgärder**:
|
||||||
|
- Använd `rawName` istället för `receiptName` för konsistens.
|
||||||
|
- Mappa `signals.originCountries[0]` till `inventory.origin`.
|
||||||
|
- Mappa `categoryId` till `product.categoryId` om en produkt skapas/uppdateras.
|
||||||
|
|
||||||
|
### 3. Uppdatera mappningslogiken
|
||||||
|
- **Mål**: Förenkla mappningen från importfunktionerna till `inventory`-tabellen.
|
||||||
|
- **Åtgärder**:
|
||||||
|
- Uppdatera `receipt-import.service.ts` för att använda `inventory`-fältnamn.
|
||||||
|
- Uppdatera `flyer-import.service.ts` för att använda `inventory`-fältnamn.
|
||||||
|
|
||||||
|
### 4. Uppdatera databasen (lång sikt)
|
||||||
|
- **Mål**: Stödja `originCountries` som en array i `inventory`-tabellen.
|
||||||
|
- **Åtgärder**:
|
||||||
|
- Lägg till `originCountries Json?` i `inventory`-tabellen.
|
||||||
|
- Uppdatera `CreateInventoryDto` för att inkludera `originCountries`.
|
||||||
|
|
||||||
|
### 5. Uppdatera DTO:er
|
||||||
|
- **Mål**: Säkerställa att DTO:er matchar `inventory`-tabellen.
|
||||||
|
- **Åtgärder**:
|
||||||
|
- Uppdatera `CreateInventoryDto` för att inkludera `originCountries`.
|
||||||
|
|
||||||
|
## Leverabler
|
||||||
|
- Uppdaterade `ParsedReceiptItem` och `FlyerImportItem` som matchar `inventory`-tabellen.
|
||||||
|
- Uppdaterad mappningslogik i `receipt-import.service.ts` och `flyer-import.service.ts`.
|
||||||
|
- Uppdaterad databas för att stödja `originCountries` som en array.
|
||||||
|
- Uppdaterade DTO:er för att inkludera `originCountries`.
|
||||||
|
|
||||||
|
## Acceptanskriterier
|
||||||
|
- `ParsedReceiptItem` och `FlyerImportItem` använder samma fältnamn och datatyper som `inventory`-tabellen.
|
||||||
|
- Mappningslogiken i importfunktionerna är förenklad och använder `inventory`-fältnamn.
|
||||||
|
- `inventory`-tabellen stödjer `originCountries` som en array.
|
||||||
|
- `CreateInventoryDto` inkluderar `originCountries`.
|
||||||
|
|
||||||
|
## Rekommenderad genomförandeordning
|
||||||
|
1. Uppdatera `ParsedReceiptItem` och `FlyerImportItem`.
|
||||||
|
2. Uppdatera mappningslogiken i importfunktionerna.
|
||||||
|
3. Uppdatera databasen för att stödja `originCountries` som en array.
|
||||||
|
4. Uppdatera DTO:er för att inkludera `originCountries`.
|
||||||
|
|
||||||
|
## Handover from Planning Session
|
||||||
|
- Planen är klar och redo för implementering.
|
||||||
|
- Inga frågor eller otydligheter kvarstår.
|
||||||
@@ -0,0 +1,706 @@
|
|||||||
|
🚨 Kritiska Problem (Prioritet 1: Fixa Omedelbart)
|
||||||
|
Dessa problem påverkar användarupplevelsen mest och bör åtgärdas först.
|
||||||
|
|
||||||
|
1. Prestanda: Långsam inladdning & Stora Filer
|
||||||
|
🔴 Problem:
|
||||||
|
|
||||||
|
Total storlek: 2,978 KiB (för stor för en webbapp).
|
||||||
|
|
||||||
|
/main.dart.js: 1,216 KiB (Flutter’s kompilade JavaScript).
|
||||||
|
canvaskit.wasm: 1,592 KiB (Flutter’s CanvasKit-renderare).
|
||||||
|
MaterialIcons-Regular.otf: 9.8 KiB (ikoner).
|
||||||
|
|
||||||
|
Largest Contentful Paint (LCP) misslyckades (tidsgräns överskreds).
|
||||||
|
Total Blocking Time (TBT) misslyckades (långa JavaScript-uppgifter blockerar huvudtråden).
|
||||||
|
JavaScript-exekveringstid: 1.8s (för långt).
|
||||||
|
🟢 Lösningar:
|
||||||
|
A. Optimera Flutter för Web
|
||||||
|
Flutter-webbappar är tyngre än traditionella webbappar på grund av CanvasKit. Här är hur du minskar storleken:
|
||||||
|
|
||||||
|
|
||||||
|
Använd HTML-renderaren istället för CanvasKit
|
||||||
|
|
||||||
|
CanvasKit ger bättre grafik men är mycket tyngre.
|
||||||
|
Ändra i index.html:
|
||||||
|
html
|
||||||
|
Copy
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.flutterConfiguration = {
|
||||||
|
renderMode: "html", // Istället för "canvas" (CanvasKit)
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Fördel: Minskar storleken med ~1.5MB (WASM-filen laddas inte).
|
||||||
|
Nackdel: Vissa avancerade animationer/efekter fungerar inte lika bra.
|
||||||
|
|
||||||
|
|
||||||
|
Aktivera komprimering (GZIP/Brotli)
|
||||||
|
|
||||||
|
Din server skickar okomprimerade filer.
|
||||||
|
Lösning:
|
||||||
|
|
||||||
|
Nginx: Lägg till i konfigurationen:
|
||||||
|
nginx
|
||||||
|
Copy
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
brotli on;
|
||||||
|
brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Apache: Använd mod_deflate eller mod_brotli.
|
||||||
|
Cloudflare: Aktivera "Brotli Compression" i inställningarna.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Cachea statiska resurser
|
||||||
|
|
||||||
|
Problem: /main.dart.js och flutter_bootstrap.js har ingen cache-TTL.
|
||||||
|
Lösning: Lägg till Cache-Control-headers:
|
||||||
|
nginx
|
||||||
|
Copy
|
||||||
|
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|wasm|otf)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Besparing: ~1.2MB vid upprepade besök.
|
||||||
|
|
||||||
|
|
||||||
|
Dela upp JavaScript-koden (Code Splitting)
|
||||||
|
|
||||||
|
Flutter laddar allt i en stor fil (main.dart.js).
|
||||||
|
Lösning: Använd deferred imports för att ladda funktioner på begäran:
|
||||||
|
dart
|
||||||
|
Copy
|
||||||
|
|
||||||
|
// I din Dart-kod:
|
||||||
|
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
|
||||||
|
import 'package:my_app/receipt_import.dart' deferred as receipt_import;
|
||||||
|
|
||||||
|
// Ladda endast när behövs:
|
||||||
|
Future<void> loadReceiptImport() async {
|
||||||
|
await receipt_import.loadLibrary();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Effekt: Minskar initial laddningstid.
|
||||||
|
|
||||||
|
|
||||||
|
Optimera bilder
|
||||||
|
|
||||||
|
Problem: Bilder laddas inte optimalt.
|
||||||
|
Lösning:
|
||||||
|
|
||||||
|
Använd flutter_image_compress för att komprimera bilder innan uppladdning.
|
||||||
|
För webb: Använd <picture> med srcset för responsiva bilder.
|
||||||
|
Exempel:
|
||||||
|
html
|
||||||
|
Copy
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
<source srcset="image-480w.jpg" media="(max-width: 600px)">
|
||||||
|
<source srcset="image-800w.jpg" media="(max-width: 1200px)">
|
||||||
|
<img src="image-1200w.jpg" alt="Receptbild">
|
||||||
|
</picture>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
B. Förbättra Laddningsordningen
|
||||||
|
|
||||||
|
Fördröj laddning av icke-kritiska resurser
|
||||||
|
|
||||||
|
Ladda canvaskit.wasm efter att sidan har renderats:
|
||||||
|
html
|
||||||
|
Copy
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://www.gstatic.com/canvaskit/v1/chromium/canvaskit.wasm';
|
||||||
|
document.body.appendChild(script);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Använd preload för kritiska resurser
|
||||||
|
|
||||||
|
Lägg till i <head>:
|
||||||
|
html
|
||||||
|
Copy
|
||||||
|
|
||||||
|
<link rel="preload" href="/main.dart.js" as="script">
|
||||||
|
<link rel="preload" href="/flutter_bootstrap.js" as="script">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2. Tillgänglighet: Grundläggande Problem
|
||||||
|
🔴 Problem:
|
||||||
|
|
||||||
|
[user-scalable="no"] i viewport-meta-taggen
|
||||||
|
|
||||||
|
Varför det är dåligt: Användare med nedsatt syn kan inte zooma in.
|
||||||
|
Lösning: Ta bort user-scalable="no":
|
||||||
|
html
|
||||||
|
Copy
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Saknas alt-texter på bilder
|
||||||
|
|
||||||
|
Problem: Skärmläsare kan inte beskriva bilder.
|
||||||
|
Lösning: Lägg till alt-texter i Flutter:
|
||||||
|
dart
|
||||||
|
Copy
|
||||||
|
|
||||||
|
Image.network(
|
||||||
|
'https://example.com/recept.jpg',
|
||||||
|
semanticLabel: 'Bild på lasagne med ost och tomatsås', // Alt-text
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Saknas lang-attribut
|
||||||
|
|
||||||
|
Problem: Skärmläsare vet inte vilket språk sidan använder.
|
||||||
|
Lösning: Lägg till i <html>:
|
||||||
|
html
|
||||||
|
Copy
|
||||||
|
|
||||||
|
<html lang="sv">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. SEO: Grundläggande Problem
|
||||||
|
🔴 Problem:
|
||||||
|
|
||||||
|
Saknas meta description
|
||||||
|
|
||||||
|
Varför det är dåligt: Sökmotorer visar ingen beskrivning i resultaten.
|
||||||
|
Lösning: Lägg till i <head>:
|
||||||
|
html
|
||||||
|
Copy
|
||||||
|
|
||||||
|
<meta name="description" content="Upptäck och lagra dina recept. Importera kvitton och håll koll på ditt kylskåp med vår smarta app.">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Ogiltig robots.txt
|
||||||
|
|
||||||
|
Problem: Din robots.txt innehåller HTML-kod istället för korrekt syntax.
|
||||||
|
Lösning: Skapa en korrekt robots.txt:
|
||||||
|
text
|
||||||
|
Copy
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
Sitemap: https://recept.gynther.se/sitemap.xml
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
⚠️ Viktiga Förbättringar (Prioritet 2: Fixa Inom 1-2 Veckor)
|
||||||
|
Dessa problem påverkar användarupplevelsen och SEO, men är inte lika kritiska.
|
||||||
|
|
||||||
|
1. Prestanda: JavaScript & Rendering
|
||||||
|
🟡 Problem:
|
||||||
|
|
||||||
|
Lång JavaScript-exekveringstid (1.8s)
|
||||||
|
|
||||||
|
Orsak: Flutter’s main.dart.js tar 1.7s att parsas och exekveras.
|
||||||
|
|
||||||
|
Minify CSS/JS misslyckades
|
||||||
|
|
||||||
|
Flutter genererar redan minifierad kod, men du kan optimera vidare.
|
||||||
|
|
||||||
|
🟢 Lösningar:
|
||||||
|
|
||||||
|
|
||||||
|
Aktivera Flutter’s --release flagga
|
||||||
|
|
||||||
|
Bygg appen med:
|
||||||
|
bash
|
||||||
|
Copy
|
||||||
|
|
||||||
|
flutter build web --release
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Detta genererar optimerad och minifierad kod.
|
||||||
|
|
||||||
|
|
||||||
|
Använd flutter build web --no-source-maps
|
||||||
|
|
||||||
|
Source maps ökar filstorleken. Ta bort dem i produktion:
|
||||||
|
bash
|
||||||
|
Copy
|
||||||
|
|
||||||
|
flutter build web --no-source-maps
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Fördröj laddning av icke-kritisk JavaScript
|
||||||
|
|
||||||
|
Använd defer eller async för skript som inte behövs omedelbart:
|
||||||
|
html
|
||||||
|
Copy
|
||||||
|
|
||||||
|
<script src="/flutter_bootstrap.js" defer></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Reducera DOM-storlek
|
||||||
|
|
||||||
|
Problem: Din sida har 21 element med en max-djup på 7 (acceptabelt, men kan optimeras).
|
||||||
|
Lösning: Undvik onödiga Container-widgets i Flutter. Använd const widgets där möjligt.
|
||||||
|
|
||||||
|
|
||||||
|
2. Tillgänglighet: Interaktiva Element
|
||||||
|
🟡 Problem:
|
||||||
|
|
||||||
|
Saknas focus-indikatorer
|
||||||
|
|
||||||
|
Användare som navigerar med tangentbord ser inte vilka element som är fokuserade.
|
||||||
|
|
||||||
|
Saknas aria-label på anpassade knappar
|
||||||
|
|
||||||
|
Skärmläsare vet inte vad knapparna gör.
|
||||||
|
|
||||||
|
🟢 Lösningar:
|
||||||
|
|
||||||
|
Lägg till focus-stilar i CSS
|
||||||
|
|
||||||
|
Exempel:
|
||||||
|
css
|
||||||
|
Copy
|
||||||
|
|
||||||
|
button:focus, [tabindex="0"]:focus {
|
||||||
|
outline: 2px solid #4285F4;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Använd semantics i Flutter för tillgänglighet
|
||||||
|
|
||||||
|
Exempel för en knapp:
|
||||||
|
dart
|
||||||
|
Copy
|
||||||
|
|
||||||
|
Semantics(
|
||||||
|
label: 'Importera kvitto',
|
||||||
|
button: true,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => _importReceipt(),
|
||||||
|
child: Text('Importera'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Lägg till aria-label på ikonknappar
|
||||||
|
|
||||||
|
Exempel:
|
||||||
|
dart
|
||||||
|
Copy
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.upload),
|
||||||
|
onPressed: () => _uploadFile(),
|
||||||
|
tooltip: 'Ladda upp kvitto', // Visas på hover
|
||||||
|
semanticsLabel: 'Ladda upp kvitto', // För skärmläsare
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
3. Säkerhet: Content Security Policy (CSP)
|
||||||
|
🟡 Problem:
|
||||||
|
|
||||||
|
Saknas CSP-header
|
||||||
|
|
||||||
|
Din sida är sårbar för XSS-attacker (Cross-Site Scripting).
|
||||||
|
|
||||||
|
🟢 Lösning:
|
||||||
|
Lägg till en stark CSP i din serverkonfiguration:
|
||||||
|
nginx
|
||||||
|
Copy
|
||||||
|
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.gstatic.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.gstatic.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://recept.gynther.se; frame-src 'none'; object-src 'none';";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Förklaring:
|
||||||
|
|
||||||
|
script-src: Tillåter skript från self och gstatic.com (för Flutter).
|
||||||
|
unsafe-inline: Nödvändigt för Flutter (men försök minska användningen).
|
||||||
|
img-src: Tillåter bilder från self och gstatic.com.
|
||||||
|
|
||||||
|
|
||||||
|
📌 Mindre Problem (Prioritet 3: Fixa När Tid Finns)
|
||||||
|
Dessa är förbättringar som inte är kritiska men kan förbättra UX och SEO.
|
||||||
|
|
||||||
|
1. Prestanda: Bildoptimering
|
||||||
|
|
||||||
|
Problem: Bilder laddas utan width och height (orsakar layout shifts).
|
||||||
|
Lösning: Använd Image.network med explicit storlek:
|
||||||
|
dart
|
||||||
|
Copy
|
||||||
|
|
||||||
|
Image.network(
|
||||||
|
'https://example.com/recept.jpg',
|
||||||
|
width: 300,
|
||||||
|
height: 200,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
2. SEO: Structured Data
|
||||||
|
|
||||||
|
Problem: Saknas schema.org-markup för recept (gör att Google kan visa "rich results").
|
||||||
|
Lösning: Lägg till JSON-LD i <head>:
|
||||||
|
html
|
||||||
|
Copy
|
||||||
|
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Recipe",
|
||||||
|
"name": "Lasagne",
|
||||||
|
"author": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "Nils-Johan Gynther"
|
||||||
|
},
|
||||||
|
"datePublished": "2026-05-21",
|
||||||
|
"description": "En klassisk lasagne med köttfärs och bechamelsås.",
|
||||||
|
"prepTime": "PT30M",
|
||||||
|
"cookTime": "PT45M",
|
||||||
|
"totalTime": "PT1H15M",
|
||||||
|
"recipeYield": "4 portioner",
|
||||||
|
"recipeCategory": "Middag",
|
||||||
|
"recipeCuisine": "Italiensk",
|
||||||
|
"keywords": "lasagne, pasta, köttfärs",
|
||||||
|
"recipeIngredient": ["500g köttfärs", "250g ost", "1 paket lasagneplattor"],
|
||||||
|
"recipeInstructions": [
|
||||||
|
{
|
||||||
|
"@type": "HowToStep",
|
||||||
|
"name": "Börja med att fräsa köttfärsen.",
|
||||||
|
"text": "Fräs köttfärsen i en stekpanna tills den är genomstekt."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "HowToStep",
|
||||||
|
"name": "Lagra lasagnen i ugnen.",
|
||||||
|
"text": "Skikta lasagneplattor, köttfärs och sås i en ugnsform. Grädda i 45 minuter på 200°C."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Effekt: Google kan visa receptkort i sökresultaten (ökad klickfrekvens).
|
||||||
|
|
||||||
|
3. Tillgänglighet: Logisk Tab-Order
|
||||||
|
|
||||||
|
Problem: Användare kan tabba till element som är dolda eller off-screen.
|
||||||
|
Lösning: Använd FocusableAction i Flutter för att kontrollera tab-order:
|
||||||
|
dart
|
||||||
|
Copy
|
||||||
|
|
||||||
|
FocusableActionDetector(
|
||||||
|
autofocus: true,
|
||||||
|
onFocusChange: (hasFocus) {
|
||||||
|
if (hasFocus) {
|
||||||
|
// Hantera focus
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: MyWidget(),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
📊 Sammanfattning av Prioriteringar
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Prioritet
|
||||||
|
Problem
|
||||||
|
Lösning
|
||||||
|
Tidsuppskattning
|
||||||
|
Impact
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
1 (Kritisk)
|
||||||
|
Stora filer (2.9MB)
|
||||||
|
Byt till HTML-renderare, aktivera GZIP, cachea resurser
|
||||||
|
1-2 timmar
|
||||||
|
⭐⭐⭐⭐⭐ (Hög)
|
||||||
|
|
||||||
|
|
||||||
|
1 (Kritisk)
|
||||||
|
Lång JavaScript-exekvering (1.8s)
|
||||||
|
Code splitting, defer non-critical JS
|
||||||
|
2-4 timmar
|
||||||
|
⭐⭐⭐⭐⭐ (Hög)
|
||||||
|
|
||||||
|
|
||||||
|
1 (Kritisk)
|
||||||
|
user-scalable="no"
|
||||||
|
Ta bort från viewport-meta-taggen
|
||||||
|
5 minuter
|
||||||
|
⭐⭐⭐⭐ (Hög)
|
||||||
|
|
||||||
|
|
||||||
|
1 (Kritisk)
|
||||||
|
Saknas meta description
|
||||||
|
Lägg till i
|
||||||
|
5 minuter
|
||||||
|
⭐⭐⭐ (Medel)
|
||||||
|
|
||||||
|
|
||||||
|
1 (Kritisk)
|
||||||
|
Ogiltig robots.txt
|
||||||
|
Skapa korrekt robots.txt
|
||||||
|
10 minuter
|
||||||
|
⭐⭐⭐ (Medel)
|
||||||
|
|
||||||
|
|
||||||
|
2 (Viktig)
|
||||||
|
Saknas CSP-header
|
||||||
|
Lägg till i serverkonfigurationen
|
||||||
|
1 timme
|
||||||
|
⭐⭐⭐⭐ (Hög)
|
||||||
|
|
||||||
|
|
||||||
|
2 (Viktig)
|
||||||
|
Saknas alt-texter
|
||||||
|
Lägg till semanticLabel i Flutter
|
||||||
|
1-2 timmar
|
||||||
|
⭐⭐⭐ (Medel)
|
||||||
|
|
||||||
|
|
||||||
|
2 (Viktig)
|
||||||
|
Lång DOM-parsning
|
||||||
|
Minska onödiga widgets
|
||||||
|
1-2 timmar
|
||||||
|
⭐⭐ (Låg)
|
||||||
|
|
||||||
|
|
||||||
|
3 (Mindre)
|
||||||
|
Saknas structured data
|
||||||
|
Lägg till JSON-LD för recept
|
||||||
|
1 timme
|
||||||
|
⭐⭐ (Låg)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
🛠️ Konkreta Åtgärder (Steg-för-Steg)
|
||||||
|
Steg 1: Fixa Prestanda (1-2 dagar)
|
||||||
|
|
||||||
|
Byt till HTML-renderare (sparar ~1.5MB):
|
||||||
|
|
||||||
|
Ändra index.html som visat ovan.
|
||||||
|
|
||||||
|
Aktivera GZIP/Brotli på servern.
|
||||||
|
Cachea statiska filer (1 år för JS/CSS).
|
||||||
|
Bygg appen med --release:
|
||||||
|
bash
|
||||||
|
Copy
|
||||||
|
|
||||||
|
flutter build web --release --no-source-maps
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Ladd upp den optimerade versionen till din server.
|
||||||
|
|
||||||
|
Steg 2: Fixa Tillgänglighet (1 dag)
|
||||||
|
|
||||||
|
Ta bort user-scalable="no" från viewport.
|
||||||
|
Lägg till lang="sv" i <html>.
|
||||||
|
Lägg till alt-texter på alla bilder.
|
||||||
|
Lägg till semanticsLabel på knappar och ikoner.
|
||||||
|
|
||||||
|
Steg 3: Fixa SEO (1 dag)
|
||||||
|
|
||||||
|
Lägg till meta description.
|
||||||
|
Fixa robots.txt.
|
||||||
|
Lägg till structured data för recept.
|
||||||
|
|
||||||
|
Steg 4: Säkerhet (1 timme)
|
||||||
|
|
||||||
|
Lägg till CSP-header i serverkonfigurationen.
|
||||||
|
|
||||||
|
📈 Förväntade Resultat
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Mätning
|
||||||
|
Nuvarande
|
||||||
|
Efter Fixar
|
||||||
|
Förbättring
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Total storlek
|
||||||
|
2,978 KiB
|
||||||
|
~500-800 KiB
|
||||||
|
-70%
|
||||||
|
|
||||||
|
|
||||||
|
Laddningstid (LCP)
|
||||||
|
Timeout
|
||||||
|
< 2.5s
|
||||||
|
✅ Fixad
|
||||||
|
|
||||||
|
|
||||||
|
JavaScript-exekvering
|
||||||
|
1.8s
|
||||||
|
< 1s
|
||||||
|
-45%
|
||||||
|
|
||||||
|
|
||||||
|
Tillgänglighet
|
||||||
|
82/100
|
||||||
|
95+/100
|
||||||
|
+15 poäng
|
||||||
|
|
||||||
|
|
||||||
|
SEO
|
||||||
|
6/10
|
||||||
|
9/10
|
||||||
|
+50%
|
||||||
|
|
||||||
|
|
||||||
|
Säkerhet
|
||||||
|
Risk för XSS
|
||||||
|
Skyddad
|
||||||
|
✅ Fixad
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
🔍 Hur du Testar Förbättringarna
|
||||||
|
|
||||||
|
Kör Lighthouse igen i Chrome DevTools (F12 → "Lighthouse").
|
||||||
|
Testa på mobil (använd Chrome’s "Device Toolbar").
|
||||||
|
Använd WebPageTest (webpagetest.org) för att mäta prestanda globalt.
|
||||||
|
Testa tillgänglighet med:
|
||||||
|
|
||||||
|
WAVE Evaluation Tool
|
||||||
|
Skärmläsare (t.ex. NVDA eller VoiceOver).
|
||||||
|
|
||||||
|
|
||||||
|
💡 Extra Tips för Flutter-Webbappar
|
||||||
|
|
||||||
|
|
||||||
|
Använd flutter build web --target=lib/main_deferred.dart
|
||||||
|
|
||||||
|
Förbättrar code splitting.
|
||||||
|
|
||||||
|
|
||||||
|
Ladda Flutter asynkront
|
||||||
|
|
||||||
|
Lägg till async i <script>-taggen:
|
||||||
|
html
|
||||||
|
Copy
|
||||||
|
|
||||||
|
<script src="/flutter_bootstrap.js" async></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Använd flutter_tex för text-rendering
|
||||||
|
|
||||||
|
Minskar beroendet av CanvasKit.
|
||||||
|
|
||||||
|
|
||||||
|
Override Flutter’s default fonts
|
||||||
|
|
||||||
|
Använd systemtypsnitt för snabbare laddning:
|
||||||
|
css
|
||||||
|
Copy
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: local('Roboto'), local('Roboto-Regular');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
🎯 Sammanfattning: Vad du ska göra nu
|
||||||
|
|
||||||
|
Fixa de kritiska prestandaproblemen (HTML-renderare, GZIP, caching).
|
||||||
|
Fixa tillgänglighetsproblemen (user-scalable, alt-texter, lang).
|
||||||
|
Fixa SEO (meta description, robots.txt).
|
||||||
|
Lägg till säkerhetsheaders (CSP).
|
||||||
|
Testa och iterera med Lighthouse.
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
flyerimporter.md
|
||||||
|
📌 Steg 1: Skapa en funktion för att extrahera text från PDF:en
|
||||||
|
Använd pdf-parse som primär metod och Tesseract.js som fallback för OCR.
|
||||||
|
Kod: extractFlyerText.ts
|
||||||
|
typescript
|
||||||
|
Copy
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as pdf from 'pdf-parse';
|
||||||
|
import Tesseract from 'tesseract.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extraherar text från en PDF-fil (flyer), med fallback till OCR.
|
||||||
|
* @param pdfPath Sökväg till PDF-filen.
|
||||||
|
* @returns Extraherad text.
|
||||||
|
*/
|
||||||
|
export async function extractFlyerText(pdfPath: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
// Försök med pdf-parse först
|
||||||
|
const dataBuffer = fs.readFileSync(pdfPath);
|
||||||
|
const data = await pdf(dataBuffer);
|
||||||
|
if (data.text.trim()) {
|
||||||
|
return data.text;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('pdf-parse misslyckades, försöker med OCR...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback till Tesseract.js för OCR
|
||||||
|
try {
|
||||||
|
const { data: { text } } = await Tesseract.recognize(pdfPath, 'swe', {
|
||||||
|
logger: (m) => console.log(m),
|
||||||
|
});
|
||||||
|
return text;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OCR misslyckades:', error);
|
||||||
|
throw new Error('Kunde inte extrahera text från PDF:en.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
📌 Steg 2: Skapa en funktion för att skicka texten till Mistral Tiny
|
||||||
|
Använd Mistral Tiny för att extrahera och strukturera all produktinformation från flyern.
|
||||||
|
Kod: importFlyerWithAI.ts
|
||||||
|
typescript
|
||||||
|
Copy
|
||||||
|
|
||||||
|
import { MistralClient } from '@mistralai/mistralai';
|
||||||
|
|
||||||
|
const mistral = new MistralClient({
|
||||||
|
apiKey: process.env.MISTRAL_API_KEY,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Skickar flyer-texten till Mistral Tiny för att extrahera strukturerad data.
|
||||||
|
* @param text Texten från flyern.
|
||||||
|
* @returns Strukturerad data (JSON-array).
|
||||||
|
*/
|
||||||
|
export async function importFlyerWithAI(text: string): Promise<any[]> {
|
||||||
|
const prompt = `
|
||||||
|
Du är en expert på att tolka svenska matvaruflyers (t.ex. från Willys).
|
||||||
|
Extrahera ALL produktinformation från följande text och returnera den som en JSON-array.
|
||||||
|
|
||||||
|
För varje produkt, inkludera:
|
||||||
|
- name: Produktnamn (fullständigt namn)
|
||||||
|
- weight: Vikt (om tillgänglig, t.ex. "150g", "Ca 1kg")
|
||||||
|
- origin: Ursprung/land/märke (om tillgänglig, t.ex. "FALKENBERG", "NYBERGS DELI • Sverige")
|
||||||
|
- price: Pris (som ett nummer, t.ex. 39.90)
|
||||||
|
- comparisonPrice: Jämförpris (som ett nummer, t.ex. 266.00)
|
||||||
|
- unit: Enhet (kg, st, förp, l, etc.)
|
||||||
|
- offer: Erbjudande (t.ex. ["Max 3 köp/hushåll", "Lägsta 30-dgrspris 125:00 kr"])
|
||||||
|
- category: Kategori (t.ex. "Fisk", "Kött", "Mejeri", "Grönsaker", "Frukt", "Dryck")
|
||||||
|
- validFrom: Giltig från (datum i formatet YYYY-MM-DD, om tillgängligt)
|
||||||
|
- validTo: Giltig till (datum i formatet YYYY-MM-DD, om tillgängligt)
|
||||||
|
|
||||||
|
Texten att tolka:
|
||||||
|
${text}
|
||||||
|
|
||||||
|
Returnera ENDAST en JSON-array. Inga andra kommentarer.
|
||||||
|
Exempel på utdata:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "KALLRÖKT LAX, GRAVAD LAX",
|
||||||
|
"weight": "150g",
|
||||||
|
"origin": "FALKENBERG",
|
||||||
|
"price": 39.90,
|
||||||
|
"comparisonPrice": 266.00,
|
||||||
|
"unit": "kg",
|
||||||
|
"offer": ["Max 3 köp/hushåll"],
|
||||||
|
"category": "Fisk",
|
||||||
|
"validFrom": "2026-05-18",
|
||||||
|
"validTo": "2026-05-24"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await mistral.chat({
|
||||||
|
model: 'mistral-tiny', // Använder den enklaste modellen
|
||||||
|
messages: [{ role: 'user', content: prompt }],
|
||||||
|
temperature: 0.1, // Låg temperatur för mer deterministiska svar
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rensa upp JSON-strängen
|
||||||
|
const jsonString = response.choices[0].message.content
|
||||||
|
.replace(/```json|```/g, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// Parsa JSON:en
|
||||||
|
return JSON.parse(jsonString);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fel vid AI-import:', error);
|
||||||
|
throw new Error('Kunde inte importera flyern med AI.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
📌 Steg 3: Fullständigt importflöde
|
||||||
|
Kombinera text-extrahering och AI-import i ett fullständigt flöde.
|
||||||
|
Kod: flyerImportService.ts
|
||||||
|
typescript
|
||||||
|
Copy
|
||||||
|
|
||||||
|
import { extractFlyerText } from './extractFlyerText';
|
||||||
|
import { importFlyerWithAI } from './importFlyerWithAI';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importerar en flyer (PDF) och returnerar strukturerad data.
|
||||||
|
* @param pdfPath Sökväg till PDF-filen.
|
||||||
|
* @returns Strukturerad data från flyern.
|
||||||
|
*/
|
||||||
|
export async function importFlyer(pdfPath: string) {
|
||||||
|
try {
|
||||||
|
// 1. Extrahera text från PDF:en
|
||||||
|
console.log('Extraherar text från flyern...');
|
||||||
|
const text = await extractFlyerText(pdfPath);
|
||||||
|
|
||||||
|
// 2. Skicka texten till Mistral Tiny för att extrahera data
|
||||||
|
console.log('Skickar text till Mistral Tiny för extrahering...');
|
||||||
|
const products = await importFlyerWithAI(text);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
products,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fel vid import:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Okänt fel',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
📌 Steg 4: API-Endpoint för flyer-import
|
||||||
|
Skapa en Express-endpoint för att hantera uppladdning och import av flyers.
|
||||||
|
Kod: flyerImportRouter.ts
|
||||||
|
typescript
|
||||||
|
Copy
|
||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
import multer from 'multer';
|
||||||
|
import { importFlyer } from '../services/flyerImportService';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const upload = multer({ dest: 'uploads/' });
|
||||||
|
|
||||||
|
// Endpoint för att ladda upp och importera en flyer
|
||||||
|
router.post('/import/flyer', upload.single('flyer'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'Ingen flyer uppladdad.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await importFlyer(req.file.path);
|
||||||
|
|
||||||
|
// Rensa upp uppladdad fil
|
||||||
|
fs.unlinkSync(req.file.path);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return res.status(500).json({ error: result.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fel vid flyer-import:', error);
|
||||||
|
res.status(500).json({ error: 'Kunde inte importera flyern.' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
📌 Steg 5: Exempel på utdata
|
||||||
|
När du skickar texten från Willys flyer till Mistral Tiny, kommer du att få tillbaka en JSON-array med alla produkter, t.ex.:
|
||||||
|
json
|
||||||
|
Copy
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "KALLRÖKT LAX, GRAVAD LAX",
|
||||||
|
"weight": "150g",
|
||||||
|
"origin": "FALKENBERG",
|
||||||
|
"price": 39.9,
|
||||||
|
"comparisonPrice": 266.0,
|
||||||
|
"unit": "kg",
|
||||||
|
"offer": ["Max 3 köp/hushåll"],
|
||||||
|
"category": "Fisk",
|
||||||
|
"validFrom": "2026-05-18",
|
||||||
|
"validTo": "2026-05-24"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "FLÄSKYTTERFILÉ",
|
||||||
|
"weight": "Ca 1kg",
|
||||||
|
"origin": "NYBERGS DELI • Sverige",
|
||||||
|
"price": 64.9,
|
||||||
|
"comparisonPrice": 64.9,
|
||||||
|
"unit": "kg",
|
||||||
|
"offer": ["Max 3 förp/hushåll"],
|
||||||
|
"category": "Kött",
|
||||||
|
"validFrom": "2026-05-18",
|
||||||
|
"validTo": "2026-05-24"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
📌 Steg 6: Använda den extraherade datan
|
||||||
|
När du har den strukturerade datan, kan du:
|
||||||
|
|
||||||
|
Lagra den i din databas (t.ex. för att jämföra med inventory).
|
||||||
|
Visa den för användaren (t.ex. i en tabell).
|
||||||
|
Använda den för att generera recept (med eller utan AI).
|
||||||
|
Exempel: Lagra i databasen
|
||||||
|
typescript
|
||||||
|
Copy
|
||||||
|
|
||||||
|
// Antas att du har en Prisma-modell för flyer-produkter
|
||||||
|
await prisma.flyerProduct.createMany({
|
||||||
|
data: products.map((product) => ({
|
||||||
|
name: product.name,
|
||||||
|
weight: product.weight,
|
||||||
|
origin: product.origin,
|
||||||
|
price: product.price,
|
||||||
|
comparisonPrice: product.comparisonPrice,
|
||||||
|
unit: product.unit,
|
||||||
|
offer: JSON.stringify(product.offer),
|
||||||
|
category: product.category,
|
||||||
|
validFrom: product.validFrom ? new Date(product.validFrom) : null,
|
||||||
|
validTo: product.validTo ? new Date(product.validTo) : null,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
📌 Steg 7: Frontend-Integrering (Exempel: React)
|
||||||
|
Här är hur du kan integrera flyer-importen i din frontend:
|
||||||
|
Kod: FlyerImportForm.tsx
|
||||||
|
tsx
|
||||||
|
Copy
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
function FlyerImportForm() {
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [result, setResult] = useState<any>(null);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('flyer', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/import/flyer', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' },
|
||||||
|
});
|
||||||
|
setResult(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fel vid uppladdning:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2>Importera flyer</h2>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".pdf"
|
||||||
|
onChange={(e) => setFile(e.target.files?.[0] || null)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button type="submit" disabled={isLoading}>
|
||||||
|
{isLoading ? 'Importerar...' : 'Importera flyer'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{result?.success && (
|
||||||
|
<div>
|
||||||
|
<h3>Importerade produkter ({result.products.length})</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Namn</th>
|
||||||
|
<th>Pris</th>
|
||||||
|
<th>Jämförpris</th>
|
||||||
|
<th>Kategori</th>
|
||||||
|
<th>Erbjudande</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{result.products.map((product: any, index: number) => (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>{product.name}</td>
|
||||||
|
<td>{product.price} {product.unit}</td>
|
||||||
|
<td>{product.comparisonPrice} {product.unit}</td>
|
||||||
|
<td>{product.category}</td>
|
||||||
|
<td>{product.offer.join(', ')}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlyerImportForm;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
📌 Miljövariabler (.env)
|
||||||
|
env
|
||||||
|
Copy
|
||||||
|
|
||||||
|
# Mistral API-nyckel
|
||||||
|
MISTRAL_API_KEY=din_api_nyckel_här
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# Plan för förbättrad dokumentationsstruktur
|
||||||
|
|
||||||
|
## Bakgrund
|
||||||
|
Projektet har idag över 20 `.md`-filer spridda över olika mappar, vilket gör det svårt att hitta information och förstå projektets helhet. Den nya strukturen syftar till att:
|
||||||
|
- **Göra dokumentationen användarvänlig** för både utvecklare och språkmodeller.
|
||||||
|
- **Optimerad för underhåll** med tydlig separation mellan aktiv och arkiverad dokumentation.
|
||||||
|
- **Skapa kontextuell sammanhang** genom länkade filer och tydliga hierarkier.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nuvarande problem
|
||||||
|
- **Fragmentering**: Relaterat innehåll är splittrat (t.ex. Flutter-dokumentation i `_archive/docs/flutter/` vs. backend-dokumentation i rotmappen).
|
||||||
|
- **Föråldrat innehåll**: Arkiverade filer blandas med aktiva (t.ex. `SESSION_CHECKPOINT_2026-05-12.md`).
|
||||||
|
- **Duplicering**: Samma koncept dokumenteras på flera ställen (t.ex. kategoriträd).
|
||||||
|
- **Icke-optimerat för språkmodeller**: Saknar tydlig hierarki och kontextuella länkar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Föreslagen struktur
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── docs/ # Huvudkatalog för ALL aktiv dokumentation
|
||||||
|
│ ├── 01-overview/ # Övergripande projektbeskrivning
|
||||||
|
│ │ ├── README.md # Huvudsaklig ingress (ersätter rot-README.md)
|
||||||
|
│ │ ├── ARCHITECTURE.md # Systemarkitektur
|
||||||
|
│ │ └── GLOSSARY.md # Termer och definitioner
|
||||||
|
│ │
|
||||||
|
│ ├── 02-setup/ # Installation och konfiguration
|
||||||
|
│ │ ├── INSTALL.md # Miljökrav, beroenden, första uppstart
|
||||||
|
│ │ ├── CONFIG.md # Konfigurationsfiler (.env, Docker, etc.)
|
||||||
|
│ │ └── TROUBLESHOOTING.md # Vanliga problem och lösningar
|
||||||
|
│ │
|
||||||
|
│ ├── 03-development/ # Utvecklingsguider
|
||||||
|
│ │ ├── CONTRIBUTING.md # Bidragsregler, kodstandard, PR-process
|
||||||
|
│ │ ├── WORKFLOWS.md # Git-flöden, branch-strategi, CI/CD
|
||||||
|
│ │ ├── DATABASE.md # Schema, migrationer, seedning
|
||||||
|
│ │ ├── API.md # Backend-API:er, Swagger-länkar
|
||||||
|
│ │ ├── FLUTTER.md # Flutter-specifik dokumentation
|
||||||
|
│ │ └── MICROSERVICES.md # Importer, AI, etc.
|
||||||
|
│ │
|
||||||
|
│ ├── 04-deploy/ # Driftsättning och underhåll
|
||||||
|
│ │ ├── DEPLOY.md # Steg-för-steg deploy
|
||||||
|
│ │ ├── MAINTENANCE.md # Underhållsskript, backup, monitorering
|
||||||
|
│ │ └── SCALING.md # Prestanda, skalning
|
||||||
|
│ │
|
||||||
|
│ ├── 05-features/ # Djupdyk i funktioner
|
||||||
|
│ │ ├── RECIPE_IMPORT.md # Kvittosimport och flyer-parsing
|
||||||
|
│ │ ├── CATEGORY_TREE.md # Kategorihantering och L3-integration
|
||||||
|
│ │ ├── SHOPPING_LIST.md # Inköpslistor och flyer-integration
|
||||||
|
│ │ └── ... # Övriga funktioner
|
||||||
|
│ │
|
||||||
|
│ └── 06-archive/ # Arkiverade dokument
|
||||||
|
│ ├── sessions/ # Gamla sessionsanteckningar
|
||||||
|
│ ├── legacy/ # Föråldrade planer
|
||||||
|
│ └── flutter_legacy/ # Gamla Flutter-dokument
|
||||||
|
│
|
||||||
|
├── .github/ # GitHub-specifika filer
|
||||||
|
│ ├── COPILOT_INSTRUCTIONS.md
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
└── ... # Övriga projektfiler (backend/, flutter/, etc.)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migrationsplan
|
||||||
|
|
||||||
|
### Steg 1: Skapa den nya strukturen
|
||||||
|
```bash
|
||||||
|
mkdir -p docs/{01-overview,02-setup,03-development,04-deploy,05-features,06-archive/sessions,06-archive/legacy,06-archive/flutter_legacy}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steg 2: Flytta och uppdatera filer
|
||||||
|
|
||||||
|
| Källfil(er) | Målfil | Åtgärd |
|
||||||
|
|---------------------------------------------|-----------------------------------------|------------------------------------------------------------------------|
|
||||||
|
| `TEKNISK_BESKRIVNING.md` | `docs/01-overview/ARCHITECTURE.md` | Extrahera arkitekturavsnitt. |
|
||||||
|
| `TEKNISK_BESKRIVNING.md` (deploy) | `docs/04-deploy/DEPLOY.md` | Extrahera deploy-avsnitt. |
|
||||||
|
| `TEKNISK_BESKRIVNING.md` (databas) | `docs/03-development/DATABASE.md` | Extrahera databasavsnitt + lägg till underhållsskript. |
|
||||||
|
| `flyerimporter.md` | `docs/05-features/RECIPE_IMPORT.md` | Uppdatera med nya flöden och API-endpoints. |
|
||||||
|
| `_archive/docs/flutter/*` | `docs/03-development/FLUTTER.md` | Slå samman och uppdatera Flutter-dokumentation. |
|
||||||
|
| `README.md` | `docs/01-overview/README.md` | Uppdatera med länkar till nya dokument. |
|
||||||
|
| `.github/copilot-instructions.md` | `.github/COPILOT_INSTRUCTIONS.md` | Flytta och uppdatera. |
|
||||||
|
| `_archive/docs/SESSION_*.md` | `docs/06-archive/sessions/` | Arkivera gamla sessionsanteckningar. |
|
||||||
|
| `MVP_CHECKLISTA.md` | `docs/06-archive/legacy/` | Arkivera föråldrade planer. |
|
||||||
|
|
||||||
|
### Steg 3: Skapa nya filer
|
||||||
|
|
||||||
|
| Fil | Syfte |
|
||||||
|
|-----------------------------|-----------------------------------------------------------------------|
|
||||||
|
| `docs/03-development/CONTRIBUTING.md` | Standardiserade bidragsregler (branches, PR, code review). |
|
||||||
|
| `docs/03-development/API.md` | Dokumentation av backend-API:er (OpenAPI-länkar, exempel). |
|
||||||
|
| `docs/03-development/MICROSERVICES.md` | Beskrivning av microservices (Importer, AI). |
|
||||||
|
| `docs/05-features/CATEGORY_TREE.md` | Djupdyk i kategorihantering och L3-integration. |
|
||||||
|
| `docs/04-deploy/MAINTENANCE.md` | Backup, monitorering, logghantering. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exempel på innehåll
|
||||||
|
|
||||||
|
### `docs/01-overview/ARCHITECTURE.md`
|
||||||
|
```markdown
|
||||||
|
# Systemarkitektur
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[Flutter App] -->|HTTP/REST| B[Backend API]
|
||||||
|
B -->|Prisma Client| C[MariaDB]
|
||||||
|
B -->|gRPC| D[Importer Microservice]
|
||||||
|
D -->|HTTP| E[Externa API:er]
|
||||||
|
C -->|Seed| F[Initial Data]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Komponenter
|
||||||
|
1. **Flutter App**: State management med Riverpod, UI med Material Design.
|
||||||
|
2. **Backend API**: NestJS + Prisma, autentisering via JWT.
|
||||||
|
3. **Microservices**: Importer (Node.js + Puppeteer), AI (Python + Mistral).
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `docs/03-development/DATABASE.md`
|
||||||
|
```markdown
|
||||||
|
# Databas
|
||||||
|
|
||||||
|
## Schema
|
||||||
|
```mermaid
|
||||||
|
erDiagram
|
||||||
|
User ||--o{ Recipe : creates
|
||||||
|
User ||--o{ InventoryItem : owns
|
||||||
|
Category ||--o{ Product : "L3"
|
||||||
|
FlyerSession ||--o{ FlyerItem : contains
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migrationer
|
||||||
|
### Standardflöde
|
||||||
|
1. Uppdatera `prisma/schema.prisma`.
|
||||||
|
2. Skapa migration:
|
||||||
|
```bash
|
||||||
|
npx prisma migrate dev --name add_feature_x
|
||||||
|
```
|
||||||
|
3. Testa lokalt:
|
||||||
|
```bash
|
||||||
|
npx prisma migrate reset
|
||||||
|
npx prisma db seed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Underhållsskript
|
||||||
|
- **Rensa databas** (behåll kategorier):
|
||||||
|
```bash
|
||||||
|
./deploy.sh --clean-database
|
||||||
|
```
|
||||||
|
> Obs! Uppdatera `prisma/maintenance/clean-database.sql` när nya tabeller läggs till.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fördelar
|
||||||
|
- **Lätt att hitta**: Logisk gruppering (t.ex. all Flutter-dokumentation på ett ställe).
|
||||||
|
- **Uppdaterad**: Arkiverade filer separerade från aktiv dokumentation.
|
||||||
|
- **Optimerad för språkmodeller**: Tydliga rubriker, länkar, och maskinläsbara format.
|
||||||
|
- **Underhållbar**: Modulär struktur gör det enkelt att uppdatera enskilda delar.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nästa steg
|
||||||
|
1. **Godkänn planen**: Bekräfta att strukturen uppfyller era behov.
|
||||||
|
2. **Implementera**: Skapa den nya strukturen och flytta filer stegvis.
|
||||||
|
3. **Uppdatera länkar**: Se till att alla referenser pekar på de nya platserna.
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Plan: Anpassad multiplattformsplan for recipe-app
|
||||||
|
|
||||||
|
## Mal
|
||||||
|
Gora Flutter-klienten i `flutter/` till en riktig multiplattformsklient (web + Android + iOS) utan att bryta befintligt webbflode via Docker/Caddy, och med tydlig miljohantering for API-anrop pa mobil.
|
||||||
|
|
||||||
|
## Nulagesanalys (projektanpassad)
|
||||||
|
- Flutter-projektet ar i praktiken web-only just nu: `flutter/` saknar `android/` och `ios/`.
|
||||||
|
- Webbbygget ar redan etablerat och stabilt via Docker:
|
||||||
|
- `flutter/Dockerfile` bygger `flutter build web --dart-define=API_BASE_URL=/api`.
|
||||||
|
- `compose.flutter.yml` och `flutter/Caddyfile` proxar `/api/*` till `recipe-api:8080`.
|
||||||
|
- API-basurl hanteras redan med `--dart-define` (bra grund for multiplattform):
|
||||||
|
- `flutter/lib/core/api/api_client.dart`
|
||||||
|
- `flutter/lib/features/import/data/import_repository.dart`
|
||||||
|
- Token-lagring ar forberedd for mobil men inte implementerad:
|
||||||
|
- `flutter/lib/core/platform/token_storage.dart`
|
||||||
|
- `flutter/lib/core/platform/platform_providers.dart` (har TODO om secure storage).
|
||||||
|
|
||||||
|
## Viktig skillnad mot gamla planen
|
||||||
|
- Ingen hardkodad `Config.apiUrl` med fasta domaner ska inforas som huvudlosning.
|
||||||
|
- Projektet anvander redan `String.fromEnvironment('API_BASE_URL')`; vi behaller detta och utokar till mobil.
|
||||||
|
- Befintlig Docker-setup for web ska inte ersattas, bara kompletteras med mobil-byggflode.
|
||||||
|
|
||||||
|
## Foreslagen implementation
|
||||||
|
|
||||||
|
### Fas 1: Aktivera plattformsstommar utan att rora webdeploy
|
||||||
|
1. Skapa Android/iOS-mappar i `flutter/`:
|
||||||
|
- `flutter create --platforms android,ios .`
|
||||||
|
2. Verifiera att webfiler och befintliga Dart-filer inte overskrivs pa ett destruktivt satt.
|
||||||
|
3. Bekrafta att Docker-webbygget fortfarande fungerar oforandrat.
|
||||||
|
|
||||||
|
**Leverabel:** Projektet innehaller `flutter/android/` och `flutter/ios/` samtidigt som webflodet ar intakt.
|
||||||
|
|
||||||
|
### Fas 2: Plattformsaker konfiguration av API-basurl
|
||||||
|
1. Standardisera API-konfiguration kring en enda kontraktspunkt:
|
||||||
|
- Behall `API_BASE_URL` via `--dart-define`.
|
||||||
|
2. Satt tydliga miljoer:
|
||||||
|
- Web i Docker: `API_BASE_URL=/api` (som idag).
|
||||||
|
- Android emulator lokalt: t.ex. `http://10.0.2.2:8080/api` (vid lokal backend utan reverse proxy).
|
||||||
|
- Fysisk mobil/test/prod: publik HTTPS-url (doman som ar natbar utanfor Docker).
|
||||||
|
3. Se over alla direkta API-basar i Flutter-koden sa att de gar via samma pattern (inga hardkodade hostnamn).
|
||||||
|
|
||||||
|
**Leverabel:** Samma kodbas fungerar pa web och mobil genom miljoinjektering, inte forks av API-klient.
|
||||||
|
|
||||||
|
### Fas 3: Mobilanpassad auth/tokenlagring
|
||||||
|
1. Implementera `SecureTokenStorage` med `flutter_secure_storage` for mobil.
|
||||||
|
2. Uppdatera `platform_providers.dart` till plattformsval:
|
||||||
|
- Web -> befintlig `WebTokenStorage`.
|
||||||
|
- Android/iOS -> `SecureTokenStorage`.
|
||||||
|
3. Verifiera att inloggning/logout/session beter sig lika mellan web och mobil.
|
||||||
|
|
||||||
|
**Leverabel:** JWT lagras sakert pa mobil, befintligt webbeteende bibehalls.
|
||||||
|
|
||||||
|
### Fas 4: UI- och UX-hardning for mindre skarmar
|
||||||
|
1. Identifiera skarmar med hog informationsdensitet (admin/import/tabeller).
|
||||||
|
2. Lagg in responsiva brytpunkter med `LayoutBuilder`/`MediaQuery` dar det behovs.
|
||||||
|
3. Prioritera funktionellt minimum for mobil i forsta iteration:
|
||||||
|
- Login
|
||||||
|
- Inventarie
|
||||||
|
- Receptlista
|
||||||
|
- Grundlaggande importfloden
|
||||||
|
4. Markera admin-tunga vyer som sekundara om de inte ar mobilkritiska i fas 1.
|
||||||
|
|
||||||
|
**Leverabel:** Nyckelfloden ar anvandbara pa telefon utan horisontell overflow eller blockerande layoutfel.
|
||||||
|
|
||||||
|
### Fas 5: Build- och releaseflode (utan att blanda ihop med Docker-runtime)
|
||||||
|
1. Dokumentera separata kommandon:
|
||||||
|
- Web (befintligt): Docker/compose.
|
||||||
|
- Android: `flutter build apk --release` (och ev. `appbundle`).
|
||||||
|
- iOS: `flutter build ios --release` (kraver macOS/Xcode).
|
||||||
|
2. Behall principen: mobilappar kor inte i Docker; Docker far anvandas som byggmiljo dar det ar rimligt.
|
||||||
|
3. Om CI ska byggas senare: separera web-jobb och mobil-jobb for tydlighet.
|
||||||
|
|
||||||
|
**Leverabel:** Reproducerbar byggprocess for web och mobil med tydlig ansvarsskillnad.
|
||||||
|
|
||||||
|
### Fas 6: Test- och verifieringsplan
|
||||||
|
1. Statisk kvalitet:
|
||||||
|
- `flutter analyze`
|
||||||
|
- `flutter test`
|
||||||
|
2. Plattformsverifiering:
|
||||||
|
- Web via befintlig container
|
||||||
|
- Android emulator + fysisk enhet
|
||||||
|
- iOS simulator (pa macOS)
|
||||||
|
3. Natverksverifiering:
|
||||||
|
- Bekrafta att mobil kan na vald `API_BASE_URL` over HTTPS/CORS/proxyregler.
|
||||||
|
4. Regression:
|
||||||
|
- Inloggning, token-refresh/logout
|
||||||
|
- CRUD i inventarie/recept
|
||||||
|
- Importendpoints med storre payloads
|
||||||
|
|
||||||
|
**Leverabel:** Checklista med passerade verifieringspunkter innan distribution.
|
||||||
|
|
||||||
|
## Konkreta filer som sannolikt berors
|
||||||
|
- `flutter/pubspec.yaml` (nytt beroende for secure storage)
|
||||||
|
- `flutter/lib/core/platform/platform_providers.dart`
|
||||||
|
- `flutter/lib/core/platform/token_storage.dart` (ev. endast kontraktsjustering)
|
||||||
|
- Ny fil: `flutter/lib/core/platform/secure_token_storage.dart`
|
||||||
|
- Mobilplattformar som genereras: `flutter/android/**`, `flutter/ios/**`
|
||||||
|
- Dokumentation: `README.md` och/eller `TEKNISK_BESKRIVNING.md` (kommandon och miljoexempel)
|
||||||
|
|
||||||
|
## Risker och hantering
|
||||||
|
- iOS-bygg kan inte verifieras i Windows/Linux-miljo -> hanteras med separat macOS-steg.
|
||||||
|
- Hardkodade URL:er kan smyga sig in i featurekod -> hanteras med kodsok + central konfigpolicy.
|
||||||
|
- UI-regression pa web vid responsiva andringar -> hanteras med web-regressionstest av kritiska vyer.
|
||||||
|
|
||||||
|
## Prioriterad ordning for implementation
|
||||||
|
1. Fas 1 (plattformsstommar)
|
||||||
|
2. Fas 2 (API-konfiguration)
|
||||||
|
3. Fas 3 (secure token storage)
|
||||||
|
4. Fas 6 del 1 (analyze/test tidigt)
|
||||||
|
5. Fas 4 (mobil UI-hardning)
|
||||||
|
6. Fas 5 + Fas 6 slutlig verifiering och dokumentation
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- Flutter-projektet bygger for web + android (och ios dar macOS finns).
|
||||||
|
- Mobil och web anvander samma API-konfigmodell via `--dart-define`.
|
||||||
|
- Mobil lagrar token sakert; webflodet ar oforandrat.
|
||||||
|
- Minst nyckelfloden login/inventarie/recept/import ar verifierade pa mobil.
|
||||||
|
- Dokumentationen beskriver exakt hur man bygger och kor respektive plattform i detta repo.
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# Nasta steg
|
|
||||||
|
|
||||||
Detta ar huvudroadmap for Recipe App.
|
|
||||||
All detaljhistorik och djup teknisk bakgrund finns i respektive tekniska dokument.
|
|
||||||
|
|
||||||
## Dokumentstatus (2026-05-03)
|
|
||||||
|
|
||||||
- Fokus: en gemensam prioriteringslista for produkt, utveckling och drift.
|
|
||||||
- Delplaner for underomraden ska referera hit, inte duplicera hela roadmapen.
|
|
||||||
|
|
||||||
## Nyligen klart
|
|
||||||
|
|
||||||
- Kvittoimport: förbättrad antal/förpackningsinferens och robustare regelmotor.
|
|
||||||
- Kategorisering: utökade brödregler + contradiction guards och nya regler för pasta, grädde, ägg, juice, godis, och potatis.
|
|
||||||
- Kategoriträd: nya noder `Korvbröd` under `Fastfoodbröd` och `Grädde` under `Matlagning` i seed-data.
|
|
||||||
- Flutter: klientpersistens för pågående kvittoimport.
|
|
||||||
- Produktmodell: user-scoped produkter och seed renodlad till kategorier.
|
|
||||||
- Testinfrastruktur: parametriserade enhetstester för kvittoimport (18 testfall).
|
|
||||||
- CI/CD: GitHub Actions-pipeline för automatiserad testkörning vid push och pull request.
|
|
||||||
- **Node.js versionsparitet:** `package-lock.json` spåras nu i git för båda repos; Dockerfiles kör `npm ci` — reproducerbara byggen i alla miljöer.
|
|
||||||
- **PDF-kvittoimport stabiliserad:** `pdf-parse` CJS-fix + `pdfjs-dist/legacy` fallback löser `DOMMatrix`-fel i Node.js.
|
|
||||||
- **Felkods-forwarding fixad:** `receipt-import.service.ts` i recipe-api vidarebefordrar nu 503 från importer-api som `ServiceUnavailableException` istället för att alltid returnera 400.
|
|
||||||
- **AI-optimering (PDF):** `looksLikeReceiptProductLine()` i importer-api filtrerar bort header/footer-rader — Mistral anropas enbart för rader som faktiskt kan vara produkter.
|
|
||||||
- **Receptparser hardening:** gemensam parserutility, intervall/parantes/brak-forbattringar samt testtackning for parsern.
|
|
||||||
- **Receptsakerhet och dataintegritet:** aktiv-produktvalidering, transaktion vid update, orphan-fil-cleanup och striktare owner-hantering av legacy-recept.
|
|
||||||
- **Alternativa ingredienser (Option A):** lagring i `alternativeProductIds` (JSON), matchning mot flera alternativ och lagerkoll som inkluderar alternativ.
|
|
||||||
- **Flutter felhantering:** kopierbara felmeddelanden i snackbar + global textselektion for enklare support/felsokning.
|
|
||||||
|
|
||||||
## Huvudprioriteringar
|
|
||||||
|
|
||||||
1. Aliasstrategi i kvittoimport: user-scope som standard, global fallback via admin.
|
|
||||||
3. Stabilisera bildimport och diagnostik i alla miljoer.
|
|
||||||
4. Lokalisera kvarvarande stora Flutter-vyer i import/inventarie.
|
|
||||||
5. Forbereda avancerad AI-integration med tydlig loggning/audit.
|
|
||||||
6. Pa borja EAN-stod via Open Food Facts.
|
|
||||||
|
|
||||||
## Beslut som styr arbetet
|
|
||||||
|
|
||||||
- User-scope for data som ar personligt agd.
|
|
||||||
- Backend-kontrakt ar sanningskalla; klienter foljer kontrakten.
|
|
||||||
- Importfunktionalitet ar delegerad till microservice-importer dar det ar beslutat.
|
|
||||||
|
|
||||||
## Framtida förbättringsområden
|
|
||||||
|
|
||||||
### Alternativa ingredienser — migrering till relationsmodell (Option B)
|
|
||||||
|
|
||||||
Nuläge: `RecipeIngredient.alternativeProductIds` lagras som JSON-kolumn (Option A).
|
|
||||||
Detta fungerar men saknar referensintegritet — om en alternativ produkt tas bort uppdateras inte kolumnen automatiskt.
|
|
||||||
|
|
||||||
Framtida lösning: Ersätt JSON-kolumnen med en separat tabell:
|
|
||||||
```prisma
|
|
||||||
model RecipeIngredientAlternative {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
recipeIngredientId Int
|
|
||||||
recipeIngredient RecipeIngredient @relation(fields: [recipeIngredientId], references: [id], onDelete: Cascade)
|
|
||||||
productId Int
|
|
||||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Fördelar: FK-integritet, möjlig sortering/prioritering av alternativ, lättare att querrya.
|
|
||||||
Förutsättning: migration som konverterar befintlig JSON-data till rader i tabellen.
|
|
||||||
|
|
||||||
## Relaterade dokument
|
|
||||||
|
|
||||||
- `README.md` - anvandarperspektiv.
|
|
||||||
- `TEKNISK_BESKRIVNING.md` - teknisk huvudreferens.
|
|
||||||
- `produktlansering.md` - releasechecklista.
|
|
||||||
- `migrering-MSI.md` - migreringshistorik for importer.
|
|
||||||
- `flutter/next_steps_flutter.md` - Flutter-specifik plan.
|
|
||||||
- `_archive/microservice-ai/AI-FUNKTIONER.md` - AI-strategi och historik.
|
|
||||||
@@ -15,8 +15,83 @@ Se även:
|
|||||||
- [TEKNISK_BESKRIVNING.md](TEKNISK_BESKRIVNING.md) för teknisk genomgång.
|
- [TEKNISK_BESKRIVNING.md](TEKNISK_BESKRIVNING.md) för teknisk genomgång.
|
||||||
- [AI-FUNKTIONER.md](_archive/microservice-ai/AI-FUNKTIONER.md) för AI-översikt.
|
- [AI-FUNKTIONER.md](_archive/microservice-ai/AI-FUNKTIONER.md) för AI-översikt.
|
||||||
|
|
||||||
|
# Session 2026-05-09: Simplified Matching Logic (Consolidation Phase 3)
|
||||||
|
|
||||||
|
Denna session har genomfört den tredje omarbetningsfasen som konsoliderar receipt-import matching-flödet:
|
||||||
|
|
||||||
|
## Genomförda förbättringar
|
||||||
|
|
||||||
|
### 1. Unified Matcher (`matchAndEnrichReceiptItem`)
|
||||||
|
- **Tidigare:** Matching var splittrad mellan `matchProducts()` (alias + word-match) och `enrichWithAiCategories()` (~850 rader).
|
||||||
|
- **Nu:** En central metod som gör allt i explicit ordning:
|
||||||
|
1. **Alias lookup** — certifierad match från ReceiptAlias
|
||||||
|
2. **Word-match** — fuzzy produktmatchning med scoring
|
||||||
|
3. **Categorization** — regel-baserad → AI (fallback) → guards → hard overrides
|
||||||
|
|
||||||
|
### 2. Improved Context Management (`prepareMatchingContext`)
|
||||||
|
- Alla data (aliases, produkter, unit mappings, categories) hämtas **en gång** per receipt
|
||||||
|
- Parallell loading med `Promise.all()` — högre performance än tidigare sekventiell loading
|
||||||
|
- Context passeras till alla items för effektiv återanvändning
|
||||||
|
|
||||||
|
### 3. Better Decision Logging (`enrichCategoryForItem`)
|
||||||
|
- Structured trace med steg-för-steg-loggning:
|
||||||
|
- ✓ Rule-based hits with path name
|
||||||
|
- ✓ AI suggestions
|
||||||
|
- ⚠️ Guard remaps (contradiction resolution)
|
||||||
|
- ⚠️ Hard overrides (special cases)
|
||||||
|
- ✅ Final decision with confidence level
|
||||||
|
- Debug-träd med alla decision points
|
||||||
|
- Kan aktiveras per receipt via `RECEIPT_TRACE_DECISIONS` env-var
|
||||||
|
|
||||||
|
### 4. Simplified parseReceipt Flow
|
||||||
|
```
|
||||||
|
Before:
|
||||||
|
parseReceipt() → matchProducts() → enrichWithAiCategories()
|
||||||
|
|
||||||
|
After:
|
||||||
|
parseReceipt() → prepareContext() → matchAndEnrichReceiptItem() (per item)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
**Nya metoder:**
|
||||||
|
- `matchAndEnrichReceiptItem()` — unified matching pipeline per item
|
||||||
|
- `prepareMatchingContext()` — one-time context preparation
|
||||||
|
- `enrichCategoryForItem()` — consolidated categorization logic
|
||||||
|
- `findWordMatchWithScore()` — refactored word matching with explicit score return
|
||||||
|
|
||||||
|
**Datatyper:**
|
||||||
|
- `MatchDecision` interface för strukturerad result (i `dto/match-decision.ts`)
|
||||||
|
|
||||||
|
**Build Status:**
|
||||||
|
- ✅ Full TypeScript compilation successful
|
||||||
|
- ✅ No breaking changes to existing API contracts
|
||||||
|
|
||||||
|
## Cleanup Pending
|
||||||
|
|
||||||
|
De gamla metoderna är fortfarande i koden men kan nu ta bort:
|
||||||
|
- `private async matchProducts()` — deprecated by unified matcher
|
||||||
|
- `private async enrichWithAiCategories()` — deprecated by unified matcher
|
||||||
|
- `private findWordMatch()` — replaced by findWordMatchWithScore()
|
||||||
|
|
||||||
|
**Cleanup tasks:**
|
||||||
|
1. Ta bort `matchProducts()`
|
||||||
|
2. Ta bort `enrichWithAiCategories()`
|
||||||
|
3. Ta bort `findWordMatch()` (gammal version)
|
||||||
|
4. Uppdatera kommentarer/docstrings
|
||||||
|
5. Kör full test suite för regression detection
|
||||||
|
|
||||||
|
Cleanup bör göras i nästa session för att ge tid för monitoring/QA.
|
||||||
|
|
||||||
# Plan för omarbetning av receptimport
|
# Plan för omarbetning av receptimport
|
||||||
|
|
||||||
|
# 2026-05-07: Säkerhets- och deployförbättringar
|
||||||
|
|
||||||
|
- **Inventory är nu user-scopad:** Alla inventory-operationer kräver och filtrerar på userId i backend (schema, migration, service, controller, tester).
|
||||||
|
- **IDOR-skydd för inventory:** Det är nu omöjligt för användare att läsa eller ändra andras inventarieposter. Tester verifierar att åtkomst nekas vid försök till IDOR.
|
||||||
|
- **.gitignore och deploy-hygien:** backend/dist och backend/tsconfig.tsbuildinfo ignoreras och är ej längre spårade i git. .env och .env.* ignoreras, men .env.example finns och är uppdaterad.
|
||||||
|
- **CI/CD-härdning:** npm audit och prisma validate körs i pipeline. Alla tester och byggen måste passera.
|
||||||
|
|
||||||
## Bakgrund
|
## Bakgrund
|
||||||
|
|
||||||
Nuvarande importflöde blandar ihop två olika ansvar:
|
Nuvarande importflöde blandar ihop två olika ansvar:
|
||||||
@@ -289,7 +364,17 @@ Analysen blir en egen endpoint.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Receptmodul
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Alias-strategi och matchedVia (2026-05-07)
|
||||||
|
|
||||||
|
- Alias-strategin är nu fullt implementerad:
|
||||||
|
- Backend och Flutter stödjer user-alias och globala alias.
|
||||||
|
- matchedVia-badge visas i UI (Alias/Ordmatch/AI).
|
||||||
|
- Användare kan spara egna alias, admins kan spara globala.
|
||||||
|
- Profilsidan har alias-lista för användare, admin-panelen visar produkt-ID och radstruktur.
|
||||||
|
- Backend har 3 nya tester för matchedVia, totalt 66 tester gröna.
|
||||||
|
|
||||||
### Fil: `backend/src/recipes/recipes.module.ts`
|
### Fil: `backend/src/recipes/recipes.module.ts`
|
||||||
|
|
||||||
@@ -729,3 +814,7 @@ Om implementationen ska påbörjas direkt är detta bästa första block:
|
|||||||
5. Uppdatera `create_recipe_screen.dart` så att `_save()` skickar råingredienser i stället för att kasta bort omatchade rader.
|
5. Uppdatera `create_recipe_screen.dart` så att `_save()` skickar råingredienser i stället för att kasta bort omatchade rader.
|
||||||
|
|
||||||
Det blocket ger störst effekt med lägst risk, eftersom det löser kärnproblemet: att importen inte längre förstör receptets innehåll.
|
Det blocket ger störst effekt med lägst risk, eftersom det löser kärnproblemet: att importen inte längre förstör receptets innehåll.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
# Nyheter och förbättringar (2026-05-10)
|
||||||
|
|
||||||
|
- **Admin-inventarie:** Full CRUD, merge, filter, sortering, preview och säkerhet för admin i inventarietabellen. Endast admin kan se och hantera alla användares inventarieposter via nya endpoints och adminpanel i Flutter.
|
||||||
|
- **User-scope och IDOR-skydd:** Inventory och produkter är nu strikt user-scopade. Alla operationer kräver och filtrerar på userId. Tester verifierar att åtkomst nekas vid försök till IDOR.
|
||||||
|
- **Säkerhetshärdning:** DTO-validering, guard-ordning, logging, throttling, merge abuse-skydd, och rollbaserad access är implementerat och testat.
|
||||||
|
- **Optimeringar:** DRY i service-lager, striktare query parsing, preview-cache, API-cleanup, och kodduplication eliminerad.
|
||||||
|
- **Testtäckning:** Utökade enhets-, integrations- och säkerhetstester för alla kritiska flöden.
|
||||||
|
|
||||||
|
# Sessionlogg: Receipt Import Cleanup & Optimization
|
||||||
|
|
||||||
|
Datum: 2026-05-09
|
||||||
|
|
||||||
|
## Mål under sessionen
|
||||||
|
- Rensa bort legacy/deprecated kod i receipt-import.
|
||||||
|
- Förenkla och optimera kvarvarande kod på ett säkert sätt.
|
||||||
|
- Säkerställa att kategori-förslag visas för okända varor i import-UI.
|
||||||
|
|
||||||
|
## Genomförda förändringar
|
||||||
|
|
||||||
|
### 1) Legacy/deprecated kod borttagen (backend)
|
||||||
|
Fil: `backend/src/receipt-import/receipt-import.service.ts`
|
||||||
|
- Borttaget: `inferPackageDebugFromRawName()`
|
||||||
|
- Borttaget: `cachedCategories` + `loadCategories()` + constructor-anrop
|
||||||
|
- Borttaget: `matchProducts()`
|
||||||
|
- Borttaget: `findWordMatch()` (gammal variant)
|
||||||
|
- Borttaget: `enrichWithAiCategories()`
|
||||||
|
|
||||||
|
Fil: `backend/src/receipt-import/receipt-import.controller.ts`
|
||||||
|
- Borttaget endpoint: `POST /receipt-import/refresh-categories` (obsolet efter borttagen cache-metod)
|
||||||
|
|
||||||
|
### 2) Tester uppdaterade till unified matcher
|
||||||
|
Fil: `backend/src/receipt-import/receipt-import.service.spec.ts`
|
||||||
|
- Tester migrerade från anrop av borttagna `matchProducts()` till `matchAndEnrichReceiptItem()`.
|
||||||
|
- Tester gröna efter uppdatering.
|
||||||
|
|
||||||
|
### 3) Säkra förenklingar/optimeringar (backend)
|
||||||
|
Fil: `backend/src/receipt-import/receipt-import.service.ts`
|
||||||
|
- Infört tydliga typer för matchningskontext (`MatchingContext`) och debug-objekt.
|
||||||
|
- Extraherat helpers för:
|
||||||
|
- signaltextbyggande
|
||||||
|
- enhetsmappning
|
||||||
|
- Reducerad duplicering i kategoriseringsflöde.
|
||||||
|
- In-memory index i matchningskontext för snabbare uppslag:
|
||||||
|
- `aliasByReceiptName`
|
||||||
|
- `unitMappingByKey`
|
||||||
|
- Bakåtkompatibilitet behållen via fallback när index-fält saknas.
|
||||||
|
|
||||||
|
### 4) UI-fix: kategori-förslag visades inte för okända varor
|
||||||
|
Fil: `flutter/lib/features/import/presentation/receipt_import_tab.dart`
|
||||||
|
- Fixat så kategori-förslag visas även om rad saknar matchad produkt.
|
||||||
|
- Edit-state fylls nu även för rader med endast kategori-förslag.
|
||||||
|
- Label i UI visar `Kategoriförslag` när produkt saknas.
|
||||||
|
|
||||||
|
### 5) Diagnostik tillagd (backend)
|
||||||
|
Fil: `backend/src/receipt-import/receipt-import.service.ts`
|
||||||
|
- Varningslogg om kategorier inte kunde laddas eller om listan blev tom.
|
||||||
|
- Syfte: snabbare felsökning när kategori-förslag uteblir.
|
||||||
|
|
||||||
|
## Verifiering under sessionen
|
||||||
|
- Backend build: OK (`npm run build`)
|
||||||
|
- Backend tests: OK (66/66)
|
||||||
|
- Flutter analyze (berörda filer): OK
|
||||||
|
|
||||||
|
Notering: terminalhistorik innehöll äldre felutskrifter, men senaste verifieringarna var gröna.
|
||||||
|
|
||||||
|
## Kvar att göra nästa gång
|
||||||
|
1. Deploya senaste backend + flutter till servern.
|
||||||
|
2. Re-testa receipt import med PDF i produktion.
|
||||||
|
3. Bekräfta att rader utan produkt nu visar `Kategoriförslag` direkt i listan.
|
||||||
|
4. Vid fortsatt problem: kontrollera nya varningsloggar från `prepareMatchingContext` i backend-loggar.
|
||||||
|
|
||||||
|
## Snabb fortsättning (checklista)
|
||||||
|
- [ ] Deploy backend
|
||||||
|
- [ ] Deploy flutter
|
||||||
|
- [ ] Import-test med samma PDF
|
||||||
|
- [ ] Verifiera kategori-förslag för okända varor
|
||||||
|
- [ ] Dela loggutdrag om något saknas
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sessionlogg: Produkthantering & UI-optimeringar (samma dag, senare)
|
||||||
|
|
||||||
|
## Mål under denna del
|
||||||
|
- Fixa scroll-issue i kvittoimport-gränssnittet (7 rader men bara 5 synliga).
|
||||||
|
- Implementera product rename/merge för både admins och regular users.
|
||||||
|
- Eliminera kodduplicering i backend-service.
|
||||||
|
- Optimera admin-panel och deploy-process.
|
||||||
|
|
||||||
|
## Genomförda förändringar
|
||||||
|
|
||||||
|
### 1) Scroll-issue fixat i receipt import UI
|
||||||
|
Fil: `flutter/lib/features/import/presentation/receipt_import_tab.dart`
|
||||||
|
- **Problem:** `SizedBox(height: 620px, child: ListView.builder(...))` inuti `SingleChildScrollView` skapade konfligerade scroll-handlers.
|
||||||
|
- **Lösning:** Tog bort `SizedBox`-begränsningen, använd `shrinkWrap: true` och `physics: NeverScrollableScrollPhysics()` på ListView så parent `SingleChildScrollView` hanterar all scrolling.
|
||||||
|
- **Resultat:** ✅ Alla 7 rader nu synliga.
|
||||||
|
|
||||||
|
### 2) Product rename & merge för admin (backend)
|
||||||
|
Fil: `backend/src/products/products.controller.ts`
|
||||||
|
- Två nya endpoints:
|
||||||
|
- `PATCH /products/:id/canonical-name` — admin kan byta namn på vilken produkt som helst
|
||||||
|
- `POST /products/merge` — admin kan slå ihop två produkter
|
||||||
|
- Decorator: `@Roles('admin')`
|
||||||
|
|
||||||
|
### 3) Private product endpoints för vanliga users (backend)
|
||||||
|
Fil: `backend/src/products/products.controller.ts`
|
||||||
|
- Två nya parallella endpoints för user-owned private products:
|
||||||
|
- `PATCH /products/private/:id/canonical-name` — user kan byta namn på egen privat produkt
|
||||||
|
- `POST /products/private/merge` — user kan slå ihop egna privata produkter
|
||||||
|
- JWT extraction: `const userId = req.user.id` (ingen `@Roles`-behov, user kan bara redigera sin egna data)
|
||||||
|
- Security: `ForbiddenException` om produkt inte är privat eller inte ägs av user
|
||||||
|
|
||||||
|
### 4) Backend service refaktorering — kodduplicering eliminerad
|
||||||
|
Fil: `backend/src/products/products.service.ts`
|
||||||
|
- Två nya **private helper-metoder:**
|
||||||
|
- `_updateCanonicalNameCore(id, canonicalName)` — shared logik för trim + Prisma update
|
||||||
|
- `_mergeCore(sourceId, targetId)` — shared logik för transaction, inventory transfer, soft-delete
|
||||||
|
- Alla fyra public metoder (`updateCanonicalName`, `updateCanonicalNamePrivate`, `merge`, `mergePrivate`) använder nu dessa helpers
|
||||||
|
- **Exception fix:** Bytte `throw new Error(...)` till `throw new ForbiddenException(...)` för authorization-fel (korrekt HTTP 403)
|
||||||
|
- **Resultat:** ~80 rader kodduplicering eliminerad, bättre underhållbarhet
|
||||||
|
|
||||||
|
### 5) API-path konstanter för Flutter (frontend)
|
||||||
|
Fil: `flutter/lib/core/api/api_paths.dart`
|
||||||
|
- Lade till två nya constants i `ProductApiPaths`:
|
||||||
|
- `static const mergePrivate = '/products/private/merge'`
|
||||||
|
- `static String canonicalNamePrivate(int id) => '/products/private/$id/canonical-name'`
|
||||||
|
|
||||||
|
### 6) Admin repository uppdaterad (frontend)
|
||||||
|
Fil: `flutter/lib/features/admin/data/admin_repository.dart`
|
||||||
|
- Två nya metoder:
|
||||||
|
- `updateCanonicalNamePrivate(int productId, String canonicalName)` — user rename
|
||||||
|
- `mergeProductsPrivate({required sourceId, required targetId})` — user merge
|
||||||
|
- Komment: "Admin kan uppdatera vilken produkt som helst; users kan bara uppdatera sina egna privata produkter"
|
||||||
|
|
||||||
|
### 7) Admin panel optimeringar (frontend)
|
||||||
|
Fil: `flutter/lib/features/admin/presentation/admin_products_panel.dart`
|
||||||
|
- **Caching av kategorierna:** `_cachedCategoryOptions` beräknas en gång istället för varje build
|
||||||
|
- **Enklare `_nameForId()`:** Bytte från `where().toList()` till en enkel for-loop med early return
|
||||||
|
- **Parallell restore:** `_restoreSelected()` använder `Future.wait()` istället för seriebaserade await
|
||||||
|
- **Expression switch:** `_sortLabel()` förkortat från 12-radigt switch till en enda rad med expression switch
|
||||||
|
|
||||||
|
### 8) Deploy-script förbättring
|
||||||
|
Fil: `deploy.sh`
|
||||||
|
- Nya flaggor för selektiv build:
|
||||||
|
- `--backend` — bygga bara backend
|
||||||
|
- `--flutter` — bygga bara Flutter
|
||||||
|
- `--importer` — bygga bara microservice-importer
|
||||||
|
- `--pull-always` — tvinga Docker att hämta senaste base image (för prod-säkerhet)
|
||||||
|
- Default: bygger alla tre, använder `--pull=false` för snabbhet (ej prod)
|
||||||
|
- `--seed` flag för opt-in databaskällning
|
||||||
|
- Help: `./deploy.sh --help` visar användning
|
||||||
|
|
||||||
|
## Verifiering
|
||||||
|
- ✅ Backend build: OK (`npm run build`)
|
||||||
|
- ✅ Backend tests: OK
|
||||||
|
- ✅ Flutter analyze: OK (alla berörda filer)
|
||||||
|
- ✅ TypeScript-fel: Inga
|
||||||
|
- ✅ Git diff: Alla 4 filer granskat och godkänt
|
||||||
|
|
||||||
|
## Nyheter och förbättringar (2026-05-11)
|
||||||
|
|
||||||
|
- **Inventarie och baslager:** Möjlighet att se, sätta och ändra kategori på produkter direkt i inventarie- och baslagervyn. Identisk, sökbar/autocomplete category-picker i alla relevanta vyer (inventarie, baslager, admin, import). UX-standardisering: samma dropdown och interaktionsmönster överallt.
|
||||||
|
- **Seed-data:** Nya kategorier under `Skafferi > Sylt, mos & marmelad`: `Marmelad`, `Sylt`, `Mos` tillagda i seed.
|
||||||
|
- **Navigation/UI:** Admin-knappen flyttad från sidomenyn till profilmenyn ovanför logout.
|
||||||
|
- **Deploy och seed:** Förtydligande om att seed-data körs automatiskt vid `./deploy.sh --backend` och att seed-kontroll sker i deploy-scriptet. Hur man verifierar att seed körts och att nya kategorier finns.
|
||||||
|
- **Kodförenklingar/optimeringar:** Samtliga tre förenklingar/optimeringar från senaste commit är nu implementerade (se nedan för detaljer).
|
||||||
|
|
||||||
|
## Öppna uppgifter (nästa steg)
|
||||||
|
1. Deploy backend + Flutter med `./deploy.sh --backend --flutter`
|
||||||
|
2. Testa i produktion:
|
||||||
|
- Verifiera 7 rader nu synliga i receipt import
|
||||||
|
- Verifiera admin kan byta namn på produkter
|
||||||
|
- Verifiera admin kan slå ihop produkter
|
||||||
|
- Verifiera users kan redigera sina egna privata produkter
|
||||||
|
3. UI för users: Om private rename/merge ska exponeras i användar-app (backend redan klart, saknas bara UI)
|
||||||
|
4. Unit/integration tests för private endpoints
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Sessionlogg: Aliasstrategi i kvittoimport (samma dag, senare)
|
||||||
|
|
||||||
|
## Mål under denna del
|
||||||
|
- Göra aliasstrategin konsekvent med user-scope som standard och global fallback via admin.
|
||||||
|
- Sluta lära alias automatiskt vid manuell korrigering och kräva explicit val i UI.
|
||||||
|
- Härda backend mot brusiga eller ogiltiga alias.
|
||||||
|
|
||||||
|
## Genomförda förändringar
|
||||||
|
|
||||||
|
### 1) Gemensam aliasnormalisering och guardrails (backend)
|
||||||
|
Filer:
|
||||||
|
- `backend/src/common/utils/receipt-alias.ts`
|
||||||
|
- `backend/src/receipt-alias/receipt-alias.service.ts`
|
||||||
|
- `backend/src/receipt-import/receipt-import.service.ts`
|
||||||
|
|
||||||
|
- Infört gemensam utility för aliasnormalisering (`trim`, lowercase, kollapsad whitespace).
|
||||||
|
- Infört validering som blockerar tomma alias och brusiga alias som `rabatt`, `summa`, `pant`, `att betala`, `totalt`, m.fl.
|
||||||
|
- Receipt import och alias-API använder nu samma regler för både lookup och sparande.
|
||||||
|
|
||||||
|
### 2) Receipt import lär inte längre alias automatiskt (Flutter)
|
||||||
|
Filer:
|
||||||
|
- `flutter/lib/features/import/data/receipt_import_session.dart`
|
||||||
|
- `flutter/lib/features/import/presentation/edit_dialog.dart`
|
||||||
|
- `flutter/lib/features/import/presentation/receipt_import_tab.dart`
|
||||||
|
|
||||||
|
- Infört explicit `learnAlias`-val i edit-dialogen.
|
||||||
|
- Alias sparas nu bara om användaren aktivt markerar att kvittonamnet ska läras in.
|
||||||
|
- Valet persisteras i receipt import-sessionen så att tabbyte inte tappar användarens val.
|
||||||
|
- Om raden redan matchades via alias visas förklarande text i stället för ny aliasinlärning.
|
||||||
|
|
||||||
|
### 3) Aliasöversikter visar scope tydligare (Flutter)
|
||||||
|
Filer:
|
||||||
|
- `flutter/lib/features/admin/domain/receipt_alias.dart`
|
||||||
|
- `flutter/lib/features/profile/presentation/user_aliases_screen.dart`
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_aliases_panel.dart`
|
||||||
|
|
||||||
|
- Aliasmodellen utökad med `ownerId` och `isGlobal`.
|
||||||
|
- User alias-skärmen visar nu skillnad mellan `Privat alias` och `Global fallback`.
|
||||||
|
- Delete-knapp visas bara för privata alias i användarvyn, så UI:t matchar backend-behörigheten.
|
||||||
|
- Adminpanelen visar scope även för aliasposter.
|
||||||
|
|
||||||
|
### 4) Tester för aliasflödet
|
||||||
|
Filer:
|
||||||
|
- `backend/src/receipt-import/receipt-import.service.spec.ts`
|
||||||
|
- `backend/src/receipt-alias/receipt-alias.service.spec.ts`
|
||||||
|
|
||||||
|
- Tester tillagda för normalisering av whitespace vid alias-lookup.
|
||||||
|
- Tester tillagda för alias-upsert med normalisering.
|
||||||
|
- Tester tillagda för blockering av brusalias.
|
||||||
|
- Tester tillagda för behörighetsregler kring globala alias och borttagning.
|
||||||
|
|
||||||
|
## Verifiering
|
||||||
|
- ✅ Backend tests: 31/31 gröna för berörda aliasspecar
|
||||||
|
- ✅ Flutter analyze: OK för alla berörda alias/import-filer
|
||||||
|
|
||||||
|
## Kvar att göra
|
||||||
|
1. Manuell test i appen: receipt import med explicit alias-inlärning.
|
||||||
|
2. Produktionstest: verifiera att privata alias och global fallback beter sig rätt mot riktiga kvitton.
|
||||||
|
3. Bedöm om aliasöversikterna behöver mer avancerad filtrering eller redigering senare.
|
||||||
|
|
||||||
|
## Snabb checklista för nästa session
|
||||||
|
- [ ] Deploy backend + Flutter
|
||||||
|
- [ ] Testa scroll-fix i prod
|
||||||
|
- [ ] Testa admin rename/merge
|
||||||
|
- [ ] Testa private endpoints (API-test eller manual)
|
||||||
|
- [ ] Implementera user-UI för private rename/merge (valfritt)
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Session Checkpoint (2026-05-11)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- Branch/worktree: ändringar finns lokalt och är analyserade med `flutter analyze`.
|
||||||
|
- Senaste verifiering: `flutter analyze lib/core/utils/display_labels.dart lib/features/inventory/presentation/swipeable_inventory_tile.dart lib/features/pantry/presentation/pantry_screen.dart` -> inga fel.
|
||||||
|
|
||||||
|
## Klart i denna session
|
||||||
|
|
||||||
|
1. Kategori-chip i `/inventory` och `/baslager` visar nu djupaste kategori (L3 om den finns, annars L2/L1) utan prefix (`L1/L2/L3`).
|
||||||
|
2. Hover/long-press på chip visar full kategoriväg (`L1 > L2 > L3`) via tooltip.
|
||||||
|
3. Cache-invalidation fixad efter kategoriändring i admin:
|
||||||
|
- Invaliderar både `inventoryProvider` och `pantryProvider` i globala produktflöden.
|
||||||
|
4. Kodstädning/optimering:
|
||||||
|
- Gemensam helper returnerar både chip-label och tooltip i en parsing.
|
||||||
|
- Oanvänd helper borttagen.
|
||||||
|
5. Dokumentation uppdaterad och rubrik-konflikt städad för 2026-05-11-sektionerna.
|
||||||
|
|
||||||
|
## Ändrade filer
|
||||||
|
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_products_panel.dart`
|
||||||
|
- `flutter/lib/core/utils/display_labels.dart`
|
||||||
|
- `flutter/lib/features/inventory/presentation/swipeable_inventory_tile.dart`
|
||||||
|
- `flutter/lib/features/pantry/presentation/pantry_screen.dart`
|
||||||
|
- `TEKNISK_BESKRIVNING.md`
|
||||||
|
|
||||||
|
## Viktiga beteenderegler som nu gäller
|
||||||
|
|
||||||
|
- Chiptext: visar endast djupaste kategorinamn.
|
||||||
|
- Tooltip: visar full kategori-path.
|
||||||
|
- Om path saknas: fallback till L1/"Övrigt" enligt befintlig modell.
|
||||||
|
|
||||||
|
## Kvar / nästa steg (rekommenderat)
|
||||||
|
|
||||||
|
1. Lägg widgettester för category-chip:
|
||||||
|
- inventory: djupaste label + full tooltip
|
||||||
|
- baslager: djupaste label + full tooltip
|
||||||
|
2. Besluta om sortering ska följa visad kategori (djupaste nod) eller fortsatt L1.
|
||||||
|
3. Commit/pusha ändringarna när du är nöjd.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uppdatering 2026-05-12
|
||||||
|
|
||||||
|
### Klart nu
|
||||||
|
|
||||||
|
1. Widgettester tillagda för category-chip/tooltip:
|
||||||
|
- `flutter/test/features/inventory/presentation/swipeable_inventory_tile_test.dart`
|
||||||
|
- `flutter/test/features/pantry/presentation/pantry_screen_category_chip_test.dart`
|
||||||
|
2. Sorteringsbeslut implementerat:
|
||||||
|
- Sortering på `Kategori (A-O)` följer nu visad kategori (djupaste nod), inte enbart L1.
|
||||||
|
- Uppdaterat i både inventory- och baslagervyn.
|
||||||
|
|
||||||
|
### Deploy + manuell produktionstest (körplan)
|
||||||
|
|
||||||
|
Kör deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy.sh --backend --flutter
|
||||||
|
```
|
||||||
|
|
||||||
|
Om du vill tvinga kontroll av nya basimages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy.sh --backend --flutter --pull-always
|
||||||
|
```
|
||||||
|
|
||||||
|
Verifiera drift efter deploy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f compose.yml -f compose.flutter.yml ps
|
||||||
|
curl http://localhost:8080/api/health
|
||||||
|
curl http://localhost:8080/api/health/db
|
||||||
|
```
|
||||||
|
|
||||||
|
Manuell produktionschecklista:
|
||||||
|
|
||||||
|
- [ ] Receipt import: verifiera att alla rader syns (scroll-fix).
|
||||||
|
- [ ] Receipt import: verifiera att okända varor visar `Kategoriförslag`.
|
||||||
|
- [ ] Inventory: verifiera category-chip (djupaste nod) + tooltip (full path).
|
||||||
|
- [ ] Baslager: verifiera category-chip (djupaste nod) + tooltip (full path).
|
||||||
|
- [ ] Admin: verifiera rename/merge av produkter.
|
||||||
|
- [ ] User: verifiera private rename/merge-behörighet och funktion.
|
||||||
|
|
||||||
|
Fallback vid avvikelse:
|
||||||
|
|
||||||
|
- Spara backend-loggar för receipt import och kategori-matchning.
|
||||||
|
- Notera exakt testdata (kvitto/PDF, produktnamn, förväntat vs faktiskt utfall).
|
||||||
|
|
||||||
|
## Snabb återstart nästa gång
|
||||||
|
|
||||||
|
Kör:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
git diff -- flutter/lib/core/utils/display_labels.dart \
|
||||||
|
flutter/lib/features/inventory/presentation/swipeable_inventory_tile.dart \
|
||||||
|
flutter/lib/features/pantry/presentation/pantry_screen.dart \
|
||||||
|
flutter/lib/features/admin/presentation/admin_products_panel.dart \
|
||||||
|
TEKNISK_BESKRIVNING.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Och därefter:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd flutter
|
||||||
|
flutter analyze lib/core/utils/display_labels.dart \
|
||||||
|
lib/features/inventory/presentation/swipeable_inventory_tile.dart \
|
||||||
|
lib/features/pantry/presentation/pantry_screen.dart
|
||||||
|
```
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Session Checkpoint (2026-05-12)
|
||||||
|
|
||||||
|
> Föregående checkpoint: [SESSION_CHECKPOINT_2026-05-11.md](SESSION_CHECKPOINT_2026-05-11.md)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- Alla ändringar är lokalt implementerade och verifierade (tester gröna, inga type-fel, 0 npm-sårbarheter).
|
||||||
|
- Ej committat/pushat.
|
||||||
|
|
||||||
|
## Klart i denna session
|
||||||
|
|
||||||
|
### 1. Alias-scopehantering (admin kan höja privat → globalt)
|
||||||
|
|
||||||
|
**Backend (`receipt-alias`):**
|
||||||
|
- `UpdateReceiptAliasDto` utökat med `isGlobal?: boolean` (`@IsBoolean()`).
|
||||||
|
- `receipt-alias.service.ts`: `update()` hanterar scope-byte med:
|
||||||
|
- Admin-only-regel: vanlig användare → `ForbiddenException`.
|
||||||
|
- Global → privat utan owner → `BadRequestException`.
|
||||||
|
- Beräknar `nextIsGlobal` + `nextOwnerId` korrekt och sparar.
|
||||||
|
- Konfliktcheck baseras på nästa scope, inte gammalt.
|
||||||
|
|
||||||
|
**Admin-UI (`flutter/lib/features/admin/presentation/admin_aliases_panel.dart`):**
|
||||||
|
- Edit-dialog har scope-switch.
|
||||||
|
- Switch är **låst (disabled)** om aliaset redan är globalt — endast privat → global är möjligt.
|
||||||
|
- Subtitel förklarar läget dynamiskt.
|
||||||
|
- Bekräftelse-chip visas i listan i 6 sekunder efter genomförd scope-ändring.
|
||||||
|
- `_scopeChangedAliasId` / `_scopeChangedToGlobal` state-fält med Timer + dispose-cleanup.
|
||||||
|
|
||||||
|
**Admin repository (`flutter/lib/features/admin/data/admin_repository.dart`):**
|
||||||
|
- `updateReceiptAlias` tar emot `bool? isGlobal` och inkluderar det i PATCH-body.
|
||||||
|
|
||||||
|
**Tester (16/16 passerar):**
|
||||||
|
- `receipt-alias.service.spec.ts`: admin kan höja privat → global; användare blockeras; global → privat utan owner ger `BadRequestException`.
|
||||||
|
- `receipt-alias.security.spec.ts`: controller delegerar DTO med `isGlobal` korrekt.
|
||||||
|
|
||||||
|
### 2. Kategori-chip fallback
|
||||||
|
|
||||||
|
- Den gemensamma chip-buildern returnerar nu `SizedBox.shrink()` vid tom kategoriväg — "okänd"-chip visas inte längre i admin- eller importpaneler.
|
||||||
|
|
||||||
|
### 3. Beroendehärdning
|
||||||
|
|
||||||
|
**recipe-app backend:**
|
||||||
|
- Uppgraderat till **NestJS 11** (`@nestjs/common/core/platform-express/testing` 11.1.19, `@nestjs/jwt` 11.0.2, `@nestjs/passport` 11.0.5).
|
||||||
|
- `multer` uppgraderat till **2.1.1** (åtgärdar high-severity CVE i transitivt beroende).
|
||||||
|
- `@types/express` → **5.0.5**, `@nestjs/cli` → **11.0.21**, `@nestjs/schematics` → **11.1.0**.
|
||||||
|
- `jwt.strategy.ts`: fail-fast check för `JWT_SECRET` + typed `secretOrKey`.
|
||||||
|
- `JWT_SECRET: ci-test-secret` satt i CI-env i `.gitea/workflows/test.yml`.
|
||||||
|
- `npm audit --audit-level=high` = **0 sårbarheter**.
|
||||||
|
|
||||||
|
**microservice-importer backend:**
|
||||||
|
- Samma uppgradering till Nest 11 + multer 2.1.1 + `@types/express` 5.
|
||||||
|
- `npm audit --audit-level=high` = **0 sårbarheter**.
|
||||||
|
|
||||||
|
### 4. Quality-gate scripts
|
||||||
|
|
||||||
|
**recipe-app/backend/package.json** – nya scripts:
|
||||||
|
|
||||||
|
| Script | Syfte |
|
||||||
|
|---|---|
|
||||||
|
| `prisma:validate` | `prisma validate --schema prisma/schema.prisma` |
|
||||||
|
| `typecheck` | `tsc --noEmit` |
|
||||||
|
| `audit:high` | `npm audit --audit-level=high` |
|
||||||
|
| `quality:ci` | Kedja: validate → generate → typecheck → test → build → audit |
|
||||||
|
|
||||||
|
**recipe-app/.gitea/workflows/test.yml** – uppdateringar:
|
||||||
|
- Prisma-steg kör `npm run prisma:validate`.
|
||||||
|
- Audit-steg kör `npm run audit:high`.
|
||||||
|
- Nytt **Typecheck backend**-steg i PR-snabbjobb och push-fulljobb.
|
||||||
|
|
||||||
|
**microservice-importer/backend/package.json** – nya scripts:
|
||||||
|
|
||||||
|
| Script | Syfte |
|
||||||
|
|---|---|
|
||||||
|
| `typecheck` | `tsc --noEmit` |
|
||||||
|
| `audit:high` | `npm audit --audit-level=high` |
|
||||||
|
| `quality:ci` | Kedja: typecheck → build → audit |
|
||||||
|
|
||||||
|
**microservice-importer/.github/workflows/test.yml** – uppdateringar:
|
||||||
|
- `npm install` → `npm ci`.
|
||||||
|
- Prisma/test-steg ersatta med: typecheck → build → audit.
|
||||||
|
- `continue-on-error` borttagen från build.
|
||||||
|
|
||||||
|
## Ändrade filer (recipe-app)
|
||||||
|
|
||||||
|
- `backend/src/receipt-alias/dto/update-receipt-alias.dto.ts`
|
||||||
|
- `backend/src/receipt-alias/receipt-alias.service.ts`
|
||||||
|
- `backend/src/receipt-alias/receipt-alias.service.spec.ts`
|
||||||
|
- `backend/src/receipt-alias/receipt-alias.security.spec.ts`
|
||||||
|
- `backend/src/auth/jwt.strategy.ts`
|
||||||
|
- `backend/package.json`
|
||||||
|
- `backend/package-lock.json`
|
||||||
|
- `.gitea/workflows/test.yml`
|
||||||
|
- `flutter/lib/features/admin/data/admin_repository.dart`
|
||||||
|
- `flutter/lib/features/admin/presentation/admin_aliases_panel.dart`
|
||||||
|
- `TEKNISK_BESKRIVNING.md`
|
||||||
|
- `NEXT_STEPS.md`
|
||||||
|
- `_archive/docs/SESSION_CHECKPOINT_2026-05-12.md` (denna fil)
|
||||||
|
|
||||||
|
## Ändrade filer (microservice-importer)
|
||||||
|
|
||||||
|
- `backend/package.json`
|
||||||
|
- `.github/workflows/test.yml`
|
||||||
|
- `TEKNISK_BESKRIVNING.md`
|
||||||
|
- `next_steps_MSImporter.md`
|
||||||
|
|
||||||
|
## Snabb återstart nästa gång
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verifiera status
|
||||||
|
cd recipe-app/backend
|
||||||
|
npm run quality:ci
|
||||||
|
|
||||||
|
cd ../../microservice-importer/backend
|
||||||
|
npm run quality:ci
|
||||||
|
```
|
||||||
|
|
||||||
|
Flutter-analys:
|
||||||
|
```bash
|
||||||
|
cd recipe-app/flutter
|
||||||
|
flutter analyze lib/features/admin/presentation/admin_aliases_panel.dart
|
||||||
|
```
|
||||||
|
|
||||||
|
Alias-tester:
|
||||||
|
```bash
|
||||||
|
cd recipe-app/backend
|
||||||
|
npm test -- src/receipt-alias/receipt-alias.service.spec.ts src/receipt-alias/receipt-alias.security.spec.ts --runInBand
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nästa rekommenderade steg
|
||||||
|
|
||||||
|
1. Commit och push av samtliga lokala ändringar.
|
||||||
|
2. Deploy till produktionsmiljö och verifiera alias-flöde med riktiga kvitton.
|
||||||
|
3. Manuell verifiering: admin kan höja privat → globalt via UI; vanlig användare kan inte.
|
||||||
|
4. Widgettester för alias-panelens switch (privat → global, disabled-state om globalt).
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Session Checkpoint (2026-05-21)
|
||||||
|
|
||||||
|
> Föregående checkpoint: [SESSION_CHECKPOINT_2026-05-12.md](SESSION_CHECKPOINT_2026-05-12.md)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- Arbetsytan är ren (`git status --short` gav ingen output).
|
||||||
|
- Kritiska build-blockers för Flutter-l10n är åtgärdade.
|
||||||
|
- Backend build + backend tester + Flutter tester verifierade gröna i denna session.
|
||||||
|
|
||||||
|
## Klart i denna session
|
||||||
|
|
||||||
|
### 1. Felsökning och fix av Docker-fel i Flutter `gen-l10n`
|
||||||
|
|
||||||
|
**Problem:** Docker-bygg kraschade vid `flutter gen-l10n` p.g.a. ogiltig ARB-JSON och konflikt i locale-filer.
|
||||||
|
|
||||||
|
**Åtgärder:**
|
||||||
|
- `flutter/lib/l10n/app_en.arb` reparerad (felaktig JSON-struktur, saknade/utanförliggande nycklar).
|
||||||
|
- Krock mellan engelska locale-filer hanterad (dubbla `en`-källor var en del av tidigare felsymptom).
|
||||||
|
- `flutter gen-l10n` kördes om utan formatteringsfel.
|
||||||
|
|
||||||
|
### 2. Fix av Flutter test-fel: saknad l10n-nyckel `required`
|
||||||
|
|
||||||
|
**Problem:** `flutter test` föll på:
|
||||||
|
- `The getter 'required' isn't defined for the type 'AppLocalizations'`
|
||||||
|
- fel i `lib/features/admin/presentation/admin_users_panel.dart`.
|
||||||
|
|
||||||
|
**Åtgärder:**
|
||||||
|
- Återställde saknade nycklar i `flutter/lib/l10n/app_en.arb`:
|
||||||
|
- `required`
|
||||||
|
- `logoutAction`
|
||||||
|
- `adminAiDescription`
|
||||||
|
- `adminPagePrefix`
|
||||||
|
- Synkade svenska ARB-filen och la till saknad nyckel:
|
||||||
|
- `profileDatabaseDescription`
|
||||||
|
- Regenererade lokaliseringar med `flutter gen-l10n`.
|
||||||
|
|
||||||
|
### 3. Kvalitetsverifiering
|
||||||
|
|
||||||
|
Körda verifieringar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
cd backend
|
||||||
|
npm run build
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# Flutter
|
||||||
|
cd ../flutter
|
||||||
|
flutter gen-l10n
|
||||||
|
flutter test --reporter compact
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resultat:**
|
||||||
|
- Backend build: OK
|
||||||
|
- Backend tests: OK (29/29 suites, 245/245 tester)
|
||||||
|
- Flutter tests: OK (alla passerar)
|
||||||
|
|
||||||
|
## Viktig kontext inför nästa session
|
||||||
|
|
||||||
|
- Root-varningen från Flutter i Docker (`trying to run flutter as root`) är en varning och blockerar inte i sig.
|
||||||
|
- Den blockerande orsaken var ARB/l10n-konsistens, inte root-varningen.
|
||||||
|
- Nuvarande l10n-läge är stabilt efter regeneration.
|
||||||
|
|
||||||
|
## Rekommenderad snabbstart imorgon
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) Verifiera ren arbetsyta
|
||||||
|
git status --short
|
||||||
|
|
||||||
|
# 2) Reprova hela lokala verifieringen
|
||||||
|
cd backend
|
||||||
|
npm run build && npm run test
|
||||||
|
|
||||||
|
cd ../flutter
|
||||||
|
flutter gen-l10n
|
||||||
|
flutter test --reporter compact
|
||||||
|
|
||||||
|
# 3) Om allt är grönt, kör deploy/build-pipeline igen
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ändrade filer i denna session (huvudsakligen)
|
||||||
|
|
||||||
|
- `flutter/lib/l10n/app_en.arb`
|
||||||
|
- `flutter/lib/l10n/app_sv.arb`
|
||||||
|
- genererade l10n-filer under `flutter/lib/l10n/generated/*`
|
||||||
|
- mindre korrigeringar i backend-test/service under felsökningen, slutläge verifierat grönt.
|
||||||
+27
-8
@@ -1,4 +1,12 @@
|
|||||||
# 🔒 Säkerhetshärdningsplan för Recipe-App (Flutter + NestJS + MariaDB)
|
# 🔒 Säkerhetshärdningsplan för Recipe-App (Flutter + NestJS + MariaDB)
|
||||||
|
# Nyheter och förbättringar (2026-05-10)
|
||||||
|
|
||||||
|
- **Admin-inventarie:** Endast admin har tillgång till CRUD, merge, filter, sortering och preview för alla användares inventarieposter. Endpoints och UI är skyddade med @Roles('admin') och testade.
|
||||||
|
- **User-scope och IDOR-skydd:** Inventory och produkter är strikt user-scopade. Alla operationer kräver och filtrerar på userId. Tester verifierar att åtkomst nekas vid försök till IDOR.
|
||||||
|
- **Säkerhetshärdning:** DTO-validering, guard-ordning, logging, throttling, merge abuse-skydd, och rollbaserad access är implementerat och testat.
|
||||||
|
- **Optimeringar:** DRY i service-lager, striktare query parsing, preview-cache, API-cleanup, och kodduplication eliminerad.
|
||||||
|
- **Testtäckning:** Utökade enhets-, integrations- och säkerhetstester för alla kritiska flöden.
|
||||||
|
|
||||||
|
|
||||||
**Reviderad:** 2026-05-07 — baserad på faktisk kodgranskning av repo.
|
**Reviderad:** 2026-05-07 — baserad på faktisk kodgranskning av repo.
|
||||||
|
|
||||||
@@ -372,14 +380,14 @@ gitleaks protect --staged # kör före varje commit
|
|||||||
| **Åtgärd** | **Status** | **Ansvarsområde** |
|
| **Åtgärd** | **Status** | **Ansvarsområde** |
|
||||||
| -------------------------------- | ---------- | ----------------------- |
|
| -------------------------------- | ---------- | ----------------------- |
|
||||||
| IDOR-skydd: recipes, pantry, meal-plan, receipt-alias | ✅ Klart | Backend |
|
| IDOR-skydd: recipes, pantry, meal-plan, receipt-alias | ✅ Klart | Backend |
|
||||||
| IDOR-skydd + userId-filtrering för **inventory** | ⬜ **CRITICAL** | Backend (NestJS/Prisma) |
|
| IDOR-skydd + userId-filtrering för **inventory** | ✅ Klart | Backend (NestJS/Prisma) |
|
||||||
| `dist/` tillagd i `.gitignore` | ⬜ **HIGH** | Git |
|
| `dist/` tillagd i `.gitignore` | ✅ Klart | Git |
|
||||||
| `.env` (utan suffix) i `.gitignore` + `.env.example` | ⬜ MEDIUM | Git |
|
| `.env` (utan suffix) i `.gitignore` + `.env.example` | ✅ Klart | Git |
|
||||||
| Helmet, CORS, ThrottlerGuard, ValidationPipe | ✅ Klart | Backend |
|
| Helmet, CORS, ThrottlerGuard, ValidationPipe | ✅ Klart | Backend |
|
||||||
| Flutter: token-abstraktion (`ITokenStorage`) | ✅ Klart | Flutter |
|
| Flutter: token-abstraktion (`ITokenStorage`) | ✅ Klart | Flutter |
|
||||||
| Flutter: httpOnly cookie-alternativ (om XSS är reell risk) | ⬜ LOW | Flutter + Backend |
|
| Flutter: httpOnly cookie-alternativ (om XSS är reell risk) | ⬜ LOW | Flutter + Backend |
|
||||||
| Gitea webhook-signaturvalidering (om webhooks används) | ⬜ LOW | Backend |
|
| Gitea webhook-signaturvalidering (om webhooks används) | ⬜ LOW | Backend |
|
||||||
| `npm audit` i CI-pipeline | ⬜ MEDIUM | CI/CD (Gitea) |
|
| `npm audit` i CI-pipeline | ✅ Klart | CI/CD (Gitea) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -387,9 +395,20 @@ gitleaks protect --staged # kör före varje commit
|
|||||||
|
|
||||||
## 🎯 **8. Prioriterad Ordning för Implementering**
|
## 🎯 **8. Prioriterad Ordning för Implementering**
|
||||||
|
|
||||||
1. **Inventory IDOR + userId-fält** (CRITICAL) — påverkar alla användares data
|
1. **Inventory IDOR + userId-fält** (CRITICAL) — ✅ KLART 2026-05-07
|
||||||
2. **dist/ i .gitignore** (HIGH) — förhindrar återkommande deploy-problem
|
2. **dist/ i .gitignore** (HIGH) — ✅ KLART 2026-05-07
|
||||||
3. **bare .env + .env.example** (MEDIUM)
|
3. **bare .env + .env.example** (MEDIUM) — ✅ KLART 2026-05-07
|
||||||
4. **npm audit i CI** (MEDIUM)
|
4. **npm audit i CI** (MEDIUM) — ✅ KLART 2026-05-07
|
||||||
5. **Flutter httpOnly cookies** (LOW — kräver arkitekturförändring)
|
5. **Flutter httpOnly cookies** (LOW — kräver arkitekturförändring)
|
||||||
6. **Gitea webhook-validering** (LOW — bara relevant om webhooks används)
|
6. **Gitea webhook-validering** (LOW — bara relevant om webhooks används)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-07: Sammanfattning av senaste säkerhetsförbättringar
|
||||||
|
|
||||||
|
- **Inventory är nu user-scopad:** Alla inventory-operationer kräver och filtrerar på userId i backend (schema, migration, service, controller, tester).
|
||||||
|
- **IDOR-skydd för inventory:** Det är nu omöjligt för användare att läsa eller ändra andras inventarieposter. Tester verifierar att åtkomst nekas vid försök till IDOR.
|
||||||
|
- **.gitignore och deploy-hygien:** backend/dist och backend/tsconfig.tsbuildinfo ignoreras och är ej längre spårade i git. .env och .env.* ignoreras, men .env.example finns och är uppdaterad.
|
||||||
|
- **CI/CD-härdning:** npm audit och prisma validate körs i pipeline. Alla tester och byggen måste passera.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
# Implementeringsplan: "Se kvitto"-Modal för Kvittoimporten
|
||||||
|
|
||||||
|
**Mål**: MVP-vägen för split-view UX – lägg till modal som visar OCR-text från parsade kvittoraderna.
|
||||||
|
**Scope**: 2-3 timmar
|
||||||
|
**Status**: Planering
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ändringar i `receipt_import_tab.dart`
|
||||||
|
|
||||||
|
### 1.1 Lägg till knapp "Se kvitto" i header-raden (rad ~745-752)
|
||||||
|
|
||||||
|
**Plats**: Höger om "Välj alla/Avmarkera alla"-knappen
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// Innan: Row med bara "Välj alla"-knapp
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('${items.length} rader — tryck för att redigera', style: theme.textTheme.titleSmall),
|
||||||
|
TextButton(...), // "Välj alla/Avmarkera alla"
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Efter: Lägg till "Se kvitto"-knapp
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text('${items.length} rader — tryck för att redigera', style: theme.textTheme.titleSmall),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: items.isEmpty ? null : () => _showReceiptPreview(context, items),
|
||||||
|
icon: const Icon(Icons.description_outlined),
|
||||||
|
label: const Text('Se kvitto'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => setState(...), // Befintlig "Välj alla"
|
||||||
|
child: Text(...),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 Implementera `_showReceiptPreview`-metod
|
||||||
|
|
||||||
|
Lägg till denna metod i `_ReceiptImportTabState`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> _showReceiptPreview(BuildContext context, List<ParsedReceiptItem> items) async {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => _ReceiptPreviewDialog(items: items),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Ny widget: `_ReceiptPreviewDialog`
|
||||||
|
|
||||||
|
Lägg till denna widget **i samma fil** (`receipt_import_tab.dart`), efter `_ReceiptImportResultRow`-klassen:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class _ReceiptPreviewDialog extends StatelessWidget {
|
||||||
|
final List<ParsedReceiptItem> items;
|
||||||
|
|
||||||
|
const _ReceiptPreviewDialog({required this.items});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Kvittotexten i sin helhet'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: 600, // Responsiv bredd på desktop
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Här visas all OCR-parsad text från kvittot. En rad per artikel:',
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerLowest,
|
||||||
|
border: Border.all(color: theme.colorScheme.outlineVariant),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: SelectableText.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: items.isEmpty
|
||||||
|
? [TextSpan(text: '(Inga rader)', style: theme.textTheme.bodySmall)]
|
||||||
|
: items
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((entry) {
|
||||||
|
final item = entry.value;
|
||||||
|
final lineNumber = entry.key + 1;
|
||||||
|
final lineText = _formatReceiptLine(item);
|
||||||
|
return TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: '$lineNumber. ',
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: theme.colorScheme.outlineVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: lineText,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(text: '\n'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
style: theme.textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Stäng'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatReceiptLine(ParsedReceiptItem item) {
|
||||||
|
final parts = <String>[];
|
||||||
|
|
||||||
|
if (item.quantity != null) {
|
||||||
|
parts.add('${item.quantity}');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.unit != null) {
|
||||||
|
parts.add(item.unit!);
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.add(item.rawName);
|
||||||
|
|
||||||
|
if (item.price != null) {
|
||||||
|
parts.add('— ${item.price} kr');
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Implementeringssteg (steg-för-steg)
|
||||||
|
|
||||||
|
1. **Läs receipt_import_tab.dart** och identifiera raden med "Välj alla/Avmarkera alla"-knappen
|
||||||
|
2. **Refaktorera Row**: Lägg "Se kvitto"-knapp bredvid befintliga knapp
|
||||||
|
3. **Lägg till `_showReceiptPreview()`-metod** i `_ReceiptImportTabState`
|
||||||
|
4. **Implementera `_ReceiptPreviewDialog`-widget** på slutet av filen
|
||||||
|
5. **Testa**:
|
||||||
|
- Ladda ett kvitto
|
||||||
|
- Klicka "Se kvitto"-knappen
|
||||||
|
- Verifiera att texten är lesbar och formaterad
|
||||||
|
- Testa responsive bredd (dialog behöver minska på mobil)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Responsiv förbättring (optional)
|
||||||
|
|
||||||
|
Om dialogen behöver anpassas för mobil:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// I _ReceiptPreviewDialog.build():
|
||||||
|
final isWide = MediaQuery.of(context).size.width > 600;
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
insetPadding: const EdgeInsets.all(16),
|
||||||
|
child: SizedBox(
|
||||||
|
width: isWide ? 600 : double.maxFinite, // Full bredd på mobil
|
||||||
|
// ...
|
||||||
|
),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Långsiktiga förbättringar (Phase 2)
|
||||||
|
|
||||||
|
Se `next_steps_flutter.md` för split-view roadmap:
|
||||||
|
- Horisontell split-view på desktop
|
||||||
|
- Scroll-synkronisering
|
||||||
|
- Tab-fallback på mobil
|
||||||
|
- AI-guiding labels
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ärendemal
|
||||||
|
|
||||||
|
**Titel**: "Se kvitto"-modal för kvittoimporten
|
||||||
|
**Branch**: `feat/receipt-preview-modal`
|
||||||
|
**Labels**: `enhancement`, `import-ux`, `phase-1-mvp`
|
||||||
|
**Estimate**: 2-3h
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Flutter Performance – Profileringsguide
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
|
||||||
|
| Mätpunkt | Gränsvärde |
|
||||||
|
|---|---|
|
||||||
|
| Frame build-tid (60 Hz) | < 16 ms |
|
||||||
|
| Frame build-tid (120 Hz) | < 8 ms |
|
||||||
|
| Scroll jank (tappade frames) | 0 vid normal scroll |
|
||||||
|
| Minnesfotavtryck (app) | < 200 MB |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Starta i profile-läge
|
||||||
|
|
||||||
|
Kör alltid profilmätningar i **profile mode**, inte debug. Debug-läget har JIT-kompilering och extra overhead.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Mot fysisk enhet
|
||||||
|
flutter run --profile
|
||||||
|
|
||||||
|
# Mot Chrome (web)
|
||||||
|
flutter run -d chrome --profile
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Flutter DevTools – Öppna
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter pub global activate devtools
|
||||||
|
flutter pub global run devtools
|
||||||
|
```
|
||||||
|
|
||||||
|
Eller anslut direkt från terminalen när appen körs i profile mode – Flutter skriver ut en DevTools-URL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Timeline – Mät frame-tider
|
||||||
|
|
||||||
|
1. Öppna **Performance**-fliken i DevTools.
|
||||||
|
2. Klicka **Record**.
|
||||||
|
3. Utför den aktion du vill mäta (t.ex. byt vy, scrolla).
|
||||||
|
4. Klicka **Stop**.
|
||||||
|
5. Granska:
|
||||||
|
- **UI thread** (Dart-kod) – bör vara < 16 ms per frame.
|
||||||
|
- **Raster thread** (GPU) – bör vara < 16 ms per frame.
|
||||||
|
- Röda/gula staplar = jank.
|
||||||
|
|
||||||
|
### Kritiska mätpunkter i appen
|
||||||
|
|
||||||
|
| Scenario | Vad att leta efter |
|
||||||
|
|---|---|
|
||||||
|
| Byta vy (NavigationBar) | Frame-tid vid `StatefulShellRoute`-byte; bör vara < 32 ms totalt |
|
||||||
|
| Scrolla receptlista | Inga röda frames; `GridView.builder` bör recykla element |
|
||||||
|
| Scrolla adminpaneler | `ListView.builder` i embedded-läge; verifiera att ingen `NeverScrollableScrollPhysics` blockerar |
|
||||||
|
| Kvittoimport – kryssa i rad | Endast den berörda raden bör rebuilda (`ConsumerWidget.select`) |
|
||||||
|
| Kvittoimport – "Välj alla" | Batch-uppdatering via `setSelectedForAll` – en enda `state =` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Widget Rebuild-spårning
|
||||||
|
|
||||||
|
Aktivera rebuild-räknare i DevTools under **Inspector → Widget rebuild counts**.
|
||||||
|
|
||||||
|
Alternativt: lägg till tillfällig räknare i en widget:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
int _buildCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
debugPrint('${widget.runtimeType} build #${++_buildCount}');
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Förväntade rebuild-mönster efter optimeringar
|
||||||
|
|
||||||
|
- `_ReceiptImportResultRow` med index X ska bara rebuilda när `selected[X]` ändras, inte när andra rader kryssas.
|
||||||
|
- `AppShell` ska inte rebuilda vid vy-byte (StatefulShellRoute bevarar grenar).
|
||||||
|
- Admin-paneler ska inte rebuilda hela listan vid en alias-ändring.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Memory Profiler
|
||||||
|
|
||||||
|
1. DevTools → **Memory**-fliken.
|
||||||
|
2. Klicka **Take snapshot** före och efter en tung operation.
|
||||||
|
3. Jämför levande objekt – leta efter läckor (ackumulerade `StreamSubscription`, `Timer`, `Notifier`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. flutter analyze + dart fix
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter analyze
|
||||||
|
dart fix --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
Åtgärda alla varningar om `const` och onödiga rebuilds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Identifierade optimeringar (genomförda)
|
||||||
|
|
||||||
|
| Område | Åtgärd | Effekt |
|
||||||
|
|---|---|---|
|
||||||
|
| Admin-paneler | Tog bort `NeverScrollableScrollPhysics` + `shrinkWrap` | Scroll fungerar, O(n) layout istället för O(n²) |
|
||||||
|
| Admin alias-lista | `ListView.builder` istället för spread | Virtualiserad lista |
|
||||||
|
| FABs | Explicita `heroTag` på alla FABs | Eliminerar hero-animation-krasch vid vy-byte |
|
||||||
|
| Scrollables | `PageStorageKey` på alla listvy | Scrollposition bevaras vid vy-byte |
|
||||||
|
| Router | `StatefulShellRoute.indexedStack` | Branch-state bevaras; ingen ombyggnad vid tab-byte |
|
||||||
|
| Kvittoimport – resultatlista | `ListView.builder` + `SizedBox` bound height | Virtualiserad; max 620 px synlig |
|
||||||
|
| Kvittoimport – radwidget | `ConsumerWidget` med `provider.select((s) => s?.selected[index])` | Endast ändrad rad rebuildar vid checkbox-toggle |
|
||||||
|
| Kvittoimport – batch-API | `setSelectedForAll`, `setSelectedForIndexes`, `setImportedResult` | En `state =` och en SharedPreferences-skrivning per operation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Snabbtest – Verifiera förbättringar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Kör i profile mode och öppna DevTools automatiskt
|
||||||
|
flutter run --profile --devtools-server-address=http://127.0.0.1:9100
|
||||||
|
```
|
||||||
|
|
||||||
|
Kontrollchecklista:
|
||||||
|
|
||||||
|
- [ ] Vy-byte NavigationBar: inga röda frames i Timeline
|
||||||
|
- [ ] Scroll i receptlista: < 2 tappade frames per 100 frames
|
||||||
|
- [ ] Scroll i admin-flikar: fungerar utan lock
|
||||||
|
- [ ] Kvittoimport – checkbox-toggle: rebuild-räknare ökar bara för berörd rad
|
||||||
|
- [ ] Kvittoimport – "Välj alla": en burst av rebuilds (en per rad), inga dubbla
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
Detta dokument ar for anvandare och operativa testare.
|
Detta dokument ar for anvandare och operativa testare.
|
||||||
Har beskriver vi vad som fungerar i Flutter-klienten och hur den anvands i praktiken.
|
Har beskriver vi vad som fungerar i Flutter-klienten och hur den anvands i praktiken.
|
||||||
|
|
||||||
## Dokumentstatus (2026-05-03)
|
## Dokumentstatus (2026-05-19)
|
||||||
|
|
||||||
- Fokus: anvandarflode, inte implementation.
|
- Fokus: anvandarflode, inte implementation.
|
||||||
- Teknisk detaljniva finns i `teknisk_beskrivning_flutter.md`.
|
- Teknisk detaljniva finns i `teknisk_beskrivning_flutter.md`.
|
||||||
@@ -20,6 +20,8 @@ Den anvands parallellt med Next-frontenden under migrering och verifiering.
|
|||||||
- Pagande kvittoimport sparas i klientens session och kan atertas efter refresh/navigation.
|
- Pagande kvittoimport sparas i klientens session och kan atertas efter refresh/navigation.
|
||||||
- Tolkning av antal/forpackning i kvittorader ar forbattrad, inklusive format som `2st`.
|
- Tolkning av antal/forpackning i kvittorader ar forbattrad, inklusive format som `2st`.
|
||||||
- AI-kategoriforslag och produktforslag visas separerat for tydligare val.
|
- AI-kategoriforslag och produktforslag visas separerat for tydligare val.
|
||||||
|
- Flyerimport har nu sessionpersistens med lattviktig cache (`sessionId`, filnamn, valda rader).
|
||||||
|
- Flyer-tabben hydrerar tillstand via backend-sessioner vid aterbesok och app-omstart.
|
||||||
|
|
||||||
## Aktuella anvandarfloden
|
## Aktuella anvandarfloden
|
||||||
|
|
||||||
@@ -45,3 +47,7 @@ Den anvands parallellt med Next-frontenden under migrering och verifiering.
|
|||||||
- `next_steps_flutter.md` - roadmap och prioriteringar.
|
- `next_steps_flutter.md` - roadmap och prioriteringar.
|
||||||
- `teknisk_beskrivning_flutter.md` - teknisk referens for drift/utveckling.
|
- `teknisk_beskrivning_flutter.md` - teknisk referens for drift/utveckling.
|
||||||
- `../README.md` - overgripande produktinformation.
|
- `../README.md` - overgripande produktinformation.
|
||||||
|
|
||||||
|
## Notering
|
||||||
|
|
||||||
|
Aktiv och detaljerad status for Flutter-sparet finns i rotens dokumentation och i teknisk Flutter-dokumentation i samma katalog.
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Nasta steg: Flutter-migrering
|
||||||
|
|
||||||
|
Detta dokument ar Flutter-teamets roadmap och prioriteringslista.
|
||||||
|
All historik och implementationdetaljer finns i `teknisk_beskrivning_flutter.md`.
|
||||||
|
|
||||||
|
## Dokumentstatus (2026-05-19)
|
||||||
|
|
||||||
|
- Fokus: aktiv planering framat.
|
||||||
|
- Endast en roadmap for Flutter for att undvika dubbletter.
|
||||||
|
|
||||||
|
## Klart senaste sessionerna
|
||||||
|
|
||||||
|
- Fas 6b: granskningsflode for kvittoimport (edit, destination, merge, spara).
|
||||||
|
- Fas 6c: separering av AI-kategorichip och produktforslagschip.
|
||||||
|
- Fas 6d: klientpersistens for pagande kvittoimport + forbattrad antal/forpackningsinferens.
|
||||||
|
- Flyerimport: sessionpersistens i klient och backend-hydrering via sessions-endpoints.
|
||||||
|
|
||||||
|
## Pagande arbete
|
||||||
|
|
||||||
|
- Robust bildimport och diagnostik i drift.
|
||||||
|
- Aliasstrategi i kvittoimport (hybrid user-scope + global fallback via admin).
|
||||||
|
- Utokad adminfunktionalitet i Flutter-sparet.
|
||||||
|
- E2E-verifiering av flyerimport: tab-byte, refresh och app-omstart i staging.
|
||||||
|
|
||||||
|
## Prioriterade nasta steg
|
||||||
|
|
||||||
|
1. **Kvitto-import UX förbättring (Split-view långsiktigt)**
|
||||||
|
- MVP (kort sikt): Lägg till "Se kvitto"-modal som visar full OCR-text från parsade rader
|
||||||
|
- Knapp i radlist-header, öppnar dialog med ScrollableText
|
||||||
|
- Enkelt UI, höga UX-vinster
|
||||||
|
- Implementering: ~2h
|
||||||
|
- Långsiktigt (Phase 2): Split-view med scroll-synkronisering
|
||||||
|
- Desktop: Horisontell split (kvitto-text vänster, radlista höger)
|
||||||
|
- Tablet/Mobil: Tab-based fallback (radlista standard, "Se kvitto"-tab för kontext)
|
||||||
|
- Scroll-sync mellan text och rader (om rad 3 är synlig, visa motsvarande text)
|
||||||
|
- AI-guiding labels ("Denna rad matchade mejeri automatiskt")
|
||||||
|
- Implementering: ~8h
|
||||||
|
|
||||||
|
2. Verifiera bildimport och felhantering end-to-end i testmiljo.
|
||||||
|
3. Implementera alias-inlarning vid manuell korrigering i importflodet.
|
||||||
|
4. Forbattra UI/UX i granskningsfloden for kvittoimport.
|
||||||
|
5. Fortsatt migrering av kvarvarande adminfloden.
|
||||||
|
6. Lokalisera kvarvarande delar i import- och inventarievyer.
|
||||||
|
|
||||||
|
## Viktiga beslut
|
||||||
|
|
||||||
|
- Flutter ar separat klient mot befintliga API-kontrakt.
|
||||||
|
- Ingen klientspecifik speciallogik for datamatchning; backend ar sanningskalla.
|
||||||
|
- Next-frontenden kor parallellt tills verifierad parity ar uppnadd.
|
||||||
|
|
||||||
|
## Relaterade dokument
|
||||||
|
|
||||||
|
- `README.md` - anvandarperspektiv.
|
||||||
|
- `teknisk_beskrivning_flutter.md` - teknisk referens.
|
||||||
|
- `../NEXT_STEPS.md` - overgripande roadmap for hela produkten.
|
||||||
|
|
||||||
|
|
||||||
|
## Notering
|
||||||
|
|
||||||
|
Denna fil ar arkiv/planunderlag for Flutter-sparet. Primar status och prioritering finns i rotens `NEXT_STEPS.md`.
|
||||||
+20
@@ -106,3 +106,23 @@ docker compose -f compose.yml -f compose.flutter.yml up -d --no-deps recipe-flut
|
|||||||
- `README.md` - anvandarguide.
|
- `README.md` - anvandarguide.
|
||||||
- `next_steps_flutter.md` - aktiv planering.
|
- `next_steps_flutter.md` - aktiv planering.
|
||||||
- `../TEKNISK_BESKRIVNING.md` - backend/systemovergripande teknik.
|
- `../TEKNISK_BESKRIVNING.md` - backend/systemovergripande teknik.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
@@ -260,3 +260,7 @@ backend-tjänstens env.
|
|||||||
- **Auth stannar i recipe-app backend** — microservice-importer exponeras bara internt på Docker-nätverket
|
- **Auth stannar i recipe-app backend** — microservice-importer exponeras bara internt på Docker-nätverket
|
||||||
- **Bildoptimering vid sparande** behålls i recipe-app (sker vid `RecipesService.create()`, inte vid import)
|
- **Bildoptimering vid sparande** behålls i recipe-app (sker vid `RecipesService.create()`, inte vid import)
|
||||||
- `receipt-import` splittad: AI-del → microservice, produktmatchning + DB → recipe-app backend
|
- `receipt-import` splittad: AI-del → microservice, produktmatchning + DB → recipe-app backend
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# Plan för produktlansering
|
||||||
|
|
||||||
|
Detta dokument är en releasechecklista.
|
||||||
|
Det kompletterar `NEXT_STEPS.md` och ska inte duplicera backloggen.
|
||||||
|
|
||||||
|
## Dokumentstatus (2026-05-21)
|
||||||
|
|
||||||
|
- Målgrupp: produktägare, systemadministratörer, utvecklingsteam.
|
||||||
|
- Fokus: vad som måste vara verifierat innan release.
|
||||||
|
|
||||||
|
## 1. Säkerhet och dataskydd
|
||||||
|
|
||||||
|
- [ ] Känsliga uppgifter krypterade enligt beslutad modell.
|
||||||
|
- [ ] Rate limiting aktiv på relevanta API/AI-endpoints.
|
||||||
|
- [ ] Secret-hantering verifierad (inga hardkodade hemligheter).
|
||||||
|
- [ ] Roll- och accesskontroller testade i praktiken.
|
||||||
|
- [ ] Migrera autentisering från `localStorage` till `httpOnly`-cookies i Flutter Web.
|
||||||
|
- [ ] Implementera automatiserad datarensning för `AiTrace` (retention-policy).
|
||||||
|
- [ ] Utför penetrationstest för IDOR och XSS.
|
||||||
|
- [ ] Dokumentera GDPR-processer (t.ex. rätt att glömmas, dataportabilitet).
|
||||||
|
|
||||||
|
## 2. DevOps och stabilitet
|
||||||
|
|
||||||
|
- [ ] CI/CD för build, test och deploy på plats.
|
||||||
|
- [ ] Migreringar + seedning kör konsekvent vid release.
|
||||||
|
- [ ] Health checks och loggning verifierade.
|
||||||
|
- [ ] Backup/restore testad för datavolymer.
|
||||||
|
|
||||||
|
## 3. Kvalitet och test
|
||||||
|
|
||||||
|
- [ ] Kritiska flöden har testtäckning (auth, import, CRUD, AI).
|
||||||
|
- [ ] Minst en end-to-end verifiering i testmiljö per release.
|
||||||
|
- [ ] DTO-validering och felhantering kontrollerad.
|
||||||
|
- [ ] Skapa E2E-tester för flyer- och kvittoimport (t.ex. Cypress eller Playwright).
|
||||||
|
- [ ] Validera OCR-korrigeringar med ett större dataset.
|
||||||
|
|
||||||
|
## 4. Funktionell releaseklarhet
|
||||||
|
|
||||||
|
- [ ] Kvittoimport fungerar end-to-end med granskningsteg.
|
||||||
|
- [ ] User-scoped produktmodell verifierad med flera testanvändare.
|
||||||
|
- [ ] Kategoriträd seedat och validerat i aktuell miljö.
|
||||||
|
- [ ] Bildimport och fallbackflöde fungerar i driftmiljö.
|
||||||
|
- [ ] Genomför manuell testning av aliasflödet med riktiga kvitton.
|
||||||
|
- [ ] Test sessionhydrering i olika scenarier (t.ex. flikbyte, app-krasch).
|
||||||
|
|
||||||
|
## 5. Riskhantering
|
||||||
|
|
||||||
|
- [ ] AI-kostnad, timeout och fallback-beteende verifierat.
|
||||||
|
- [ ] Ingen osynk mellan migrationer och seedskript.
|
||||||
|
- [ ] Kända release-risker dokumenterade med ansvarig ägare.
|
||||||
|
- [ ] Implementera adaptiv retry-logik med exponentiell backoff för AI-anrop.
|
||||||
|
- [ ] Lägg till kostnadsgränser och varningar i `AiTraceService`.
|
||||||
|
- [ ] Dokumentera fallback-beteende (t.ex. cache, manuell granskning).
|
||||||
|
|
||||||
|
## 6. Prestanda och skalbarhet
|
||||||
|
|
||||||
|
- [ ] Optimera Prisma-frågor med `EXPLAIN ANALYZE` och indexering.
|
||||||
|
- [ ] Implementera virtuell scrollning i Flutter för stora listor.
|
||||||
|
- [ ] Dokumentera belastningsgränser för `importer-api` och planera för skalning.
|
||||||
|
- [ ] Lägg till Redis-cache för vanliga frågor (t.ex. produkt-sökning).
|
||||||
|
|
||||||
|
## 7. Dokumentation och användarstöd
|
||||||
|
|
||||||
|
- [ ] Skapa admin-handbok med vanliga arbetsflöden.
|
||||||
|
- [ ] Förbättra felmeddelanden i UI med tydliga instruktioner.
|
||||||
|
- [ ] Utöka `HelpTextsModule` med kontextuella guider för alla huvudfunktioner.
|
||||||
|
- [ ] Generera OpenAPI-specifikation för backend (t.ex. med `@nestjs/swagger`).
|
||||||
|
|
||||||
|
## Relaterade dokument
|
||||||
|
|
||||||
|
- `NEXT_STEPS.md` — övergripande prioriteringar.
|
||||||
|
- `TEKNISK_BESKRIVNING.md` — teknisk implementation.
|
||||||
|
- `flutter/next_steps_flutter.md` — Flutter-specifik leveransplan.
|
||||||
|
- `MVP_CHECKLISTA.md` — testchecklista för MVP.
|
||||||
|
|
||||||
|
## Kritiska utvecklingsområden
|
||||||
|
|
||||||
|
### 1. Säkerhet och dataskydd (Högsta prioritet)
|
||||||
|
**Motivering:**
|
||||||
|
- Känsliga uppgifter (t.ex. JWT i localStorage) och AI-trace-loggar innehåller maskerade men potentiellt känsliga uppgifter.
|
||||||
|
- Risk för XSS-attacker och IDOR om inte åtgärdat.
|
||||||
|
|
||||||
|
**Åtgärder:**
|
||||||
|
- Migrera autentisering till `httpOnly`-cookies.
|
||||||
|
- Implementera automatiserad datarensning för `AiTrace`.
|
||||||
|
- Utför penetrationstester.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Stabilitet i AI-integration (Hög prioritet)
|
||||||
|
**Motivering:**
|
||||||
|
- Timeout och retry-logik kan leda till 503-fel.
|
||||||
|
- Saknad kostnadskontroll för AI-anrop.
|
||||||
|
|
||||||
|
**Åtgärder:**
|
||||||
|
- Implementera adaptiv retry-logik.
|
||||||
|
- Lägg till kostnadsgränser i `AiTraceService`.
|
||||||
|
- Dokumentera fallback-beteende.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Sluttestning av importflöden (Hög prioritet)
|
||||||
|
**Motivering:**
|
||||||
|
- Alias-strategin och sessionhydrering är ej fullt testade.
|
||||||
|
- Saknas E2E-tester för hela importflödet.
|
||||||
|
|
||||||
|
**Åtgärder:**
|
||||||
|
- Genomför manuell testning med riktiga kvitton.
|
||||||
|
- Skapa E2E-tester.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Prestanda och skalbarhet (Medel prioritet)
|
||||||
|
**Motivering:**
|
||||||
|
- Långsamma Prisma-frågor och renderingsproblem i Flutter-UI.
|
||||||
|
- Saknad caching-strategi.
|
||||||
|
|
||||||
|
**Åtgärder:**
|
||||||
|
- Optimera databasfrågor.
|
||||||
|
- Implementera virtuell scrollning.
|
||||||
|
- Lägg till Redis-cache.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Dokumentation och användarstöd (Medel prioritet)
|
||||||
|
**Motivering:**
|
||||||
|
- Saknas admin-guider och tydliga felmeddelanden.
|
||||||
|
- Begränsat innehåll i `HelpTextsModule`.
|
||||||
|
|
||||||
|
**Åtgärder:**
|
||||||
|
- Skapa admin-handbok.
|
||||||
|
- Utöka hjälptexter.
|
||||||
|
- Generera OpenAPI-dokumentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prioriteringsordning
|
||||||
|
|
||||||
|
| **Område** | **Prioritet** | **Berörda filer/dokument** |
|
||||||
|
|---------------------------------|---------------|-----------------------------------------------------|
|
||||||
|
| 1. Säkerhet och dataskydd | ⭐⭐⭐⭐⭐ | `TEKNISK_BESKRIVNING.md`, `flutter/.../auth/` |
|
||||||
|
| 2. Stabilitet i AI-integration | ⭐⭐⭐⭐ | `flyerimporter.md`, `backend/src/ai/` |
|
||||||
|
| 3. Sluttestning av importflöden | ⭐⭐⭐⭐ | `MVP_CHECKLISTA.md`, `NEXT_STEPS.md` |
|
||||||
|
| 4. Prestanda och skalbarhet | ⭐⭐⭐ | `backend/src/flyer-import/`, `flutter/lib/admin/` |
|
||||||
|
| 5. Dokumentation och stöd | ⭐⭐ | `README.md`, `HelpTextsModule`, `backend/src/ai/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nästa steg
|
||||||
|
|
||||||
|
1. Låsa säkerhetsbristerna (särskilt autentisering och dataskydd).
|
||||||
|
2. Stabilisera AI-integration med bättre felhantering och kostnadskontroll.
|
||||||
|
3. Kör sluttestning av importflöden och rätta eventuella regressioner.
|
||||||
|
4. Optimera prestanda i databas och UI.
|
||||||
|
5. Förbättra dokumentationen för admin och slutanvändare.
|
||||||
|
|
||||||
|
Dessa åtgärder säkerställer en **stabil, säker och användarvänlig lansering**.
|
||||||
@@ -71,6 +71,7 @@
|
|||||||
## Fas 6: Säkerhet och driftbarhet (parallellt)
|
## Fas 6: Säkerhet och driftbarhet (parallellt)
|
||||||
1. Säkerhetsgranskning:
|
1. Säkerhetsgranskning:
|
||||||
- Input-validering, auth/role-kontroller, secret-hantering.
|
- Input-validering, auth/role-kontroller, secret-hantering.
|
||||||
|
- Inventory är nu user-scopad och IDOR-skyddad: Alla inventory-operationer kräver och filtrerar på userId i backend (schema, migration, service, controller, tester). Tester verifierar att åtkomst nekas vid försök till IDOR.
|
||||||
2. Driftbarhet:
|
2. Driftbarhet:
|
||||||
- Strukturerad loggning, korrelations-id, tydligare metrics.
|
- Strukturerad loggning, korrelations-id, tydligare metrics.
|
||||||
3. Resiliens:
|
3. Resiliens:
|
||||||
@@ -97,3 +98,7 @@
|
|||||||
3. Kodduplicering reducerad i prioriterade moduler.
|
3. Kodduplicering reducerad i prioriterade moduler.
|
||||||
4. Testskydd finns för alla kritiska flöden.
|
4. Testskydd finns för alla kritiska flöden.
|
||||||
5. CI-gates förhindrar att kvaliteten glider tillbaka.
|
5. CI-gates förhindrar att kvaliteten glider tillbaka.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
@@ -3,6 +3,7 @@ WORKDIR /app
|
|||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
|
# 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
FROM node:22-alpine AS builder
|
FROM node:22-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
## Arkiverad frontend-dokumentation
|
||||||
|
|
||||||
|
- Den aktiva dokumentationen finns i rotens `README.md`, `TEKNISK_BESKRIVNING.md` och `NEXT_STEPS.md`.
|
||||||
|
- Senaste uppdatering (2026-05-19): flyerimport har session-endpoints i backend och klientpersistens/hydrering i Flutter.
|
||||||
|
- Denna fil behålls endast som arkivreferens för äldre frontend-spår.
|
||||||
@@ -75,3 +75,5 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
},
|
},
|
||||||
session: { strategy: 'jwt' },
|
session: { strategy: 'jwt' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
|||||||
Vendored
+1
@@ -1,5 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Session 2026-05-06: User-scoped AI, admin-toggles och premium
|
# Session 2026-05-06: User-scoped AI, admin-toggles och premium
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
Denna session:
|
Denna session:
|
||||||
- Införde user-scoped AI-förslag för ingrediens- och kategorimatchning (premium-funktion).
|
- Införde user-scoped AI-förslag för ingrediens- och kategorimatchning (premium-funktion).
|
||||||
- Admin kan nu slå på/av AI per användare via backend och UI.
|
- Admin kan nu slå på/av AI per användare via backend och UI.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
Read memory [](file:///c%3A/Users/Nils-JohanGynther/AppData/Roaming/Code/User/workspaceStorage/e6ea1b0bd55239bec87a0a6ab7819f74/GitHub.copilot-chat/memory-tool/memories/NTkxM2ZhMmYtYjViYi00YTE0LTg2NGEtNmYyYzZjMTcxNWEw/microservice-todo.md)
|
Read memory [](file:///c%3A/Users/Nils-JohanGynther/AppData/Roaming/Code/User/workspaceStorage/e6ea1b0bd55239bec87a0a6ab7819f74/GitHub.copilot-chat/memory-tool/memories/NTkxM2ZhMmYtYjViYi00YTE0LTg2NGEtNmYyYzZjMTcxNWEw/microservice-todo.md)
|
||||||
|
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
|
## 2026-05-10: Admin-inventarie (CRUD, merge, filter, sortering, preview, säkerhet), user-scope, IDOR-skydd, säkerhetshärdning, optimeringar och utökad testtäckning är nu genomförda och dokumenterade i README, TEKNISK_BESKRIVNING, SÄKERHETSHÄRDNINGSPLAN och SESSIONLOGGAR.
|
||||||
## Dokumentstatus (2026-05-03)
|
## Dokumentstatus (2026-05-03)
|
||||||
|
|
||||||
### Målgrupp
|
### Målgrupp
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
min-release-age=1
|
||||||
+2
-2
@@ -17,7 +17,7 @@ COPY prisma ./prisma
|
|||||||
COPY src ./src
|
COPY src ./src
|
||||||
COPY tsconfig.json nest-cli.json ./
|
COPY tsconfig.json nest-cli.json ./
|
||||||
RUN ./node_modules/.bin/prisma generate
|
RUN ./node_modules/.bin/prisma generate
|
||||||
RUN npm test
|
RUN npm test -- --runInBand
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 3: Kör applikationen
|
# Stage 3: Kör applikationen
|
||||||
@@ -31,4 +31,4 @@ COPY --from=builder /app/prisma ./prisma
|
|||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
CMD ["sh", "-c", "until ./node_modules/.bin/prisma migrate deploy; do echo 'Migration failed, retrying in 5s...'; sleep 5; done && node dist/main"]
|
CMD ["sh", "-c", "if [ \"${SKIP_MIGRATION:-false}\" != \"true\" ]; then echo 'Running automatic Prisma migration...'; until ./node_modules/.bin/prisma migrate deploy --schema prisma/schema.prisma; do echo 'Migration failed, retrying in 5s...'; sleep 5; done; else echo 'Skipping automatic Prisma migration (SKIP_MIGRATION=true).'; fi && node dist/main"]
|
||||||
|
|||||||
Vendored
-12
@@ -1,12 +0,0 @@
|
|||||||
export interface AiModelInfo {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
model: string;
|
|
||||||
path: string;
|
|
||||||
trigger: string;
|
|
||||||
access: string;
|
|
||||||
}
|
|
||||||
export declare class AiController {
|
|
||||||
getModels(): AiModelInfo[];
|
|
||||||
}
|
|
||||||
Vendored
-79
@@ -1,79 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.AiController = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
const public_decorator_1 = require("../auth/decorators/public.decorator");
|
|
||||||
const ai_service_1 = require("./ai.service");
|
|
||||||
const RECEIPT_IMPORT_MODEL = 'mistral-small-2603';
|
|
||||||
let AiController = class AiController {
|
|
||||||
getModels() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'receipt-pdf',
|
|
||||||
name: 'Kvittoimport — PDF-tolkning',
|
|
||||||
description: 'Extraherar varunamn, mängd och pris ur PDF-kvitton via textanalys.',
|
|
||||||
model: RECEIPT_IMPORT_MODEL,
|
|
||||||
path: '/import',
|
|
||||||
trigger: 'Vid uppladdning av PDF-kvitto',
|
|
||||||
access: 'Alla inloggade',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'receipt-image',
|
|
||||||
name: 'Kvittoimport — Bildtolkning',
|
|
||||||
description: 'Extraherar varunamn, mängd och pris ur kvittofoton via bildanalys.',
|
|
||||||
model: RECEIPT_IMPORT_MODEL,
|
|
||||||
path: '/import',
|
|
||||||
trigger: 'Vid uppladdning av kvittobild (JPEG, PNG, WebP, HEIC)',
|
|
||||||
access: 'Alla inloggade',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'receipt-category',
|
|
||||||
name: 'Kvittoimport — Kategorisuggestion',
|
|
||||||
description: 'För varor som inte matchas mot befintliga produkter visas ett AI-förslag på kategori som ledtråd.',
|
|
||||||
model: ai_service_1.AI_CATEGORIZATION_MODEL,
|
|
||||||
path: '/import',
|
|
||||||
trigger: 'Automatiskt efter kvittotolkning (om inga träffar hittas)',
|
|
||||||
access: 'Premium-användare + Admin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'product-suggest',
|
|
||||||
name: 'AI-kategorisering per produkt',
|
|
||||||
description: 'Ger ett AI-förslag på kategori för en enskild produkt med säkerhetsindikation (hög/medel/låg).',
|
|
||||||
model: ai_service_1.AI_CATEGORIZATION_MODEL,
|
|
||||||
path: '/admin/products',
|
|
||||||
trigger: 'Manuell — klick på "✨ Fråga AI" i produktlistan',
|
|
||||||
access: 'Admin',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'product-bulk',
|
|
||||||
name: 'AI-bulk-kategorisering',
|
|
||||||
description: 'Analyserar alla okategoriserade produkter och presenterar förslag i ett bekräftelsemodal.',
|
|
||||||
model: ai_service_1.AI_CATEGORIZATION_MODEL,
|
|
||||||
path: '/admin/products',
|
|
||||||
trigger: 'Manuell — knappen "✨ AI-kategorisera okategoriserade"',
|
|
||||||
access: 'Admin',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
exports.AiController = AiController;
|
|
||||||
__decorate([
|
|
||||||
(0, common_1.Get)('models'),
|
|
||||||
(0, public_decorator_1.Public)(),
|
|
||||||
__metadata("design:type", Function),
|
|
||||||
__metadata("design:paramtypes", []),
|
|
||||||
__metadata("design:returntype", Array)
|
|
||||||
], AiController.prototype, "getModels", null);
|
|
||||||
exports.AiController = AiController = __decorate([
|
|
||||||
(0, common_1.Controller)('ai')
|
|
||||||
], AiController);
|
|
||||||
//# sourceMappingURL=ai.controller.js.map
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"ai.controller.js","sourceRoot":"","sources":["../../src/ai/ai.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAiD;AACjD,0EAA6D;AAC7D,6CAAuD;AAEvD,MAAM,oBAAoB,GAAG,oBAAoB,CAAC;AAa3C,IAAM,YAAY,GAAlB,MAAM,YAAY;IAGvB,SAAS;QACP,OAAO;YACL;gBACE,EAAE,EAAE,aAAa;gBACjB,IAAI,EAAE,6BAA6B;gBACnC,WAAW,EAAE,oEAAoE;gBACjF,KAAK,EAAE,oBAAoB;gBAC3B,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,+BAA+B;gBACxC,MAAM,EAAE,gBAAgB;aACzB;YACD;gBACE,EAAE,EAAE,eAAe;gBACnB,IAAI,EAAE,6BAA6B;gBACnC,WAAW,EAAE,oEAAoE;gBACjF,KAAK,EAAE,oBAAoB;gBAC3B,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,uDAAuD;gBAChE,MAAM,EAAE,gBAAgB;aACzB;YACD;gBACE,EAAE,EAAE,kBAAkB;gBACtB,IAAI,EAAE,mCAAmC;gBACzC,WAAW,EAAE,mGAAmG;gBAChH,KAAK,EAAE,oCAAuB;gBAC9B,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,2DAA2D;gBACpE,MAAM,EAAE,2BAA2B;aACpC;YACD;gBACE,EAAE,EAAE,iBAAiB;gBACrB,IAAI,EAAE,+BAA+B;gBACrC,WAAW,EAAE,gGAAgG;gBAC7G,KAAK,EAAE,oCAAuB;gBAC9B,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,iDAAiD;gBAC1D,MAAM,EAAE,OAAO;aAChB;YACD;gBACE,EAAE,EAAE,cAAc;gBAClB,IAAI,EAAE,wBAAwB;gBAC9B,WAAW,EAAE,2FAA2F;gBACxG,KAAK,EAAE,oCAAuB;gBAC9B,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,uDAAuD;gBAChE,MAAM,EAAE,OAAO;aAChB;SACF,CAAC;IACJ,CAAC;CACF,CAAA;AApDY,oCAAY;AAGvB;IAFC,IAAA,YAAG,EAAC,QAAQ,CAAC;IACb,IAAA,yBAAM,GAAE;;;;6CAiDR;uBAnDU,YAAY;IADxB,IAAA,mBAAU,EAAC,IAAI,CAAC;GACJ,YAAY,CAoDxB"}
|
|
||||||
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
export declare class AiModule {
|
|
||||||
}
|
|
||||||
Vendored
-23
@@ -1,23 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.AiModule = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
const ai_service_1 = require("./ai.service");
|
|
||||||
const ai_controller_1 = require("./ai.controller");
|
|
||||||
let AiModule = class AiModule {
|
|
||||||
};
|
|
||||||
exports.AiModule = AiModule;
|
|
||||||
exports.AiModule = AiModule = __decorate([
|
|
||||||
(0, common_1.Module)({
|
|
||||||
controllers: [ai_controller_1.AiController],
|
|
||||||
providers: [ai_service_1.AiService],
|
|
||||||
exports: [ai_service_1.AiService],
|
|
||||||
})
|
|
||||||
], AiModule);
|
|
||||||
//# sourceMappingURL=ai.module.js.map
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"ai.module.js","sourceRoot":"","sources":["../../src/ai/ai.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,6CAAyC;AACzC,mDAA+C;AAOxC,IAAM,QAAQ,GAAd,MAAM,QAAQ;CAAG,CAAA;AAAX,4BAAQ;mBAAR,QAAQ;IALpB,IAAA,eAAM,EAAC;QACN,WAAW,EAAE,CAAC,4BAAY,CAAC;QAC3B,SAAS,EAAE,CAAC,sBAAS,CAAC;QACtB,OAAO,EAAE,CAAC,sBAAS,CAAC;KACrB,CAAC;GACW,QAAQ,CAAG"}
|
|
||||||
Vendored
-34
@@ -1,34 +0,0 @@
|
|||||||
import { FlatCategory } from '../categories/categories.service';
|
|
||||||
export declare const AI_CATEGORIZATION_MODEL = "mistral-tiny";
|
|
||||||
export type CategorySuggestion = {
|
|
||||||
categoryId: number;
|
|
||||||
categoryName: string;
|
|
||||||
path: string;
|
|
||||||
confidence: 'high' | 'medium' | 'low';
|
|
||||||
usedFallback: boolean;
|
|
||||||
};
|
|
||||||
export type AiIngredientMatchSuggestion = {
|
|
||||||
productId: number;
|
|
||||||
reason?: string;
|
|
||||||
confidence: 'high' | 'medium' | 'low';
|
|
||||||
};
|
|
||||||
export type AiSubstitutionSuggestion = {
|
|
||||||
productId: number;
|
|
||||||
reason?: string;
|
|
||||||
confidence: 'high' | 'medium' | 'low';
|
|
||||||
};
|
|
||||||
export declare class AiService {
|
|
||||||
private readonly logger;
|
|
||||||
suggestIngredientMatches(rawIngredient: string, candidates: Array<{
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
canonicalName?: string | null;
|
|
||||||
}>): Promise<AiIngredientMatchSuggestion[]>;
|
|
||||||
suggestSubstitutions(rawIngredient: string, availableProducts: Array<{
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
canonicalName?: string | null;
|
|
||||||
}>): Promise<AiSubstitutionSuggestion[]>;
|
|
||||||
suggestCategory(productName: string, categories: FlatCategory[]): Promise<CategorySuggestion>;
|
|
||||||
private fallbackToOvrigt;
|
|
||||||
}
|
|
||||||
Vendored
-240
@@ -1,240 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
var AiService_1;
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.AiService = exports.AI_CATEGORIZATION_MODEL = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
const MISTRAL_API_URL = 'https://api.mistral.ai/v1/chat/completions';
|
|
||||||
exports.AI_CATEGORIZATION_MODEL = 'mistral-tiny';
|
|
||||||
const MODEL = exports.AI_CATEGORIZATION_MODEL;
|
|
||||||
let AiService = AiService_1 = class AiService {
|
|
||||||
constructor() {
|
|
||||||
this.logger = new common_1.Logger(AiService_1.name);
|
|
||||||
}
|
|
||||||
async suggestIngredientMatches(rawIngredient, candidates) {
|
|
||||||
const apiKey = process.env.MISTRAL_API_KEY;
|
|
||||||
if (!apiKey || candidates.length === 0)
|
|
||||||
return [];
|
|
||||||
const candidateList = candidates
|
|
||||||
.map((c) => `[${c.id}] ${c.canonicalName || c.name}`)
|
|
||||||
.join('\n');
|
|
||||||
const systemPrompt = `Du matchar en ingrediensrad mot produktkandidater.
|
|
||||||
Svara ENDAST med JSON: {"matches":[{"productId":123,"reason":"...","confidence":"high|medium|low"}]}
|
|
||||||
Regler:
|
|
||||||
1. Välj max 3 kandidater.
|
|
||||||
2. Om inget passar, returnera tom lista.`;
|
|
||||||
const userPrompt = `Ingrediens: "${rawIngredient}"\nKandidater:\n${candidateList}`;
|
|
||||||
try {
|
|
||||||
const response = await fetch(MISTRAL_API_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: MODEL,
|
|
||||||
messages: [
|
|
||||||
{ role: 'system', content: systemPrompt },
|
|
||||||
{ role: 'user', content: userPrompt },
|
|
||||||
],
|
|
||||||
max_tokens: 300,
|
|
||||||
temperature: 0.1,
|
|
||||||
response_format: { type: 'json_object' },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
this.logger.warn(`suggestIngredientMatches API-fel: ${response.status}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const data = (await response.json());
|
|
||||||
const raw = data.choices?.[0]?.message?.content ?? '{}';
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
return Array.isArray(parsed.matches) ? parsed.matches.slice(0, 3) : [];
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
this.logger.warn(`suggestIngredientMatches misslyckades: ${String(err)}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async suggestSubstitutions(rawIngredient, availableProducts) {
|
|
||||||
const apiKey = process.env.MISTRAL_API_KEY;
|
|
||||||
if (!apiKey || availableProducts.length === 0)
|
|
||||||
return [];
|
|
||||||
const productList = availableProducts
|
|
||||||
.map((p) => `[${p.id}] ${p.canonicalName || p.name}`)
|
|
||||||
.join('\n');
|
|
||||||
const systemPrompt = `Du föreslår ersättningsvaror för en ingrediens.
|
|
||||||
Svara ENDAST med JSON: {"substitutions":[{"productId":123,"reason":"...","confidence":"high|medium|low"}]}
|
|
||||||
Regler:
|
|
||||||
1. Välj max 3 ersättningar.
|
|
||||||
2. Om inget passar, returnera tom lista.`;
|
|
||||||
const userPrompt = `Ingrediens: "${rawIngredient}"\nTillgängliga produkter:\n${productList}`;
|
|
||||||
try {
|
|
||||||
const response = await fetch(MISTRAL_API_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: MODEL,
|
|
||||||
messages: [
|
|
||||||
{ role: 'system', content: systemPrompt },
|
|
||||||
{ role: 'user', content: userPrompt },
|
|
||||||
],
|
|
||||||
max_tokens: 300,
|
|
||||||
temperature: 0.2,
|
|
||||||
response_format: { type: 'json_object' },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
this.logger.warn(`suggestSubstitutions API-fel: ${response.status}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const data = (await response.json());
|
|
||||||
const raw = data.choices?.[0]?.message?.content ?? '{}';
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
return Array.isArray(parsed.substitutions) ? parsed.substitutions.slice(0, 3) : [];
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
this.logger.warn(`suggestSubstitutions misslyckades: ${String(err)}`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async suggestCategory(productName, categories) {
|
|
||||||
const apiKey = process.env.MISTRAL_API_KEY;
|
|
||||||
if (!apiKey) {
|
|
||||||
throw new common_1.ServiceUnavailableException('MISTRAL_API_KEY är inte konfigurerad i miljövariabler');
|
|
||||||
}
|
|
||||||
const categoryList = categories
|
|
||||||
.map((c) => `[${c.id}] ${c.path}`)
|
|
||||||
.join('\n');
|
|
||||||
const systemPrompt = `Du är ett kategoriseringssystem för en livsmedelsapp. Din uppgift är att hitta den mest lämpliga kategorin för en produkt.
|
|
||||||
|
|
||||||
Tillgängliga kategorier (format: [id] Sökväg):
|
|
||||||
${categoryList}
|
|
||||||
|
|
||||||
Regler:
|
|
||||||
1. Välj den mest specifika underkategorin som passar produkten.
|
|
||||||
2. Om ingen specifik kategori passar, välj en underkategori under "Övrigt" om möjligt.
|
|
||||||
3. Om ingen underkategori under "Övrigt" passar, välj "Övrigt" (den kategori vars sökväg är exakt "Övrigt").
|
|
||||||
4. Du MÅSTE alltid returnera ett svar — aldrig null eller tomt.
|
|
||||||
5. Svara ENDAST med giltig JSON i detta format: { "categoryId": <nummer>, "confidence": "high" | "medium" | "low" }
|
|
||||||
- "high": uppenbart matchande kategori
|
|
||||||
- "medium": trolig matchning
|
|
||||||
- "low": osäker, används fallback (Övrigt eller underkategori till Övrigt)`;
|
|
||||||
const userPrompt = `Produkt: "${productName}"`;
|
|
||||||
let raw = '';
|
|
||||||
const MAX_RETRIES = 3;
|
|
||||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(MISTRAL_API_URL, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: MODEL,
|
|
||||||
messages: [
|
|
||||||
{ role: 'system', content: systemPrompt },
|
|
||||||
{ role: 'user', content: userPrompt },
|
|
||||||
],
|
|
||||||
max_tokens: 100,
|
|
||||||
temperature: 0.1,
|
|
||||||
response_format: { type: 'json_object' },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (response.status === 503 || response.status === 429) {
|
|
||||||
const err = await response.text();
|
|
||||||
this.logger.warn(`Mistral API ${response.status} (försök ${attempt}/${MAX_RETRIES}): ${err}`);
|
|
||||||
if (attempt < MAX_RETRIES) {
|
|
||||||
await new Promise((r) => setTimeout(r, attempt * 1500));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw new common_1.ServiceUnavailableException('AI-tjänsten är tillfälligt otillgänglig, försök igen');
|
|
||||||
}
|
|
||||||
if (!response.ok) {
|
|
||||||
const err = await response.text();
|
|
||||||
this.logger.error(`Mistral API-fel: ${response.status} ${err}`);
|
|
||||||
throw new common_1.ServiceUnavailableException('AI-tjänsten svarade inte korrekt');
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
raw = data.choices?.[0]?.message?.content ?? '';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
if (err instanceof common_1.ServiceUnavailableException)
|
|
||||||
throw err;
|
|
||||||
this.logger.error(`Mistral fetch-fel (försök ${attempt}/${MAX_RETRIES}): ${String(err)}`);
|
|
||||||
if (attempt < MAX_RETRIES) {
|
|
||||||
await new Promise((r) => setTimeout(r, attempt * 1500));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw new common_1.ServiceUnavailableException('Kunde inte nå AI-tjänsten');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let parsed;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(raw);
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
this.logger.warn(`AI returnerade ogiltig JSON: ${raw}`);
|
|
||||||
return this.fallbackToOvrigt(categories);
|
|
||||||
}
|
|
||||||
const validId = typeof parsed.categoryId === 'number';
|
|
||||||
const matchedCategory = validId ? categories.find((c) => c.id === parsed.categoryId) : null;
|
|
||||||
if (!matchedCategory) {
|
|
||||||
this.logger.warn(`AI returnerade okänt categoryId ${parsed.categoryId}, använder fallback`);
|
|
||||||
return this.fallbackToOvrigt(categories);
|
|
||||||
}
|
|
||||||
const confidence = ['high', 'medium', 'low'].includes(parsed.confidence)
|
|
||||||
? parsed.confidence
|
|
||||||
: 'medium';
|
|
||||||
if (confidence === 'low') {
|
|
||||||
const l1Name = matchedCategory.path.split(' > ')[0];
|
|
||||||
const l1 = categories.find((c) => c.path === l1Name);
|
|
||||||
if (l1 && l1.id !== matchedCategory.id) {
|
|
||||||
this.logger.log(`AI-guardrail: ${confidence} konfidenspoäng → remappar "${matchedCategory.path}" → L1 "${l1.path}"`);
|
|
||||||
return {
|
|
||||||
categoryId: l1.id,
|
|
||||||
categoryName: l1.name,
|
|
||||||
path: l1.path,
|
|
||||||
confidence,
|
|
||||||
usedFallback: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
categoryId: matchedCategory.id,
|
|
||||||
categoryName: matchedCategory.name,
|
|
||||||
path: matchedCategory.path,
|
|
||||||
confidence,
|
|
||||||
usedFallback: confidence === 'low',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
fallbackToOvrigt(categories) {
|
|
||||||
const ovrigt = categories.find((c) => c.path === 'Övrigt');
|
|
||||||
if (!ovrigt) {
|
|
||||||
const first = categories[0];
|
|
||||||
return { categoryId: first.id, categoryName: first.name, path: first.path, confidence: 'low', usedFallback: true };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
categoryId: ovrigt.id,
|
|
||||||
categoryName: ovrigt.name,
|
|
||||||
path: ovrigt.path,
|
|
||||||
confidence: 'low',
|
|
||||||
usedFallback: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
exports.AiService = AiService;
|
|
||||||
exports.AiService = AiService = AiService_1 = __decorate([
|
|
||||||
(0, common_1.Injectable)()
|
|
||||||
], AiService);
|
|
||||||
//# sourceMappingURL=ai.service.js.map
|
|
||||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
export declare class AppModule {
|
|
||||||
}
|
|
||||||
Vendored
-75
@@ -1,75 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.AppModule = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
const core_1 = require("@nestjs/core");
|
|
||||||
const throttler_1 = require("@nestjs/throttler");
|
|
||||||
const health_module_1 = require("./health/health.module");
|
|
||||||
const prisma_module_1 = require("./prisma/prisma.module");
|
|
||||||
const products_module_1 = require("./products/products.module");
|
|
||||||
const inventory_module_1 = require("./inventory/inventory.module");
|
|
||||||
const recipes_module_1 = require("./recipes/recipes.module");
|
|
||||||
const quick_import_module_1 = require("./quick-import/quick-import.module");
|
|
||||||
const pantry_module_1 = require("./pantry/pantry.module");
|
|
||||||
const meal_plan_module_1 = require("./meal-plan/meal-plan.module");
|
|
||||||
const receipt_import_module_1 = require("./receipt-import/receipt-import.module");
|
|
||||||
const receipt_alias_module_1 = require("./receipt-alias/receipt-alias.module");
|
|
||||||
const auth_module_1 = require("./auth/auth.module");
|
|
||||||
const users_module_1 = require("./users/users.module");
|
|
||||||
const user_products_module_1 = require("./user-products/user-products.module");
|
|
||||||
const categories_module_1 = require("./categories/categories.module");
|
|
||||||
const ai_module_1 = require("./ai/ai.module");
|
|
||||||
const jwt_auth_guard_1 = require("./auth/jwt-auth.guard");
|
|
||||||
const roles_guard_1 = require("./auth/roles.guard");
|
|
||||||
let AppModule = class AppModule {
|
|
||||||
};
|
|
||||||
exports.AppModule = AppModule;
|
|
||||||
exports.AppModule = AppModule = __decorate([
|
|
||||||
(0, common_1.Module)({
|
|
||||||
imports: [
|
|
||||||
throttler_1.ThrottlerModule.forRoot([
|
|
||||||
{
|
|
||||||
name: 'default',
|
|
||||||
ttl: 60_000,
|
|
||||||
limit: 120,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
health_module_1.HealthModule,
|
|
||||||
prisma_module_1.PrismaModule,
|
|
||||||
products_module_1.ProductsModule,
|
|
||||||
inventory_module_1.InventoryModule,
|
|
||||||
recipes_module_1.RecipesModule,
|
|
||||||
quick_import_module_1.QuickImportModule,
|
|
||||||
pantry_module_1.PantryModule,
|
|
||||||
meal_plan_module_1.MealPlanModule,
|
|
||||||
receipt_import_module_1.ReceiptImportModule,
|
|
||||||
receipt_alias_module_1.ReceiptAliasModule,
|
|
||||||
auth_module_1.AuthModule,
|
|
||||||
users_module_1.UsersModule,
|
|
||||||
user_products_module_1.UserProductsModule,
|
|
||||||
categories_module_1.CategoriesModule,
|
|
||||||
ai_module_1.AiModule,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: core_1.APP_GUARD,
|
|
||||||
useClass: throttler_1.ThrottlerGuard,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: core_1.APP_GUARD,
|
|
||||||
useClass: jwt_auth_guard_1.JwtAuthGuard,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: core_1.APP_GUARD,
|
|
||||||
useClass: roles_guard_1.RolesGuard,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
], AppModule);
|
|
||||||
//# sourceMappingURL=app.module.js.map
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,uCAAyC;AACzC,iDAAoE;AACpE,0DAAsD;AACtD,0DAAsD;AACtD,gEAA4D;AAC5D,mEAA+D;AAC/D,6DAAyD;AACzD,4EAAuE;AACvE,0DAAsD;AACtD,mEAA8D;AAC9D,kFAA6E;AAC7E,+EAA0E;AAC1E,oDAAgD;AAChD,uDAAmD;AACnD,+EAA0E;AAC1E,sEAAkE;AAClE,8CAA0C;AAC1C,0DAAqD;AACrD,oDAAgD;AA2CzC,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IAxCrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,2BAAe,CAAC,OAAO,CAAC;gBACtB;oBACE,IAAI,EAAE,SAAS;oBACf,GAAG,EAAE,MAAM;oBACX,KAAK,EAAE,GAAG;iBACX;aACF,CAAC;YACF,4BAAY;YACZ,4BAAY;YACZ,gCAAc;YACd,kCAAe;YACf,8BAAa;YACb,uCAAiB;YACjB,4BAAY;YACZ,iCAAc;YACd,2CAAmB;YACnB,yCAAkB;YAClB,wBAAU;YACV,0BAAW;YACX,yCAAkB;YAClB,oCAAgB;YAChB,oBAAQ;SACT;QACD,SAAS,EAAE;YACT;gBACE,OAAO,EAAE,gBAAS;gBAClB,QAAQ,EAAE,0BAAc;aACzB;YACD;gBACE,OAAO,EAAE,gBAAS;gBAClB,QAAQ,EAAE,6BAAY;aACvB;YACD;gBACE,OAAO,EAAE,gBAAS;gBAClB,QAAQ,EAAE,wBAAU;aACrB;SACF;KACF,CAAC;GACW,SAAS,CAAG"}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
import { AuthService } from './auth.service';
|
|
||||||
import { RegisterDto } from './dto/register.dto';
|
|
||||||
import { LoginDto } from './dto/login.dto';
|
|
||||||
export declare class AuthController {
|
|
||||||
private readonly authService;
|
|
||||||
constructor(authService: AuthService);
|
|
||||||
register(dto: RegisterDto): Promise<{
|
|
||||||
accessToken: string;
|
|
||||||
userId: number;
|
|
||||||
username: string;
|
|
||||||
role: string;
|
|
||||||
isPremium: boolean;
|
|
||||||
}>;
|
|
||||||
login(dto: LoginDto): Promise<{
|
|
||||||
accessToken: string;
|
|
||||||
userId: number;
|
|
||||||
username: string;
|
|
||||||
role: string;
|
|
||||||
isPremium: boolean;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
Vendored
-57
@@ -1,57 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
||||||
};
|
|
||||||
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
||||||
return function (target, key) { decorator(target, key, paramIndex); }
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.AuthController = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
const throttler_1 = require("@nestjs/throttler");
|
|
||||||
const auth_service_1 = require("./auth.service");
|
|
||||||
const register_dto_1 = require("./dto/register.dto");
|
|
||||||
const login_dto_1 = require("./dto/login.dto");
|
|
||||||
const public_decorator_1 = require("./decorators/public.decorator");
|
|
||||||
let AuthController = class AuthController {
|
|
||||||
constructor(authService) {
|
|
||||||
this.authService = authService;
|
|
||||||
}
|
|
||||||
register(dto) {
|
|
||||||
return this.authService.register(dto);
|
|
||||||
}
|
|
||||||
login(dto) {
|
|
||||||
return this.authService.login(dto);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
exports.AuthController = AuthController;
|
|
||||||
__decorate([
|
|
||||||
(0, public_decorator_1.Public)(),
|
|
||||||
(0, throttler_1.Throttle)({ default: { ttl: 60_000, limit: 10 } }),
|
|
||||||
(0, common_1.Post)('register'),
|
|
||||||
__param(0, (0, common_1.Body)()),
|
|
||||||
__metadata("design:type", Function),
|
|
||||||
__metadata("design:paramtypes", [register_dto_1.RegisterDto]),
|
|
||||||
__metadata("design:returntype", void 0)
|
|
||||||
], AuthController.prototype, "register", null);
|
|
||||||
__decorate([
|
|
||||||
(0, public_decorator_1.Public)(),
|
|
||||||
(0, throttler_1.Throttle)({ default: { ttl: 60_000, limit: 10 } }),
|
|
||||||
(0, common_1.HttpCode)(common_1.HttpStatus.OK),
|
|
||||||
(0, common_1.Post)('login'),
|
|
||||||
__param(0, (0, common_1.Body)()),
|
|
||||||
__metadata("design:type", Function),
|
|
||||||
__metadata("design:paramtypes", [login_dto_1.LoginDto]),
|
|
||||||
__metadata("design:returntype", void 0)
|
|
||||||
], AuthController.prototype, "login", null);
|
|
||||||
exports.AuthController = AuthController = __decorate([
|
|
||||||
(0, common_1.Controller)('auth'),
|
|
||||||
__metadata("design:paramtypes", [auth_service_1.AuthService])
|
|
||||||
], AuthController);
|
|
||||||
//# sourceMappingURL=auth.controller.js.map
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"auth.controller.js","sourceRoot":"","sources":["../../src/auth/auth.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAA8E;AAC9E,iDAA6C;AAC7C,iDAA6C;AAC7C,qDAAiD;AACjD,+CAA2C;AAC3C,oEAAuD;AAGhD,IAAM,cAAc,GAApB,MAAM,cAAc;IACzB,YAA6B,WAAwB;QAAxB,gBAAW,GAAX,WAAW,CAAa;IAAG,CAAC;IAKzD,QAAQ,CAAS,GAAgB;QAC/B,OAAO,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC;IAMD,KAAK,CAAS,GAAa;QACzB,OAAO,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrC,CAAC;CACF,CAAA;AAjBY,wCAAc;AAMzB;IAHC,IAAA,yBAAM,GAAE;IACR,IAAA,oBAAQ,EAAC,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC;IACjD,IAAA,aAAI,EAAC,UAAU,CAAC;IACP,WAAA,IAAA,aAAI,GAAE,CAAA;;qCAAM,0BAAW;;8CAEhC;AAMD;IAJC,IAAA,yBAAM,GAAE;IACR,IAAA,oBAAQ,EAAC,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC;IACjD,IAAA,iBAAQ,EAAC,mBAAU,CAAC,EAAE,CAAC;IACvB,IAAA,aAAI,EAAC,OAAO,CAAC;IACP,WAAA,IAAA,aAAI,GAAE,CAAA;;qCAAM,oBAAQ;;2CAE1B;yBAhBU,cAAc;IAD1B,IAAA,mBAAU,EAAC,MAAM,CAAC;qCAEyB,0BAAW;GAD1C,cAAc,CAiB1B"}
|
|
||||||
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
export declare class AuthModule {
|
|
||||||
}
|
|
||||||
Vendored
-40
@@ -1,40 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.AuthModule = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
const jwt_1 = require("@nestjs/jwt");
|
|
||||||
const passport_1 = require("@nestjs/passport");
|
|
||||||
const auth_controller_1 = require("./auth.controller");
|
|
||||||
const auth_service_1 = require("./auth.service");
|
|
||||||
const jwt_strategy_1 = require("./jwt.strategy");
|
|
||||||
const users_module_1 = require("../users/users.module");
|
|
||||||
let AuthModule = class AuthModule {
|
|
||||||
};
|
|
||||||
exports.AuthModule = AuthModule;
|
|
||||||
exports.AuthModule = AuthModule = __decorate([
|
|
||||||
(0, common_1.Module)({
|
|
||||||
imports: [
|
|
||||||
users_module_1.UsersModule,
|
|
||||||
passport_1.PassportModule,
|
|
||||||
jwt_1.JwtModule.register({
|
|
||||||
secret: (() => {
|
|
||||||
const secret = process.env.JWT_SECRET;
|
|
||||||
if (!secret)
|
|
||||||
throw new Error('JWT_SECRET saknas i miljövariabler');
|
|
||||||
return secret;
|
|
||||||
})(),
|
|
||||||
signOptions: { expiresIn: '7d' },
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
controllers: [auth_controller_1.AuthController],
|
|
||||||
providers: [auth_service_1.AuthService, jwt_strategy_1.JwtStrategy],
|
|
||||||
exports: [auth_service_1.AuthService],
|
|
||||||
})
|
|
||||||
], AuthModule);
|
|
||||||
//# sourceMappingURL=auth.module.js.map
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"auth.module.js","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,qCAAwC;AACxC,+CAAkD;AAClD,uDAAmD;AACnD,iDAA6C;AAC7C,iDAA6C;AAC7C,wDAAoD;AAmB7C,IAAM,UAAU,GAAhB,MAAM,UAAU;CAAG,CAAA;AAAb,gCAAU;qBAAV,UAAU;IAjBtB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,0BAAW;YACX,yBAAc;YACd,eAAS,CAAC,QAAQ,CAAC;gBACjB,MAAM,EAAE,CAAC,GAAG,EAAE;oBACZ,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC;oBACtC,IAAI,CAAC,MAAM;wBAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;oBACnE,OAAO,MAAM,CAAC;gBAChB,CAAC,CAAC,EAAE;gBACJ,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;aACjC,CAAC;SACH;QACD,WAAW,EAAE,CAAC,gCAAc,CAAC;QAC7B,SAAS,EAAE,CAAC,0BAAW,EAAE,0BAAW,CAAC;QACrC,OAAO,EAAE,CAAC,0BAAW,CAAC;KACvB,CAAC;GACW,UAAU,CAAG"}
|
|
||||||
Vendored
-24
@@ -1,24 +0,0 @@
|
|||||||
import { JwtService } from '@nestjs/jwt';
|
|
||||||
import { UsersService } from '../users/users.service';
|
|
||||||
import { RegisterDto } from './dto/register.dto';
|
|
||||||
import { LoginDto } from './dto/login.dto';
|
|
||||||
export declare class AuthService {
|
|
||||||
private readonly usersService;
|
|
||||||
private readonly jwtService;
|
|
||||||
constructor(usersService: UsersService, jwtService: JwtService);
|
|
||||||
register(dto: RegisterDto): Promise<{
|
|
||||||
accessToken: string;
|
|
||||||
userId: number;
|
|
||||||
username: string;
|
|
||||||
role: string;
|
|
||||||
isPremium: boolean;
|
|
||||||
}>;
|
|
||||||
login(dto: LoginDto): Promise<{
|
|
||||||
accessToken: string;
|
|
||||||
userId: number;
|
|
||||||
username: string;
|
|
||||||
role: string;
|
|
||||||
isPremium: boolean;
|
|
||||||
}>;
|
|
||||||
private issueToken;
|
|
||||||
}
|
|
||||||
Vendored
-60
@@ -1,60 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.AuthService = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
const jwt_1 = require("@nestjs/jwt");
|
|
||||||
const bcrypt = require("bcryptjs");
|
|
||||||
const users_service_1 = require("../users/users.service");
|
|
||||||
let AuthService = class AuthService {
|
|
||||||
constructor(usersService, jwtService) {
|
|
||||||
this.usersService = usersService;
|
|
||||||
this.jwtService = jwtService;
|
|
||||||
}
|
|
||||||
async register(dto) {
|
|
||||||
const existing = await this.usersService.findByUsername(dto.username);
|
|
||||||
if (existing)
|
|
||||||
throw new common_1.ConflictException('Användarnamnet är redan taget');
|
|
||||||
const passwordHash = await bcrypt.hash(dto.password, 12);
|
|
||||||
const user = await this.usersService.create({
|
|
||||||
username: dto.username,
|
|
||||||
email: dto.email,
|
|
||||||
passwordHash,
|
|
||||||
});
|
|
||||||
return this.issueToken(user.id, user.username, user.role, user.isPremium);
|
|
||||||
}
|
|
||||||
async login(dto) {
|
|
||||||
const user = await this.usersService.findByUsername(dto.username);
|
|
||||||
if (!user)
|
|
||||||
throw new common_1.UnauthorizedException('Felaktigt användarnamn eller lösenord');
|
|
||||||
const valid = await bcrypt.compare(dto.password, user.passwordHash);
|
|
||||||
if (!valid)
|
|
||||||
throw new common_1.UnauthorizedException('Felaktigt användarnamn eller lösenord');
|
|
||||||
return this.issueToken(user.id, user.username, user.role, user.isPremium);
|
|
||||||
}
|
|
||||||
issueToken(userId, username, role, isPremium) {
|
|
||||||
const payload = { sub: userId, username, role, isPremium };
|
|
||||||
return {
|
|
||||||
accessToken: this.jwtService.sign(payload),
|
|
||||||
userId,
|
|
||||||
username,
|
|
||||||
role,
|
|
||||||
isPremium,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
exports.AuthService = AuthService;
|
|
||||||
exports.AuthService = AuthService = __decorate([
|
|
||||||
(0, common_1.Injectable)(),
|
|
||||||
__metadata("design:paramtypes", [users_service_1.UsersService,
|
|
||||||
jwt_1.JwtService])
|
|
||||||
], AuthService);
|
|
||||||
//# sourceMappingURL=auth.service.js.map
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"auth.service.js","sourceRoot":"","sources":["../../src/auth/auth.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAIwB;AACxB,qCAAyC;AACzC,mCAAmC;AACnC,0DAAsD;AAK/C,IAAM,WAAW,GAAjB,MAAM,WAAW;IACtB,YACmB,YAA0B,EAC1B,UAAsB;QADtB,iBAAY,GAAZ,YAAY,CAAc;QAC1B,eAAU,GAAV,UAAU,CAAY;IACtC,CAAC;IAEJ,KAAK,CAAC,QAAQ,CAAC,GAAgB;QAC7B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACtE,IAAI,QAAQ;YAAE,MAAM,IAAI,0BAAiB,CAAC,+BAA+B,CAAC,CAAC;QAE3E,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACzD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;YAC1C,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,YAAY;SACb,CAAC,CAAC;QAEH,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5E,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,GAAa;QACvB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAClE,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,8BAAqB,CAAC,uCAAuC,CAAC,CAAC;QAEpF,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QACpE,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,8BAAqB,CAAC,uCAAuC,CAAC,CAAC;QAErF,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5E,CAAC;IAEO,UAAU,CAAC,MAAc,EAAE,QAAgB,EAAE,IAAY,EAAE,SAAkB;QACnF,MAAM,OAAO,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;QAC3D,OAAO;YACL,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC;YAC1C,MAAM;YACN,QAAQ;YACR,IAAI;YACJ,SAAS;SACV,CAAC;IACJ,CAAC;CACF,CAAA;AAxCY,kCAAW;sBAAX,WAAW;IADvB,IAAA,mBAAU,GAAE;qCAGsB,4BAAY;QACd,gBAAU;GAH9B,WAAW,CAwCvB"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export declare const CurrentUser: (...dataOrPipes: unknown[]) => ParameterDecorator;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.CurrentUser = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
exports.CurrentUser = (0, common_1.createParamDecorator)((_data, ctx) => {
|
|
||||||
const request = ctx.switchToHttp().getRequest();
|
|
||||||
return request.user;
|
|
||||||
});
|
|
||||||
//# sourceMappingURL=current-user.decorator.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"current-user.decorator.js","sourceRoot":"","sources":["../../../src/auth/decorators/current-user.decorator.ts"],"names":[],"mappings":";;;AAAA,2CAAwE;AAE3D,QAAA,WAAW,GAAG,IAAA,6BAAoB,EAC7C,CAAC,KAAc,EAAE,GAAqB,EAAE,EAAE;IACxC,MAAM,OAAO,GAAG,GAAG,CAAC,YAAY,EAAE,CAAC,UAAU,EAAE,CAAC;IAChD,OAAO,OAAO,CAAC,IAA4C,CAAC;AAC9D,CAAC,CACF,CAAC"}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export declare const IS_PUBLIC_KEY = "isPublic";
|
|
||||||
export declare const Public: () => import("@nestjs/common").CustomDecorator<string>;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.Public = exports.IS_PUBLIC_KEY = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
exports.IS_PUBLIC_KEY = 'isPublic';
|
|
||||||
const Public = () => (0, common_1.SetMetadata)(exports.IS_PUBLIC_KEY, true);
|
|
||||||
exports.Public = Public;
|
|
||||||
//# sourceMappingURL=public.decorator.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"public.decorator.js","sourceRoot":"","sources":["../../../src/auth/decorators/public.decorator.ts"],"names":[],"mappings":";;;AAAA,2CAA6C;AAEhC,QAAA,aAAa,GAAG,UAAU,CAAC;AACjC,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,IAAA,oBAAW,EAAC,qBAAa,EAAE,IAAI,CAAC,CAAC;AAAhD,QAAA,MAAM,UAA0C"}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export declare const ROLES_KEY = "roles";
|
|
||||||
export declare const Roles: (...roles: string[]) => import("@nestjs/common").CustomDecorator<string>;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.Roles = exports.ROLES_KEY = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
exports.ROLES_KEY = 'roles';
|
|
||||||
const Roles = (...roles) => (0, common_1.SetMetadata)(exports.ROLES_KEY, roles);
|
|
||||||
exports.Roles = Roles;
|
|
||||||
//# sourceMappingURL=roles.decorator.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"roles.decorator.js","sourceRoot":"","sources":["../../../src/auth/decorators/roles.decorator.ts"],"names":[],"mappings":";;;AAAA,2CAA6C;AAEhC,QAAA,SAAS,GAAG,OAAO,CAAC;AAC1B,MAAM,KAAK,GAAG,CAAC,GAAG,KAAe,EAAE,EAAE,CAAC,IAAA,oBAAW,EAAC,iBAAS,EAAE,KAAK,CAAC,CAAC;AAA9D,QAAA,KAAK,SAAyD"}
|
|
||||||
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
export declare class LoginDto {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
Vendored
-25
@@ -1,25 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.LoginDto = void 0;
|
|
||||||
const class_validator_1 = require("class-validator");
|
|
||||||
class LoginDto {
|
|
||||||
}
|
|
||||||
exports.LoginDto = LoginDto;
|
|
||||||
__decorate([
|
|
||||||
(0, class_validator_1.IsString)(),
|
|
||||||
__metadata("design:type", String)
|
|
||||||
], LoginDto.prototype, "username", void 0);
|
|
||||||
__decorate([
|
|
||||||
(0, class_validator_1.IsString)(),
|
|
||||||
__metadata("design:type", String)
|
|
||||||
], LoginDto.prototype, "password", void 0);
|
|
||||||
//# sourceMappingURL=login.dto.js.map
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"login.dto.js","sourceRoot":"","sources":["../../../src/auth/dto/login.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAA2C;AAE3C,MAAa,QAAQ;CAMpB;AAND,4BAMC;AAJC;IADC,IAAA,0BAAQ,GAAE;;0CACM;AAGjB;IADC,IAAA,0BAAQ,GAAE;;0CACM"}
|
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
export declare class RegisterDto {
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.RegisterDto = void 0;
|
|
||||||
const class_validator_1 = require("class-validator");
|
|
||||||
class RegisterDto {
|
|
||||||
}
|
|
||||||
exports.RegisterDto = RegisterDto;
|
|
||||||
__decorate([
|
|
||||||
(0, class_validator_1.IsString)(),
|
|
||||||
__metadata("design:type", String)
|
|
||||||
], RegisterDto.prototype, "username", void 0);
|
|
||||||
__decorate([
|
|
||||||
(0, class_validator_1.IsEmail)(),
|
|
||||||
__metadata("design:type", String)
|
|
||||||
], RegisterDto.prototype, "email", void 0);
|
|
||||||
__decorate([
|
|
||||||
(0, class_validator_1.IsString)(),
|
|
||||||
(0, class_validator_1.MinLength)(8),
|
|
||||||
__metadata("design:type", String)
|
|
||||||
], RegisterDto.prototype, "password", void 0);
|
|
||||||
//# sourceMappingURL=register.dto.js.map
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"register.dto.js","sourceRoot":"","sources":["../../../src/auth/dto/register.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAA+D;AAE/D,MAAa,WAAW;CAUvB;AAVD,kCAUC;AARC;IADC,IAAA,0BAAQ,GAAE;;6CACM;AAGjB;IADC,IAAA,yBAAO,GAAE;;0CACI;AAId;IAFC,IAAA,0BAAQ,GAAE;IACV,IAAA,2BAAS,EAAC,CAAC,CAAC;;6CACI"}
|
|
||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
import { ExecutionContext } from '@nestjs/common';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { Reflector } from '@nestjs/core';
|
|
||||||
declare const JwtAuthGuard_base: import("@nestjs/passport").Type<import("@nestjs/passport").IAuthGuard>;
|
|
||||||
export declare class JwtAuthGuard extends JwtAuthGuard_base {
|
|
||||||
private reflector;
|
|
||||||
private readonly logger;
|
|
||||||
constructor(reflector: Reflector);
|
|
||||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean>;
|
|
||||||
handleRequest<TUser = any>(err: any, user: TUser, info: any): TUser;
|
|
||||||
}
|
|
||||||
export {};
|
|
||||||
Vendored
-48
@@ -1,48 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
||||||
};
|
|
||||||
var JwtAuthGuard_1;
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.JwtAuthGuard = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
const core_1 = require("@nestjs/core");
|
|
||||||
const passport_1 = require("@nestjs/passport");
|
|
||||||
const public_decorator_1 = require("./decorators/public.decorator");
|
|
||||||
let JwtAuthGuard = JwtAuthGuard_1 = class JwtAuthGuard extends (0, passport_1.AuthGuard)('jwt') {
|
|
||||||
constructor(reflector) {
|
|
||||||
super();
|
|
||||||
this.reflector = reflector;
|
|
||||||
this.logger = new common_1.Logger(JwtAuthGuard_1.name);
|
|
||||||
}
|
|
||||||
canActivate(context) {
|
|
||||||
const isPublic = this.reflector.getAllAndOverride(public_decorator_1.IS_PUBLIC_KEY, [
|
|
||||||
context.getHandler(),
|
|
||||||
context.getClass(),
|
|
||||||
]);
|
|
||||||
if (isPublic)
|
|
||||||
return true;
|
|
||||||
return super.canActivate(context);
|
|
||||||
}
|
|
||||||
handleRequest(err, user, info) {
|
|
||||||
if (err || !user) {
|
|
||||||
throw err || new common_1.UnauthorizedException();
|
|
||||||
}
|
|
||||||
if (user) {
|
|
||||||
this.logger.log(`Authenticated user ID: ${user.userId}`);
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
exports.JwtAuthGuard = JwtAuthGuard;
|
|
||||||
exports.JwtAuthGuard = JwtAuthGuard = JwtAuthGuard_1 = __decorate([
|
|
||||||
(0, common_1.Injectable)(),
|
|
||||||
__metadata("design:paramtypes", [core_1.Reflector])
|
|
||||||
], JwtAuthGuard);
|
|
||||||
//# sourceMappingURL=jwt-auth.guard.js.map
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"jwt-auth.guard.js","sourceRoot":"","sources":["../../src/auth/jwt-auth.guard.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAA0G;AAE1G,uCAAyC;AACzC,+CAA6C;AAC7C,oEAA8D;AAGvD,IAAM,YAAY,oBAAlB,MAAM,YAAa,SAAQ,IAAA,oBAAS,EAAC,KAAK,CAAC;IAGhD,YAAoB,SAAoB;QACtC,KAAK,EAAE,CAAC;QADU,cAAS,GAAT,SAAS,CAAW;QAFvB,WAAM,GAAG,IAAI,eAAM,CAAC,cAAY,CAAC,IAAI,CAAC,CAAC;IAIxD,CAAC;IAED,WAAW,CAAC,OAAyB;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAU,gCAAa,EAAE;YACxE,OAAO,CAAC,UAAU,EAAE;YACpB,OAAO,CAAC,QAAQ,EAAE;SACnB,CAAC,CAAC;QACH,IAAI,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC1B,OAAO,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;IAGD,aAAa,CAAc,GAAQ,EAAE,IAAW,EAAE,IAAS;QACzD,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YACjB,MAAM,GAAG,IAAI,IAAI,8BAAqB,EAAE,CAAC;QAC3C,CAAC;QACD,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,0BAA2B,IAAY,CAAC,MAAM,EAAE,CAAC,CAAC;QACpE,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AA1BY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,mBAAU,GAAE;qCAIoB,gBAAS;GAH7B,YAAY,CA0BxB"}
|
|
||||||
Vendored
-21
@@ -1,21 +0,0 @@
|
|||||||
import { Strategy } from 'passport-jwt';
|
|
||||||
declare const JwtStrategy_base: new (...args: any[]) => Strategy;
|
|
||||||
export declare class JwtStrategy extends JwtStrategy_base {
|
|
||||||
private readonly logger;
|
|
||||||
constructor();
|
|
||||||
validate(payload: {
|
|
||||||
sub?: number;
|
|
||||||
id?: number;
|
|
||||||
userId?: number;
|
|
||||||
username?: string;
|
|
||||||
role?: string;
|
|
||||||
isPremium?: boolean;
|
|
||||||
}): Promise<{
|
|
||||||
id: number | undefined;
|
|
||||||
userId: number | undefined;
|
|
||||||
username: string | undefined;
|
|
||||||
role: string;
|
|
||||||
isPremium: boolean;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
export {};
|
|
||||||
Vendored
-43
@@ -1,43 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
||||||
};
|
|
||||||
var JwtStrategy_1;
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.JwtStrategy = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
const passport_1 = require("@nestjs/passport");
|
|
||||||
const passport_jwt_1 = require("passport-jwt");
|
|
||||||
let JwtStrategy = JwtStrategy_1 = class JwtStrategy extends (0, passport_1.PassportStrategy)(passport_jwt_1.Strategy) {
|
|
||||||
constructor() {
|
|
||||||
super({
|
|
||||||
jwtFromRequest: passport_jwt_1.ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
||||||
ignoreExpiration: false,
|
|
||||||
secretOrKey: process.env.JWT_SECRET,
|
|
||||||
});
|
|
||||||
this.logger = new common_1.Logger(JwtStrategy_1.name);
|
|
||||||
}
|
|
||||||
async validate(payload) {
|
|
||||||
const resolvedUserId = payload.sub ?? payload.id ?? payload.userId;
|
|
||||||
this.logger.log(`Validating token for user ID: ${resolvedUserId}, Username: ${payload.username}`);
|
|
||||||
return {
|
|
||||||
id: resolvedUserId,
|
|
||||||
userId: resolvedUserId,
|
|
||||||
username: payload.username,
|
|
||||||
role: payload.role ?? 'user',
|
|
||||||
isPremium: payload.isPremium ?? false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
exports.JwtStrategy = JwtStrategy;
|
|
||||||
exports.JwtStrategy = JwtStrategy = JwtStrategy_1 = __decorate([
|
|
||||||
(0, common_1.Injectable)(),
|
|
||||||
__metadata("design:paramtypes", [])
|
|
||||||
], JwtStrategy);
|
|
||||||
//# sourceMappingURL=jwt.strategy.js.map
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"jwt.strategy.js","sourceRoot":"","sources":["../../src/auth/jwt.strategy.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAAoD;AACpD,+CAAoD;AACpD,+CAAoD;AAG7C,IAAM,WAAW,mBAAjB,MAAM,WAAY,SAAQ,IAAA,2BAAgB,EAAC,uBAAQ,CAAC;IAGzD;QACE,KAAK,CAAC;YACJ,cAAc,EAAE,yBAAU,CAAC,2BAA2B,EAAE;YACxD,gBAAgB,EAAE,KAAK;YACvB,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU;SACpC,CAAC,CAAC;QAPY,WAAM,GAAG,IAAI,eAAM,CAAC,aAAW,CAAC,IAAI,CAAC,CAAC;IAQvD,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,OAA8G;QAC3H,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,EAAE,IAAI,OAAO,CAAC,MAAM,CAAC;QACnE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,iCAAiC,cAAc,eAAe,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;QAClG,OAAO;YACL,EAAE,EAAE,cAAc;YAClB,MAAM,EAAE,cAAc;YACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,MAAM;YAC5B,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,KAAK;SACtC,CAAC;IACJ,CAAC;CACF,CAAA;AAtBY,kCAAW;sBAAX,WAAW;IADvB,IAAA,mBAAU,GAAE;;GACA,WAAW,CAsBvB"}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
||||||
export declare class PremiumOrAdminGuard implements CanActivate {
|
|
||||||
canActivate(context: ExecutionContext): boolean;
|
|
||||||
}
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.PremiumOrAdminGuard = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
let PremiumOrAdminGuard = class PremiumOrAdminGuard {
|
|
||||||
canActivate(context) {
|
|
||||||
const { user } = context.switchToHttp().getRequest();
|
|
||||||
if (user?.role === 'admin' || user?.isPremium === true) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
throw new common_1.ForbiddenException('Denna funktion kräver premiumkonto eller admin-behörighet.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
exports.PremiumOrAdminGuard = PremiumOrAdminGuard;
|
|
||||||
exports.PremiumOrAdminGuard = PremiumOrAdminGuard = __decorate([
|
|
||||||
(0, common_1.Injectable)()
|
|
||||||
], PremiumOrAdminGuard);
|
|
||||||
//# sourceMappingURL=premium-or-admin.guard.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"premium-or-admin.guard.js","sourceRoot":"","sources":["../../src/auth/premium-or-admin.guard.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA+F;AAOxF,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAC9B,WAAW,CAAC,OAAyB;QACnC,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAE,CAAC;QAErD,IAAI,IAAI,EAAE,IAAI,KAAK,OAAO,IAAI,IAAI,EAAE,SAAS,KAAK,IAAI,EAAE,CAAC;YACvD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,IAAI,2BAAkB,CAAC,4DAA4D,CAAC,CAAC;IAC7F,CAAC;CACF,CAAA;AAVY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,mBAAU,GAAE;GACA,mBAAmB,CAU/B"}
|
|
||||||
Vendored
-7
@@ -1,7 +0,0 @@
|
|||||||
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
||||||
import { Reflector } from '@nestjs/core';
|
|
||||||
export declare class RolesGuard implements CanActivate {
|
|
||||||
private reflector;
|
|
||||||
constructor(reflector: Reflector);
|
|
||||||
canActivate(context: ExecutionContext): boolean;
|
|
||||||
}
|
|
||||||
Vendored
-39
@@ -1,39 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.RolesGuard = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
const core_1 = require("@nestjs/core");
|
|
||||||
const roles_decorator_1 = require("./decorators/roles.decorator");
|
|
||||||
let RolesGuard = class RolesGuard {
|
|
||||||
constructor(reflector) {
|
|
||||||
this.reflector = reflector;
|
|
||||||
}
|
|
||||||
canActivate(context) {
|
|
||||||
const requiredRoles = this.reflector.getAllAndOverride(roles_decorator_1.ROLES_KEY, [
|
|
||||||
context.getHandler(),
|
|
||||||
context.getClass(),
|
|
||||||
]);
|
|
||||||
if (!requiredRoles || requiredRoles.length === 0)
|
|
||||||
return true;
|
|
||||||
const { user } = context.switchToHttp().getRequest();
|
|
||||||
if (!user?.role || !requiredRoles.includes(user.role)) {
|
|
||||||
throw new common_1.ForbiddenException('Åtkomst nekad — admin-behörighet krävs');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
exports.RolesGuard = RolesGuard;
|
|
||||||
exports.RolesGuard = RolesGuard = __decorate([
|
|
||||||
(0, common_1.Injectable)(),
|
|
||||||
__metadata("design:paramtypes", [core_1.Reflector])
|
|
||||||
], RolesGuard);
|
|
||||||
//# sourceMappingURL=roles.guard.js.map
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"roles.guard.js","sourceRoot":"","sources":["../../src/auth/roles.guard.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA+F;AAC/F,uCAAyC;AACzC,kEAAyD;AAGlD,IAAM,UAAU,GAAhB,MAAM,UAAU;IACrB,YAAoB,SAAoB;QAApB,cAAS,GAAT,SAAS,CAAW;IAAG,CAAC;IAE5C,WAAW,CAAC,OAAyB;QACnC,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAW,2BAAS,EAAE;YAC1E,OAAO,CAAC,UAAU,EAAE;YACpB,OAAO,CAAC,QAAQ,EAAE;SACnB,CAAC,CAAC;QAGH,IAAI,CAAC,aAAa,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAE9D,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAE,CAAC;QACrD,IAAI,CAAC,IAAI,EAAE,IAAI,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACtD,MAAM,IAAI,2BAAkB,CAAC,wCAAwC,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAA;AAlBY,gCAAU;qBAAV,UAAU;IADtB,IAAA,mBAAU,GAAE;qCAEoB,gBAAS;GAD7B,UAAU,CAkBtB"}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { CategoriesService } from './categories.service';
|
|
||||||
export declare class CategoriesController {
|
|
||||||
private readonly categoriesService;
|
|
||||||
constructor(categoriesService: CategoriesService);
|
|
||||||
findAll(): Promise<{
|
|
||||||
name: string;
|
|
||||||
id: number;
|
|
||||||
parentId: number | null;
|
|
||||||
}[]>;
|
|
||||||
findTree(): Promise<import("./categories.service").CategoryNode[]>;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
||||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
||||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
||||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
||||||
};
|
|
||||||
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
||||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
||||||
};
|
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
|
||||||
exports.CategoriesController = void 0;
|
|
||||||
const common_1 = require("@nestjs/common");
|
|
||||||
const categories_service_1 = require("./categories.service");
|
|
||||||
const public_decorator_1 = require("../auth/decorators/public.decorator");
|
|
||||||
let CategoriesController = class CategoriesController {
|
|
||||||
constructor(categoriesService) {
|
|
||||||
this.categoriesService = categoriesService;
|
|
||||||
}
|
|
||||||
findAll() {
|
|
||||||
return this.categoriesService.findAll();
|
|
||||||
}
|
|
||||||
findTree() {
|
|
||||||
return this.categoriesService.findTree();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
exports.CategoriesController = CategoriesController;
|
|
||||||
__decorate([
|
|
||||||
(0, public_decorator_1.Public)(),
|
|
||||||
(0, common_1.Get)(),
|
|
||||||
__metadata("design:type", Function),
|
|
||||||
__metadata("design:paramtypes", []),
|
|
||||||
__metadata("design:returntype", void 0)
|
|
||||||
], CategoriesController.prototype, "findAll", null);
|
|
||||||
__decorate([
|
|
||||||
(0, public_decorator_1.Public)(),
|
|
||||||
(0, common_1.Get)('tree'),
|
|
||||||
__metadata("design:type", Function),
|
|
||||||
__metadata("design:paramtypes", []),
|
|
||||||
__metadata("design:returntype", void 0)
|
|
||||||
], CategoriesController.prototype, "findTree", null);
|
|
||||||
exports.CategoriesController = CategoriesController = __decorate([
|
|
||||||
(0, common_1.Controller)('categories'),
|
|
||||||
__metadata("design:paramtypes", [categories_service_1.CategoriesService])
|
|
||||||
], CategoriesController);
|
|
||||||
//# sourceMappingURL=categories.controller.js.map
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"file":"categories.controller.js","sourceRoot":"","sources":["../../src/categories/categories.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAiD;AACjD,6DAAyD;AACzD,0EAA6D;AAGtD,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IAC/B,YAA6B,iBAAoC;QAApC,sBAAiB,GAAjB,iBAAiB,CAAmB;IAAG,CAAC;IAIrE,OAAO;QACL,OAAO,IAAI,CAAC,iBAAiB,CAAC,OAAO,EAAE,CAAC;IAC1C,CAAC;IAID,QAAQ;QACN,OAAO,IAAI,CAAC,iBAAiB,CAAC,QAAQ,EAAE,CAAC;IAC3C,CAAC;CACF,CAAA;AAdY,oDAAoB;AAK/B;IAFC,IAAA,yBAAM,GAAE;IACR,IAAA,YAAG,GAAE;;;;mDAGL;AAID;IAFC,IAAA,yBAAM,GAAE;IACR,IAAA,YAAG,EAAC,MAAM,CAAC;;;;oDAGX;+BAbU,oBAAoB;IADhC,IAAA,mBAAU,EAAC,YAAY,CAAC;qCAEyB,sCAAiB;GADtD,oBAAoB,CAchC"}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export declare class CategoriesModule {
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user