Add receipt extraction, manual crop, and UX improvements

- Add Claude Haiku vision integration to extract amount and date from
  receipt photos, re-reading on photo replacement
- Add manual crop overlay with draggable handles for receipt photos
- Open camera directly when tapping + to add new receipt
- Make add/edit modal scrollable on small screens
- Show "Tap to change photo" hint on uploaded photos
- Include api-key in Docker image for Anthropic API access

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 23:35:16 -06:00
parent 9c35cc71e9
commit 6e93b7f672
3 changed files with 388 additions and 7 deletions

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""Receipt Manager backend — proxies receipt data and photos to Nextcloud via WebDAV."""
import base64
import getpass
import hashlib
import io
@@ -30,6 +31,8 @@ PHOTOS_DIR = "photos"
# Server-side session store: {token: True}
SESSIONS = {}
ANTHROPIC_API_KEY = ""
def hash_password(password):
"""Hash a password with SHA-256."""
@@ -159,6 +162,59 @@ def build_excel(receipts):
return buf.getvalue()
# --- Receipt extraction via Claude Haiku ---------------------------------------
def extract_receipt_info(jpeg_bytes):
"""Use Claude Haiku vision to extract total amount and date from a receipt image."""
try:
img_b64 = base64.standard_b64encode(jpeg_bytes).decode("ascii")
resp = requests.post(
"https://api.anthropic.com/v1/messages",
headers={
"x-api-key": ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
"Content-Type": "application/json",
},
json={
"model": "claude-3-haiku-20240307",
"max_tokens": 200,
"messages": [{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": img_b64,
},
},
{
"type": "text",
"text": 'Extract the total amount and transaction date from this receipt. Reply with JSON only: {"amount": number_or_null, "date": "YYYY-MM-DD"_or_null}',
},
],
}],
},
timeout=30,
)
resp.raise_for_status()
data = resp.json()
text = data["content"][0]["text"].strip()
# Extract JSON from response (may be wrapped in markdown code block)
m = re.search(r"\{.*\}", text, re.DOTALL)
if m:
parsed = json.loads(m.group())
return {
"amount": parsed.get("amount"),
"date": parsed.get("date"),
}
except Exception as e:
print(f"[extract_receipt_info] Error: {e}")
return {"amount": None, "date": None}
# --- HTTP handler -------------------------------------------------------------
class ReuseTCPServer(HTTPServer):
@@ -347,6 +403,18 @@ class Handler(BaseHTTPRequestHandler):
self._send_error(500, str(e))
return
# POST /api/extract-receipt — extract total + date from receipt image
if self.path == "/api/extract-receipt":
if not self._check_session():
return
try:
body = self._read_body()
result = extract_receipt_info(body)
self._send_json(result)
except Exception as e:
self._send_json({"amount": None, "date": None})
return
# POST /api/photos/<id> — upload photo
m = re.fullmatch(r"/api/photos/([A-Za-z0-9_-]+)", self.path)
if m:
@@ -420,9 +488,18 @@ def ensure_folder_path():
def main():
global NC_USERNAME, NC_PASSWORD, NC_DAV_ROOT, NC_AUTH
global NC_USERNAME, NC_PASSWORD, NC_DAV_ROOT, NC_AUTH, ANTHROPIC_API_KEY
print("=== Receipt Manager — Nextcloud Backend ===\n")
# Load Anthropic API key
try:
with open("api-key", "r") as f:
ANTHROPIC_API_KEY = f.read().strip()
print("Anthropic API key loaded.")
except FileNotFoundError:
print("WARNING: api-key file not found — receipt extraction disabled.")
if not NC_USERNAME:
NC_USERNAME = input("Nextcloud username: ").strip()
if not NC_PASSWORD: