Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ Kin-CRM is published to the Unraid Community Applications store from this reposi
- The official template points at `ghcr.io/ock666/kin-crm:latest` and installs with app data in `/mnt/user/appdata/kin-crm` mapped to `/data` inside the container.
- If a listing built by a third party shows up first, prefer the one whose **Support/Project** links point to `github.com/ock666/Kin-CRM` — the author's listing is the source of truth.

Your data lives in the Docker volume at `/mnt/user/appdata/personal-crm_kin_data` mapped to `/data` inside the container — a SQLite database plus any cached Instagram session files. Back it up like any other volume, or export from the in-app Export page anytime.
Your data lives in the Docker volume at `/mnt/user/appdata/personal-crm_kin_data` mapped to `/data` inside the container — a SQLite database. Back it up like any other volume, or export from the in-app Export page anytime.

### Development

Expand Down Expand Up @@ -306,10 +306,6 @@ Kin can push birthdays and notable dates into any calendar that can subscribe to
- Keep the token secret — anyone with the link can see the synced dates. Rotate it by turning the feed off, saving, then turning it back on.
- Only the dates you've chosen to sync are included; archived people are skipped.

### Instagram integration (use with caution)

Kin includes an optional, unofficial Instagram reader using [instagrapi](https://github.com/subzeroid/instagrapi). It's against Instagram's Terms of Service. Use a throwaway/secondary account only — never your primary account. Nothing is ever posted or messaged. Posts land in the Review Queue for your approval. Leave it disabled if you'd rather not risk it; everything else works fine without it.

<p align="right">(<a href="#top">back to top</a>)</p>


Expand All @@ -334,7 +330,7 @@ Kin includes an optional, unofficial Instagram reader using [instagrapi](https:/
- **Scratchpad**: fleeting "bring up next time" reminders pinned on the person's profile
- **Notable people**: lightweight references to people in their life without full CRM profiles
- **Notable dates**: anniversaries, kids' birthdays, recurring dates
- **In-page photo viewer**: click any photo thumbnail (memories, timeline, gallery, Instagram) to dim the page and view the full-size image — dismiss with ✕, Esc, or a click on the backdrop
- **In-page photo viewer**: click any photo thumbnail (memories, timeline, gallery) to dim the page and view the full-size image — dismiss with ✕, Esc, or a click on the backdrop

### Journal: quick-capture logging
- One text box. Optional title, date, location, energy cost (low/medium/high), event type
Expand Down Expand Up @@ -394,7 +390,7 @@ Kin includes an optional, unofficial Instagram reader using [instagrapi](https:/
- Offline-first: app shell caches and works without a connection

### Data ownership
- Full JSON export (people, journal, tags, conflicts, chat transcripts, resolution plans, gift ideas, Instagram posts, settings)
- Full JSON export (people, journal, tags, conflicts, chat transcripts, resolution plans, gift ideas, settings)
- CSV export (people + journal)
- JSON/CSV import (get-or-create by name, non-destructive)
- All data in a local SQLite database (or Postgres if you prefer)
Expand Down
4 changes: 1 addition & 3 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,14 @@ def _load_or_create_secret() -> str:

class Settings:
APP_NAME = "Kin — Personal Relationship Manager"
APP_VERSION = "2026.09.2" # date-based: YYYY.MM.N (N = release within the month)
APP_VERSION = "2026.09.4" # date-based: YYYY.MM.N (N = release within the month)
DATABASE_URL: str = (os.environ.get("DATABASE_URL") or f"sqlite:///{DATA_DIR}/app.db")
SESSION_SECRET: str = _load_or_create_secret()
DATA_DIR: Path = DATA_DIR
UPLOAD_DIR: Path = DATA_DIR / "uploads"
INSTAGRAM_SESSION_DIR: Path = DATA_DIR / "ig_sessions"
TIMEZONE: str = os.environ.get("TZ", "UTC")
DISABLE_SCHEDULER: bool = os.environ.get("DISABLE_SCHEDULER", "0") == "1"


settings = Settings()
settings.UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
settings.INSTAGRAM_SESSION_DIR.mkdir(parents=True, exist_ok=True)
27 changes: 0 additions & 27 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,6 @@ class Person(Base):
# Immich linkage
immich_person_id = Column(String(100), nullable=True, index=True)

# Instagram linkage
instagram_username = Column(String(255), nullable=True)
instagram_enabled = Column(Boolean, default=False)
instagram_last_checked = Column(DateTime, nullable=True)
instagram_last_error = Column(Text, nullable=True)

# Check-in cadence (AuDHD-friendly nudges)
checkin_cadence_days = Column(Integer, nullable=True) # None = no reminder
checkin_snoozed_until = Column(Date, nullable=True)
Expand All @@ -147,7 +141,6 @@ class Person(Base):

tags = relationship("Tag", secondary=person_tags, back_populates="people")
notable_dates = relationship("NotableDate", back_populates="person", cascade="all, delete-orphan")
instagram_posts = relationship("InstagramPost", back_populates="person", cascade="all, delete-orphan")
birthday_drafts = relationship("BirthdayMessageDraft", back_populates="person", cascade="all, delete-orphan")
scratchpad_items = relationship(
"ScratchpadItem", back_populates="person", cascade="all, delete-orphan",
Expand Down Expand Up @@ -286,26 +279,6 @@ class ReviewStatus(str, enum.Enum):
skipped = "skipped"


class InstagramPost(Base):
__tablename__ = "instagram_posts"

id = Column(Integer, primary_key=True)
person_id = Column(Integer, ForeignKey("people.id", ondelete="CASCADE"), nullable=False)
ig_post_id = Column(String(100), nullable=False, index=True)
caption = Column(Text, nullable=True)
media_url = Column(String(1000), nullable=True)
permalink = Column(String(500), nullable=True)
post_type = Column(String(50), nullable=True)
posted_at = Column(DateTime, nullable=True)
fetched_at = Column(DateTime, default=utcnow)
status = Column(Enum(ReviewStatus), default=ReviewStatus.pending)
imported_as_journal_entry_id = Column(Integer, ForeignKey("journal_entries.id"), nullable=True)

person = relationship("Person", back_populates="instagram_posts")

__table_args__ = (UniqueConstraint("person_id", "ig_post_id", name="uq_person_ig_post"),)


class BirthdayMessageDraft(Base):
__tablename__ = "birthday_message_drafts"

Expand Down
5 changes: 2 additions & 3 deletions app/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from sqlalchemy.orm import Session

from .config import settings
from .models import InstagramPost, BirthdayMessageDraft, ReviewStatus
from .models import BirthdayMessageDraft, ReviewStatus
from .services import whatsnew

TEMPLATES_DIR = Path(__file__).parent / "templates"
Expand All @@ -20,9 +20,8 @@
def _pending_review_count(db: Session | None) -> int:
if db is None:
return 0
ig = db.query(InstagramPost).filter_by(status=ReviewStatus.pending).count()
bd = db.query(BirthdayMessageDraft).filter_by(status=ReviewStatus.pending).count()
return ig + bd
return bd


def _wrapped_ready(db: Session | None) -> bool:
Expand Down
2 changes: 0 additions & 2 deletions app/routers/api/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@


@router.post("/json")
def export_json(db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 19 in app/routers/api/export.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
people = db.query(Person).order_by(Person.name).all()
data = {
"format": "kin-api-export",
Expand All @@ -28,18 +28,16 @@
data["exported_people"].append({
"name": p.name, "nickname": p.nickname, "pronouns": p.pronouns,
"relationship_label": p.relationship_label,
"birthday": f"{p.birthday_month}/{p.birthday_day}/{p.birthday_year or ''}" if p.birthday_month else None,
"how_we_met": p.how_we_met,
"met_date": p.met_date.isoformat() if p.met_date else None,
"location": p.location, "phone": p.phone, "email": p.email, "notes": p.notes,
"occupation": p.occupation, "hobbies": p.hobbies, "bio": p.bio,
"ai_summary": p.ai_summary,
"archived": p.archived,
"checkin_cadence_days": p.checkin_cadence_days,

Check warning on line 38 in app/routers/api/export.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 7 lines is too similar to app/routers/api/people.py:42
"last_contact_date": p.last_contact_date.isoformat() if p.last_contact_date else None,
"relationship_state": p.relationship_state.value if p.relationship_state else "none",
"instagram_username": p.instagram_username,
"instagram_enabled": p.instagram_enabled,
"tags": [t.name for t in p.tags],
"journal_entries": [{
"date": e.entry_date.isoformat(), "title": e.title, "body": e.body,
Expand All @@ -57,7 +55,7 @@


@router.post("/csv/people")
def export_csv_people(db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 58 in app/routers/api/export.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
people = db.query(Person).all()
buf = io.StringIO()
writer = csv.writer(buf)
Expand Down
4 changes: 0 additions & 4 deletions app/routers/api/people.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from sqlalchemy.orm import Session

from ...database import get_db
from ...models import Person, Tag, JournalEntry, ConflictLog

Check warning on line 8 in app/routers/api/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-import

Unused JournalEntry imported from models

Check warning on line 8 in app/routers/api/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-import

Unused ConflictLog imported from models
from ...schemas.people import PersonCreate, PersonUpdate, PersonResponse, TagResponse

Check warning on line 9 in app/routers/api/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-import

Unused PersonResponse imported from schemas.people
from ...services import friend_rank, checkins

Check warning on line 10 in app/routers/api/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-import

Unused checkins imported from services
from .deps import get_current_api_user

router = APIRouter(prefix="/api/v1/people", tags=["people"])
Expand Down Expand Up @@ -39,22 +39,20 @@
"relationship_label": p.relationship_label,
"birthday_month": p.birthday_month,
"birthday_day": p.birthday_day,
"birthday_year": p.birthday_year,
"how_we_met": p.how_we_met,
"met_date": p.met_date.isoformat() if p.met_date else None,
"location": p.location,
"phone": p.phone,
"email": p.email,
"notes": p.notes,
"occupation": p.occupation,

Check warning on line 49 in app/routers/api/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 7 lines is too similar to app/routers/api/export.py:31
"hobbies": p.hobbies,
"bio": p.bio,
"ai_summary": p.ai_summary,
"checkin_cadence_days": p.checkin_cadence_days,
"last_contact_date": p.last_contact_date.isoformat() if p.last_contact_date else None,
"relationship_state": p.relationship_state.value if p.relationship_state else "none",
"instagram_username": p.instagram_username,
"instagram_enabled": p.instagram_enabled,
"archived": p.archived,
"tags": [t.name for t in p.tags],
"friend_rank": rank.get("score", 0),
Expand Down Expand Up @@ -98,8 +96,6 @@
hobbies=body.hobbies,
bio=body.bio,
checkin_cadence_days=_safe_int(body.checkin_cadence_days),
instagram_username=body.instagram_username,
instagram_enabled=body.instagram_enabled,
archived=body.archived,
)
db.add(p)
Expand Down Expand Up @@ -150,7 +146,7 @@


@router.get("/{person_id}/journal")
def get_person_journal(person_id: int, db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 149 in app/routers/api/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
p = db.get(Person, person_id)
if not p:
raise HTTPException(status_code=404, detail="Person not found")
Expand All @@ -167,21 +163,21 @@


@router.get("/{person_id}/conflicts")
def get_person_conflicts(person_id: int, db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 166 in app/routers/api/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
p = db.get(Person, person_id)
if not p:
raise HTTPException(status_code=404, detail="Person not found")
return [{
"id": c.id, "summary": c.summary, "status": c.status.value,
"resolution_notes": c.resolution_notes,
"resolved_at": c.resolved_at.isoformat() if c.resolved_at else None,
"created_at": c.created_at.isoformat() if c.created_at else None,
"person_name": c.person.name if c.person else None,
} for c in p.conflict_logs]


Check warning on line 178 in app/routers/api/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 7 lines is too similar to app/routers/api/conflicts.py:29
@router.get("/{person_id}/notable-dates")
def get_person_notable_dates(person_id: int, db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 180 in app/routers/api/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
p = db.get(Person, person_id)
if not p:
raise HTTPException(status_code=404, detail="Person not found")
Expand Down
17 changes: 0 additions & 17 deletions app/routers/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from sqlalchemy.orm import Session

from ...database import get_db
from ...models import User

Check warning on line 7 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-import

Unused User imported from models
from ...settings_store import get_all_settings, set_many, get_setting_sensitive

Check warning on line 8 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-import

Unused get_setting_sensitive imported from settings_store
from ...auth import verify_password
from ...services.mfa import generate_totp_secret, verify_totp, generate_recovery_codes, decrypt_secret
from ...config import settings
Expand All @@ -15,36 +15,31 @@


class GeneralSettingsUpdate(BaseModel):
birthday_lead_days: str | None = None

Check failure on line 18 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
checkin_default_cadence_days: str | None = None

Check failure on line 19 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
daily_job_hour: str | None = None

Check failure on line 20 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
conflict_plan_idle_minutes: str | None = None

Check failure on line 21 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
chat_retention_days: str | None = None

Check failure on line 22 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |


class NotificationSettingsUpdate(BaseModel):
push_enabled: str | None = None

Check failure on line 26 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
push_birthdays: str | None = None

Check failure on line 27 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
push_cadence: str | None = None

Check failure on line 28 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |


class ImmichSettingsUpdate(BaseModel):
immich_url: str | None = None

Check failure on line 32 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
immich_api_key: str | None = None

Check failure on line 33 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |


class AiSettingsUpdate(BaseModel):
ai_base_url: str | None = None

Check failure on line 37 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
ai_api_key: str | None = None

Check failure on line 38 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
ai_model: str | None = None

Check failure on line 39 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
support_chat_model: str | None = None

Check failure on line 40 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |


class InstagramSettingsUpdate(BaseModel):
instagram_username: str | None = None
instagram_password: str | None = None


class MfaDisableRequest(BaseModel):
password: str

Expand All @@ -58,13 +53,13 @@


@router.get("")
def get_settings(db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 56 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
cfg = get_all_settings(db)
return cfg


@router.put("/general")
def update_general(body: GeneralSettingsUpdate, db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 62 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
updates = {k: v for k, v in body.model_dump(exclude_unset=True).items() if v is not None}
if updates:
set_many(db, updates)
Expand All @@ -72,7 +67,7 @@


@router.put("/notifications")
def update_notifications(body: NotificationSettingsUpdate, db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 70 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
updates = {k: v for k, v in body.model_dump(exclude_unset=True).items() if v is not None}
if updates:
set_many(db, updates)
Expand All @@ -80,7 +75,7 @@


@router.put("/immich")
def update_immich(body: ImmichSettingsUpdate, db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 78 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
updates = {}
if body.immich_url is not None:
updates["immich_url"] = body.immich_url.strip()
Expand All @@ -92,7 +87,7 @@


@router.put("/ai")
def update_ai(body: AiSettingsUpdate, db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 90 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
updates = {}
if body.ai_base_url is not None:
updates["ai_base_url"] = body.ai_base_url.strip()
Expand All @@ -107,18 +102,6 @@
return get_all_settings(db)


@router.put("/instagram")
def update_instagram(body: InstagramSettingsUpdate, db: Session = Depends(get_db), user=Depends(get_current_api_user)):
updates = {}
if body.instagram_username is not None:
updates["instagram_username"] = body.instagram_username.strip()
if body.instagram_password:
updates["instagram_password"] = body.instagram_password
if updates:
set_many(db, updates)
return get_all_settings(db)


# --- MFA ---

@router.get("/mfa/setup")
Expand All @@ -126,11 +109,11 @@
if user.totp_enabled:
raise HTTPException(status_code=400, detail="MFA already enabled")
if user.totp_secret is None:
encrypted, uri = generate_totp_secret(settings.APP_NAME)

Check warning on line 112 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-variable

Unused variable 'uri'
user.totp_secret = encrypted
db.commit()
secret = decrypt_secret(user.totp_secret)
import base64, io, qrcode

Check warning on line 116 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

import-outside-toplevel

Import outside toplevel (base64, io, qrcode)

Check warning on line 116 in app/routers/api/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

multiple-imports

Multiple imports on one line (base64, io, qrcode)
uri_to_use = f"otpauth://totp/{settings.APP_NAME}:kin-user?secret={secret}&issuer={settings.APP_NAME}"
qr = qrcode.make(uri_to_use)
buf = io.BytesIO()
Expand Down
17 changes: 5 additions & 12 deletions app/routers/api/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from sqlalchemy.orm import Session

from ...database import get_db
from ...models import InstagramPost, BirthdayMessageDraft, ReviewStatus
from ...models import BirthdayMessageDraft, ReviewStatus
from ...services import gamification
from .deps import get_current_api_user

Expand All @@ -11,28 +11,21 @@


@router.get("/reviews")
def get_reviews(db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 14 in app/routers/api/stats.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
ig = db.query(InstagramPost).filter_by(status=ReviewStatus.pending).order_by(
InstagramPost.posted_at.desc()).all()
bd = db.query(BirthdayMessageDraft).filter_by(status=ReviewStatus.pending).order_by(
BirthdayMessageDraft.created_at.desc()).all()
BirthdayMessageDraft.generated_at.desc()).all()
return {
"instagram_posts": [
{"id": p.id, "person_name": p.person.name if p.person else "", "caption": p.caption,
"media_url": p.media_url, "permalink": p.permalink, "post_type": p.post_type,
"posted_at": p.posted_at.isoformat() if p.posted_at else None}
for p in ig
],
"birthday_drafts": [
{"id": d.id, "person_name": d.person.name if d.person else "", "message": d.message,
"created_at": d.created_at.isoformat() if d.created_at else None}
{"id": d.id, "person_name": d.person.name if d.person else "",
"message": d.draft_text,
"created_at": d.generated_at.isoformat() if d.generated_at else None}
for d in bd
],
}


@router.get("/gamification")
def get_gamification(db: Session = Depends(get_db), user=Depends(get_current_api_user)):

Check warning on line 28 in app/routers/api/stats.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
data = gamification.get_stats_and_achievements(db)
return {
"xp": data["stats"].total_xp,
Expand Down
8 changes: 0 additions & 8 deletions app/routers/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,31 +26,29 @@
def export_json(db: Session = Depends(get_db), user=Depends(current_user)):
people = db.query(Person).order_by(Person.name).all()
data = {
"format": "kin-export",
"version": 2,
"exported_at": dt.datetime.utcnow().isoformat(),
"exported_people": [],
}
for p in people:
data["exported_people"].append({
"name": p.name, "nickname": p.nickname, "pronouns": p.pronouns,
"relationship_label": p.relationship_label,
"birthday": f"{p.birthday_month}/{p.birthday_day}/{p.birthday_year or ''}" if p.birthday_month else None,
"how_we_met": p.how_we_met,
"met_date": p.met_date.isoformat() if p.met_date else None,
"location": p.location, "phone": p.phone, "email": p.email, "notes": p.notes,
"occupation": p.occupation, "hobbies": p.hobbies,
"bio": p.bio,
"ai_summary": p.ai_summary,
"ai_starters_json": p.ai_starters_json,
"avatar_url": p.avatar_url,

Check warning on line 46 in app/routers/export.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 17 lines is too similar to app/routers/api/export.py:22
"archived": p.archived,
"checkin_cadence_days": p.checkin_cadence_days,
"last_contact_date": p.last_contact_date.isoformat() if p.last_contact_date else None,
"reminders_dismissed": p.reminders_dismissed,
"relationship_state": p.relationship_state.value if p.relationship_state else "none",
"instagram_username": p.instagram_username,
"instagram_enabled": p.instagram_enabled,
"tags": [t.name for t in p.tags],
"notable_dates": [{"label": nd.label, "month": nd.month, "day": nd.day, "year": nd.year,
"recurring": nd.recurring, "notes": nd.notes} for nd in p.notable_dates],
Expand All @@ -70,12 +68,6 @@
"created_at": cm.created_at.isoformat() if cm.created_at else None,
} for cm in (c.chat_messages or [])],
} for c in p.conflict_logs],
"instagram_posts": [{
"ig_post_id": ip.ig_post_id, "caption": ip.caption,
"media_url": ip.media_url, "permalink": ip.permalink,
"post_type": ip.post_type, "posted_at": ip.posted_at.isoformat() if ip.posted_at else None,
"status": ip.status.value,
} for ip in p.instagram_posts],
"journal_entries": [{
"date": e.entry_date.isoformat(), "title": e.title, "body": e.body,
"event_type": e.event_type.value if e.event_type else None,
Expand Down
5 changes: 1 addition & 4 deletions app/routers/people.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
router = APIRouter()


def _safe_int(value: str) -> int | None:

Check failure on line 19 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
try:
return int(value) if value else None
except (ValueError, TypeError):
return None


def _safe_date(value: str) -> dt.date | None:

Check failure on line 26 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
if not value:
return None
try:
Expand Down Expand Up @@ -67,7 +67,7 @@
else:
for t in p.tags:
circles_dict.setdefault(t.name, []).append(p)
circles = [(tag_name, tag_people) for tag_name, tag_people in circles_dict.items()]

Check warning on line 70 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unnecessary-comprehension

Unnecessary use of a comprehension, use list(circles_dict.items()) instead.
circles.sort(key=lambda t: t[0])
if untagged:
circles.append(("Uncircled", untagged))
Expand All @@ -87,19 +87,19 @@


@router.post("/people/new")
def people_create(

Check warning on line 90 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

too-many-arguments

Too many arguments (20/10)
request: Request, db: Session = Depends(get_db), user=Depends(current_user),
name: str = Form(...), nickname: str = Form(""), pronouns: str = Form(""),
relationship_label: str = Form(""), birthday_month: str = Form(""), birthday_day: str = Form(""),
birthday_year: str = Form(""), how_we_met: str = Form(""), met_date: str = Form(""),
location: str = Form(""), phone: str = Form(""), email: str = Form(""), notes: str = Form(""),
occupation: str = Form(""), hobbies: str = Form(""), bio: str = Form(""),
checkin_cadence_days: str = Form(""),
):
if not user:
return RedirectResponse("/login")
clean_name = name.strip()
person = Person(

Check warning on line 102 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 11 lines is too similar to app/routers/people.py:170
name=clean_name, nickname=(nickname.strip() or clean_name), pronouns=pronouns or None,
relationship_label=relationship_label or None,
birthday_month=_safe_int(birthday_month),
Expand All @@ -119,14 +119,14 @@


@router.get("/people/{person_id}")
def person_detail(person_id: int, request: Request, db: Session = Depends(get_db), user=Depends(current_user)):
if not user:
return RedirectResponse("/login")
person = db.get(Person, person_id)
if not person:
return RedirectResponse("/people")
entries = person.journal_entries
rank = friend_rank.compute_friend_rank(person)

Check warning on line 129 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 7 lines is too similar to app/routers/people.py:158
watermeter = checkins.compute_cadence_watermeter(person)
open_conflicts = [c for c in person.conflict_logs if c.status == ConflictStatus.unresolved]
today = dt.date.today()
Expand Down Expand Up @@ -155,31 +155,30 @@


@router.get("/people/{person_id}/edit")
def person_edit(person_id: int, request: Request, db: Session = Depends(get_db), user=Depends(current_user)):
if not user:
return RedirectResponse("/login")
person = db.get(Person, person_id)
if not person:
return RedirectResponse("/people")
return render(request, "person_form.html", db=db, user=user, active="people",
person=person, months=_month_names())

Check warning on line 165 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 7 lines is too similar to app/routers/people.py:122


@router.post("/people/{person_id}/edit")
def person_update(

Check warning on line 169 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

too-many-arguments

Too many arguments (21/10)
person_id: int, request: Request, db: Session = Depends(get_db), user=Depends(current_user),
name: str = Form(...), nickname: str = Form(""), pronouns: str = Form(""),
relationship_label: str = Form(""), birthday_month: str = Form(""), birthday_day: str = Form(""),
birthday_year: str = Form(""), how_we_met: str = Form(""), met_date: str = Form(""),
location: str = Form(""), phone: str = Form(""), email: str = Form(""), notes: str = Form(""),
occupation: str = Form(""), hobbies: str = Form(""), bio: str = Form(""),
checkin_cadence_days: str = Form(""), instagram_username: str = Form(""),
instagram_enabled: str = Form(""),
checkin_cadence_days: str = Form(""),
):
if not user:
return RedirectResponse("/login")
person = db.get(Person, person_id)
if not person:

Check warning on line 181 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 11 lines is too similar to app/routers/people.py:91
return RedirectResponse("/people")
clean_name = name.strip()
person.name = clean_name
Expand All @@ -199,15 +198,13 @@
person.hobbies = hobbies.strip() or None
person.bio = bio.strip() or None
person.checkin_cadence_days = _safe_int(checkin_cadence_days)
person.instagram_username = instagram_username.strip().lstrip("@") or None
person.instagram_enabled = bool(instagram_enabled)
db.commit()
gamification.award_and_flash(request, db, "PROFILE_UPDATED")
return RedirectResponse(f"/people/{person.id}", status_code=303)


@router.post("/people/{person_id}/archive")
def person_archive(person_id: int, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 207 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
person = db.get(Person, person_id)
if person:
person.archived = not person.archived
Expand All @@ -216,7 +213,7 @@


@router.post("/people/{person_id}/delete")
def person_delete(person_id: int, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 216 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
person = db.get(Person, person_id)
if person:
db.delete(person)
Expand All @@ -225,7 +222,7 @@


@router.post("/people/{person_id}/tags")
def add_tag(person_id: int, db: Session = Depends(get_db), user=Depends(current_user), tag_name: str = Form(...)):

Check warning on line 225 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
person = db.get(Person, person_id)
if not person:
return RedirectResponse("/people")
Expand All @@ -243,7 +240,7 @@


@router.post("/people/{person_id}/tags/{tag_id}/remove")
def remove_tag(person_id: int, tag_id: int, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 243 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
person = db.get(Person, person_id)
tag = db.get(Tag, tag_id)
if person and tag and tag in person.tags:
Expand All @@ -254,7 +251,7 @@

@router.post("/people/{person_id}/notable-dates")
def add_notable_date(
person_id: int, db: Session = Depends(get_db), user=Depends(current_user),

Check warning on line 254 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
label: str = Form(...), month: int = Form(...), day: int = Form(...), year: str = Form(""),
):
nd = NotableDate(person_id=person_id, label=label.strip(), month=month, day=day,
Expand All @@ -265,7 +262,7 @@


@router.post("/notable-dates/{nd_id}/delete")
def delete_notable_date(nd_id: int, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 265 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
nd = db.get(NotableDate, nd_id)
if nd:
person_id = nd.person_id
Expand All @@ -276,7 +273,7 @@


@router.post("/people/{person_id}/link-immich")
def link_immich(person_id: int, request: Request, db: Session = Depends(get_db), user=Depends(current_user),

Check warning on line 276 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
immich_person_id: str = Form(...)):
person = db.get(Person, person_id)
if person:
Expand All @@ -288,7 +285,7 @@


@router.post("/people/{person_id}/unlink-immich")
def unlink_immich(person_id: int, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 288 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
person = db.get(Person, person_id)
if person:
person.immich_person_id = None
Expand All @@ -298,7 +295,7 @@


@router.post("/people/{person_id}/scratchpad")
def add_scratchpad_item(person_id: int, request: Request, db: Session = Depends(get_db), user=Depends(current_user),

Check warning on line 298 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
text: str = Form(...)):
text = text.strip()
if text:
Expand All @@ -309,7 +306,7 @@


@router.post("/scratchpad/{item_id}/delete")
def delete_scratchpad_item(item_id: int, request: Request, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 309 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
item = db.get(ScratchpadItem, item_id)
if item:
person_id = item.person_id
Expand All @@ -321,7 +318,7 @@


@router.post("/people/{person_id}/notable-people")
def add_notable_person(person_id: int, request: Request, db: Session = Depends(get_db), user=Depends(current_user),

Check warning on line 321 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
name: str = Form(...), relation: str = Form("")):
name = name.strip()
if name:
Expand All @@ -332,7 +329,7 @@


@router.post("/notable-people/{ref_id}/delete")
def delete_notable_person(ref_id: int, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 332 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
ref = db.get(NotablePersonRef, ref_id)
if ref:
person_id = ref.person_id
Expand All @@ -346,7 +343,7 @@
def set_relationship_state(person_id: int, request: Request, db: Session = Depends(get_db),
user=Depends(current_user), state: str = Form("")):
person = db.get(Person, person_id)
if person and state in ("none", "wants_space", "drifted"):

Check warning on line 346 in app/routers/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

use-set-for-membership

Consider using set for membership test
person.relationship_state = RelationshipState(state)
db.commit()
labels = {"none": "cleared", "wants_space": "set to 'wants space'", "drifted": "marked as drifted"}
Expand Down
40 changes: 2 additions & 38 deletions app/routers/reviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
from ..database import get_db
from ..deps import current_user
from ..models import (
InstagramPost, BirthdayMessageDraft, GiftIdea, GiftStatus, ReviewStatus,
JournalEntry, JournalImage, EventType,
BirthdayMessageDraft, GiftIdea, GiftStatus, ReviewStatus,
)
from ..render import render
from ..services import birthdays as bday_service
from ..services import instagram_poll
from ..services.ai_client import get_client_from_settings, build_person_context, AIError

router = APIRouter()
Expand All @@ -22,7 +20,6 @@
def reviews_page(request: Request, db: Session = Depends(get_db), user=Depends(current_user)):
if not user:
return RedirectResponse("/login")
ig_posts = db.query(InstagramPost).filter_by(status=ReviewStatus.pending).order_by(InstagramPost.posted_at.desc()).all()
bday_drafts = db.query(BirthdayMessageDraft).filter_by(status=ReviewStatus.pending).all()
approved_bday = db.query(BirthdayMessageDraft).filter_by(status=ReviewStatus.approved).all()

Expand All @@ -32,46 +29,13 @@
gifts_by_key = {(g.person_id, g.year): g for g in gift_ideas}

return render(request, "reviews.html", db=db, user=user, active="reviews",
ig_posts=ig_posts, bday_drafts=bday_drafts, approved_bday=approved_bday,
bday_drafts=bday_drafts, approved_bday=approved_bday,
gifts_by_key=gifts_by_key)


@router.post("/reviews/run-now")
def run_now(request: Request, db: Session = Depends(get_db), user=Depends(current_user)):
bday_service.generate_birthday_drafts(db)
instagram_poll.poll_all(db)
return RedirectResponse("/reviews", status_code=303)


@router.post("/reviews/instagram/{post_id}/approve")
def approve_instagram(post_id: int, db: Session = Depends(get_db), user=Depends(current_user)):
post = db.get(InstagramPost, post_id)
if post:
entry = JournalEntry(
author_user_id=user.id if user else None,
title=f"Instagram post from @{post.person.instagram_username}",
body=post.caption or "(no caption)",
entry_date=post.posted_at.date() if post.posted_at else dt.date.today(),
event_type=EventType.instagram,
source="instagram",
)
entry.people.append(post.person)
db.add(entry)
db.flush()
if post.media_url:
db.add(JournalImage(journal_entry_id=entry.id, upload_path=post.media_url, caption="From Instagram"))
post.status = ReviewStatus.approved
post.imported_as_journal_entry_id = entry.id
db.commit()
return RedirectResponse("/reviews", status_code=303)


@router.post("/reviews/instagram/{post_id}/dismiss")
def dismiss_instagram(post_id: int, db: Session = Depends(get_db), user=Depends(current_user)):
post = db.get(InstagramPost, post_id)
if post:
post.status = ReviewStatus.dismissed
db.commit()
return RedirectResponse("/reviews", status_code=303)


Expand All @@ -97,7 +61,7 @@


@router.post("/reviews/birthday/{draft_id}/dismiss")
def dismiss_birthday(draft_id: int, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 64 in app/routers/reviews.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
draft = db.get(BirthdayMessageDraft, draft_id)
if draft:
draft.status = ReviewStatus.skipped
Expand All @@ -106,7 +70,7 @@


@router.post("/reviews/birthday/{draft_id}/regenerate")
def regenerate_birthday(draft_id: int, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 73 in app/routers/reviews.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
draft = db.get(BirthdayMessageDraft, draft_id)
if draft:
try:
Expand All @@ -123,7 +87,7 @@


@router.post("/reviews/gift/{gift_id}/given")
def mark_gift_given(gift_id: int, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 90 in app/routers/reviews.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
gift = db.get(GiftIdea, gift_id)
if gift:
gift.status = GiftStatus.given
Expand All @@ -132,7 +96,7 @@


@router.post("/reviews/gift/{gift_id}/dismiss")
def dismiss_gift(gift_id: int, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 99 in app/routers/reviews.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
gift = db.get(GiftIdea, gift_id)
if gift:
gift.status = GiftStatus.dismissed
Expand All @@ -141,7 +105,7 @@


@router.post("/reviews/gift/{gift_id}/regenerate")
def regenerate_gift(gift_id: int, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 108 in app/routers/reviews.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
gift = db.get(GiftIdea, gift_id)
if gift:
try:
Expand Down
10 changes: 0 additions & 10 deletions app/routers/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@


@router.post("/settings/immich")
def save_immich(request: Request, db: Session = Depends(get_db), user=Depends(current_user),

Check warning on line 37 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'request'

Check warning on line 37 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
immich_url: str = Form(""), immich_api_key: str = Form("")):
set_many(db, {"immich_url": immich_url.strip(), "immich_api_key": immich_api_key.strip()})
return RedirectResponse("/settings", status_code=303)
Expand All @@ -48,7 +48,7 @@
client = ImmichClient(cfg["immich_url"], get_setting_sensitive(db, "immich_api_key"))
client.test_connection()
result = ("success", "Connected to Immich successfully.")
except ImmichError as e:

Check warning on line 51 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-variable

Unused variable 'e'
result = ("danger", "Could not connect to Immich. Check your URL and API key.")
users = db.query(User).order_by(User.id).all()
return render(request, "settings.html", db=db, user=user, active="settings", cfg=cfg, users=users,
Expand All @@ -56,7 +56,7 @@


@router.post("/settings/ai")
def save_ai(request: Request, db: Session = Depends(get_db), user=Depends(current_user),

Check warning on line 59 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'request'

Check warning on line 59 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
ai_base_url: str = Form(...), ai_api_key: str = Form(""), ai_model: str = Form(...),
support_chat_model: str = Form("gpt-4o")):
set_many(db, {
Expand All @@ -76,7 +76,7 @@
client = AIClient(cfg["ai_base_url"], get_setting_sensitive(db, "ai_api_key"), cfg["ai_model"])
reply = client.test_connection()
result = ("success", f"AI responded: {reply}")
except AIError as e:

Check warning on line 79 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-variable

Unused variable 'e'
result = ("danger", "AI connection failed. Check your credentials and try again.")
users = db.query(User).order_by(User.id).all()
return render(request, "settings.html", db=db, user=user, active="settings", cfg=cfg, users=users,
Expand All @@ -84,7 +84,7 @@


@router.post("/settings/whisper")
def save_whisper(request: Request, db: Session = Depends(get_db), user=Depends(current_user),

Check warning on line 87 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'request'

Check warning on line 87 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
whisper_provider: str = Form("openai"), whisper_base_url: str = Form(""),
whisper_api_key: str = Form(""), whisper_model: str = Form("whisper-1")):
provider = (whisper_provider or "openai").strip() or "openai"
Expand All @@ -103,12 +103,12 @@


@router.post("/settings/whisper/test")
def test_whisper(request: Request, db: Session = Depends(get_db), user=Depends(current_user)):
cfg = get_all_settings(db)
users = db.query(User).order_by(User.id).all()
result = None
try:
import httpx

Check warning on line 111 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

import-outside-toplevel

Import outside toplevel (httpx)
provider = (cfg.get("whisper_provider") or "openai").lower()

Check warning on line 112 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 6 lines is too similar to app/routers/settings.py:165
base = (cfg.get("whisper_base_url") or cfg.get("ai_base_url") or "").strip()
if not base:
Expand All @@ -132,14 +132,14 @@
result = ("success", "OpenAI-compatible endpoint reachable.")
else:
raise RuntimeError("Endpoint returned an error")
except Exception:

Check warning on line 135 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

broad-exception-caught

Catching too general exception Exception
result = ("danger", "Whisper endpoint not reachable. Check URL and container.")
return render(request, "settings.html", db=db, user=user, active="settings", cfg=cfg, users=users,
whisper_test=result)


@router.post("/settings/tts")
def save_tts(request: Request, db: Session = Depends(get_db), user=Depends(current_user),

Check warning on line 142 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

too-many-arguments

Too many arguments (13/10)

Check warning on line 142 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'request'

Check warning on line 142 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
tts_provider: str = Form("piper"), tts_base_url: str = Form(""), tts_api_key: str = Form(""),
tts_voice: str = Form("en_GB-alba-medium"), tts_lang: str = Form("en-GB"),
tts_format: str = Form("mp3"), tts_piper_host: str = Form(""), tts_piper_port: str = Form("10200"),
Expand All @@ -162,13 +162,13 @@


@router.post("/settings/tts/test")
def test_tts(request: Request, db: Session = Depends(get_db), user=Depends(current_user)):
cfg = get_all_settings(db)
users = db.query(User).order_by(User.id).all()
result = None
try:
# Connection test only: for OpenAI, try a base URL GET; for Piper, try Wyoming TCP or web UI status
import httpx, socket

Check warning on line 171 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 6 lines is too similar to app/routers/settings.py:106

Check warning on line 171 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

import-outside-toplevel

Import outside toplevel (httpx, socket)

Check warning on line 171 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

multiple-imports

Multiple imports on one line (httpx, socket)
prov = (cfg.get("tts_provider") or "piper").lower()
if prov == "openai":
base = (cfg.get("tts_base_url") or "https://api.openai.com/v1").strip()
Expand All @@ -191,7 +191,7 @@
ok = True
finally:
try: s.close()
except Exception: pass

Check warning on line 194 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

broad-exception-caught

Catching too general exception Exception
if not ok:
base = (cfg.get("tts_base_url") or "").strip()
if not base:
Expand All @@ -200,19 +200,19 @@
r = client.get(base.rstrip("/") + "/api/status")
ok = (r.status_code < 400)
result = ("success", "Piper reachable.") if ok else ("danger", "Piper not reachable.")
except Exception:

Check warning on line 203 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

broad-exception-caught

Catching too general exception Exception
result = ("danger", "Connection test failed. Check provider settings.")
return render(request, "settings.html", db=db, user=user, active="settings", cfg=cfg, users=users,
tts_test=result)


@router.get("/settings/tts/voices")
def list_tts_voices(request: Request, db: Session = Depends(get_db), user=Depends(current_user)):

Check warning on line 210 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'request'

Check warning on line 210 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-argument

Unused argument 'user'
cfg = get_all_settings(db)
prov = (cfg.get("tts_provider") or "piper").lower()
voices: list[str] = []
try:
import httpx

Check warning on line 215 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

import-outside-toplevel

Import outside toplevel (httpx)
if prov == "piper":
base = (cfg.get("tts_base_url") or "").strip()
if not base:
Expand All @@ -233,7 +233,7 @@
# OpenAI: suggest a small set; cannot enumerate via API
voices = ["alloy", "verse", "aria", "sage"]
return JSONResponse({"voices": voices})
except Exception as e:

Check warning on line 236 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

broad-exception-caught

Catching too general exception Exception

Check warning on line 236 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-variable

Unused variable 'e'
return JSONResponse({"voices": [], "error": "Failed to fetch voices."}, status_code=400)


Expand All @@ -252,16 +252,6 @@
return JSONResponse({"error": "Sample failed"}, status_code=400)


@router.post("/settings/instagram")
def save_instagram(request: Request, db: Session = Depends(get_db), user=Depends(current_user),
instagram_username: str = Form(""), instagram_password: str = Form("")):
values = {"instagram_username": instagram_username.strip()}
if instagram_password:
values["instagram_password"] = instagram_password
set_many(db, values)
return RedirectResponse("/settings", status_code=303)


@router.post("/settings/calendar")
def save_calendar(request: Request, db: Session = Depends(get_db), user=Depends(current_user),
calendar_ics_enabled: str = Form("0"),
Expand Down Expand Up @@ -363,13 +353,13 @@
import io
import qrcode
uri_to_use = uri or f"otpauth://totp/{settings.APP_NAME}:kin-user?secret={secret}&issuer={settings.APP_NAME}"
qr = qrcode.make(uri_to_use)
buf = io.BytesIO()
qr.save(buf, format="PNG")
qr_b64 = base64.b64encode(buf.getvalue()).decode()
return render(request, "mfa_setup.html", db=db, user=user, active="settings",
qr_b64=qr_b64, totp_key=secret, mfa_setup_done=False)

Check warning on line 362 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 6 lines is too similar to app/routers/settings.py:377

@router.post("/settings/mfa/setup")
def mfa_setup_post(request: Request, db: Session = Depends(get_db), user=Depends(current_user),
Expand All @@ -384,13 +374,13 @@
import io
import qrcode
uri = f"otpauth://totp/Kin:kin-user?secret={secret}&issuer=Kin"
qr = qrcode.make(uri)
buf = io.BytesIO()
qr.save(buf, format="PNG")
qr_b64 = base64.b64encode(buf.getvalue()).decode()
return render(request, "mfa_setup.html", db=db, user=user, active="settings",
qr_b64=qr_b64, totp_key=secret, mfa_setup_done=False,
error="That code didn't work. Please try again.")

Check warning on line 383 in app/routers/settings.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 6 lines is too similar to app/routers/settings.py:356
user.totp_enabled = True
plain_codes, hashed_json = generate_recovery_codes()
user.mfa_recovery_codes = hashed_json
Expand Down
4 changes: 0 additions & 4 deletions app/schemas/people.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from pydantic import BaseModel, Field

Check warning on line 1 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-import

Unused Field imported from pydantic
from datetime import date

Check warning on line 2 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

wrong-import-order

standard import "from datetime import date" should be placed before "from pydantic import BaseModel, Field"

Check warning on line 2 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unused-import

Unused date imported from datetime


class PersonCreate(BaseModel):
name: str
nickname: str | None = None

Check failure on line 7 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
pronouns: str | None = None

Check failure on line 8 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
relationship_label: str | None = None

Check failure on line 9 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
birthday_month: int | None = None

Check failure on line 10 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
birthday_day: int | None = None

Check failure on line 11 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
birthday_year: int | None = None

Check failure on line 12 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
how_we_met: str | None = None
met_date: str | None = None
location: str | None = None
Expand All @@ -20,8 +20,6 @@
hobbies: str | None = None
bio: str | None = None
checkin_cadence_days: int | None = None
instagram_username: str | None = None
instagram_enabled: bool = False
archived: bool = False

Check warning on line 23 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 17 lines is too similar to app/schemas/people.py:32


Expand All @@ -31,31 +29,29 @@

class PersonResponse(BaseModel):
id: int
name: str
nickname: str | None = None
pronouns: str | None = None
relationship_label: str | None = None
birthday_month: int | None = None
birthday_day: int | None = None
birthday_year: int | None = None
how_we_met: str | None = None
met_date: str | None = None

Check failure on line 40 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
location: str | None = None

Check failure on line 41 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
phone: str | None = None

Check failure on line 42 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
email: str | None = None

Check failure on line 43 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
notes: str | None = None

Check failure on line 44 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
occupation: str | None = None

Check failure on line 45 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
hobbies: str | None = None

Check failure on line 46 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
bio: str | None = None

Check failure on line 47 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
ai_summary: str | None = None

Check failure on line 48 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
checkin_cadence_days: int | None = None

Check warning on line 49 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

CodeDuplication

This block of 17 lines is too similar to app/schemas/people.py:6

Check failure on line 49 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
last_contact_date: str | None = None

Check failure on line 50 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
relationship_state: str | None = None

Check failure on line 51 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |
instagram_username: str | None = None
instagram_enabled: bool = False
archived: bool = False
tags: list[str] = []
friend_rank: int | None = None

Check failure on line 54 in app/schemas/people.py

View check run for this annotation

Codeac.io / Codeac Code Quality

unsupported-binary-operation

unsupported operand type(s) for |


class TagResponse(BaseModel):
Expand Down
2 changes: 1 addition & 1 deletion app/services/friend_rank.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def compute_friend_rank(person) -> dict:
(bool(person.how_we_met), "how you met"),
(bool(person.occupation), "their occupation"),
(bool(person.hobbies), "their hobbies/interests"),
(bool(person.email or person.phone or person.instagram_username), "contact info"),
(bool(person.email or person.phone), "contact info"),
(bool(person.notable_dates), "a notable date (anniversary, etc.)"),
(bool(person.notable_people_refs), "notable people in their life"),
(bool(person.immich_person_id), "a linked photo"),
Expand Down
2 changes: 1 addition & 1 deletion app/services/gamification.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def _locked(slug: str) -> bool:
_unlock("details_matter")
if _locked("well_connected"):
has_contact = db.query(Person).filter(
(Person.email.isnot(None)) | (Person.phone.isnot(None)) | (Person.instagram_username.isnot(None))
(Person.email.isnot(None)) | (Person.phone.isnot(None))
).count() >= 1
if has_contact:
_unlock("well_connected")
Expand Down
Loading
Loading