# AGENT.md

This file is a restart brief for future agent sessions on this repository.

## Project Identity

This repository supports a legacy, business-critical Redmine 3.4.4
installation. The real purpose is to make CRM/helpdesk communication data
searchable and automatable without a risky near-term platform migration.

The LAN copy used for testing is:

```text
http://192.168.50.170/
```

This is not a greenfield app. Treat it as a local-fork-and-integration project
around an existing Redmine install and old RedmineUP plugins.

Tracked local plugin source lives in `plugins/`. The full `redmine-copy/` tree
is ignored and should be treated as a working/reference copy, not the source of
truth for local plugin changes.

## Original Motivation

The business stores most external communication inside Redmine through:

- `redmine_contacts`
- `redmine_contacts_helpdesk`

The default UI/API is not good enough for:

- efficient contact search
- light contact maintenance
- helpdesk message search
- customer communication timeline lookup
- future semantic/vector search

The key realization from prior work:

```text
Helpdesk-created issues may have Anonymous issue authors.
The real customer identity lives in helpdesk_tickets, journal_messages, and contacts.
```

Never design the search/index layer as if `issues.author` were enough.

## High-Level Architecture Decision

The intended architecture is:

1. Redmine writes local outbox rows.
2. External worker reads those rows.
3. Worker enriches from read-only MySQL joins.
4. Worker builds ticket/message documents.
5. External index stores/searches those documents.

Safety rule:

```text
External failures must not break Redmine saves.
```

That is why the event boundary is local DB outbox, not request-time webhooks or
embedding calls.

## Redmine And Plugin Baseline

- Redmine version: `3.4.4`
- Old RedmineUP plugin versions in use:
  - `redmine_contacts` 4.1.2 PRO
  - `redmine_contacts_helpdesk` 3.0.9 PRO
- Near-term upgrade to newer Redmine/RedmineUP is not the current goal.
- Treat these plugins as local legacy code when necessary.

## Repository Landmarks

Top-level docs:

- [README.md](/home/iadnah/redmine/README.md:1)
- [docs/event_outbox_spec.md](/home/iadnah/redmine/docs/event_outbox_spec.md:1)
- [docs/redmineup_local_fork_changelog.md](/home/iadnah/redmine/docs/redmineup_local_fork_changelog.md:1)
- [docs/pre_existing_issues.md](/home/iadnah/redmine/docs/pre_existing_issues.md:1)

Main scripts:

- [redmine_contacts.py](/home/iadnah/redmine/redmine_contacts.py:1)
- [redmine_helpdesk_search.py](/home/iadnah/redmine/redmine_helpdesk_search.py:1)
- [redmine_outbox_worker.py](/home/iadnah/redmine/redmine_outbox_worker.py:1)

Local Redmine copy:

- [redmine-copy](/home/iadnah/redmine/redmine-copy)

Important local plugin paths:

- [redmine-copy/plugins/redmine_event_outbox](/home/iadnah/redmine/redmine-copy/plugins/redmine_event_outbox)
- [redmine-copy/plugins/redmine_contacts_helpdesk](/home/iadnah/redmine/redmine-copy/plugins/redmine_contacts_helpdesk)

## What Has Already Been Done

### Contact CLI

`redmine_contacts.py` exists and supports:

- contact fetch to local cache
- fuzzy-ish contact search
- light contact updates
- helpdesk read API calls:
  - ticket by issue
  - issues by contact
  - messages by issue
  - contact timeline

### Event Outbox Plugin

`redmine_event_outbox` exists and the known-good archive is:

- `dist/redmine_event_outbox-0.0.1-known-good-20260421T143957Z.tar.gz`

Known-good tested LAN events:

- `issue.created`
- `issue.updated`
- `journal.created`
- `contact.created`
- `contact.updated`

Local code also adds optional helpdesk events:

- `helpdesk_ticket.created`
- `helpdesk_ticket.updated`
- `journal_message.created`
- `journal_message.updated`

These helpdesk event paths are implemented locally but should still be treated
as needing fuller end-to-end validation.

### Helpdesk Read API In Local Plugin Fork

The local `redmine_contacts_helpdesk` fork includes:

- `helpdesk_search_controller.rb`
- routes under `/helpdesk_search/*`
- alias/usage routes to avoid noisy routing errors

This was deployed to the LAN copy and route-loaded successfully.

### Helpdesk Export/Search CLI

`redmine_helpdesk_search.py` was created to prove the data model and export
path. It:

- SSHes to the LAN Redmine host
- reads remote MySQL credentials from `config/database.yml`
- runs read-only MySQL queries
- exports ticket/message docs to local JSONL cache
- provides rough local search/timeline/issues-by-contact commands

Important:

```text
This script is a diagnostic/export tool, not the final search architecture.
```

Do not get dragged into treating local CLI search speed as the core mission.

### External Outbox Worker Prototype

`redmine_outbox_worker.py` now exists as the first worker/indexer boundary.
It:

- reads and optionally claims pending `event_outbox_events` over SSH/MySQL
- supports `--dry-run` for non-mutating previews
- uses `locked_at`, `locked_by`, `processed_at`, `attempts`, and `last_error`
- enriches helpdesk ticket/message/contact events with read-only joins
- writes derived JSONL to `/tmp/redmine-outbox/derived_documents.jsonl`
- marks rows processed only after a successful local write

This is still a prototype output target, not the final vector index.

## LAN Host Facts

These were verified previously:

- SSH host/user: `reddev@192.168.50.170`
- SSH key used previously: `/tmp/reddev`
- remote Redmine path: `/usr/share/redmine`
- remote Ruby: `2.5.1`
- remote Bundler: `1.17.3`
- remote DB: MySQL 5.7
- remote plugin rollback archives were stored in:
  - `/home/reddev/redmine-plugin-backups/`

If using those exact credentials again, verify they still exist before relying
on them.

## Remote Plugin Changes Previously Deployed

Changes were copied to the LAN Redmine host for:

- `plugins/redmine_event_outbox/...`
- `plugins/redmine_contacts_helpdesk/...`

Passenger was restarted with:

```text
touch /usr/share/redmine/tmp/restart.txt
```

Routes were verified by remote `rake routes`.

## Fork Hygiene Rules

Before editing RedmineUP plugin code:

1. create a rollback archive in `dist/`
2. record the change in:
   - `docs/redmineup_local_fork_changelog.md`
   - plugin-local changelog if appropriate
3. keep edits scoped
4. prefer read-only APIs/endpoints first
5. validate on the LAN copy before claiming the change is done

Existing rollback archives:

- `dist/redmine_contacts-4.1.2-local-before-helpdesk-search-20260421T215548Z.tar.gz`
- `dist/redmine_contacts_helpdesk-3.0.9-local-before-helpdesk-search-20260421T215548Z.tar.gz`

## Important Pre-Existing Issues

Read:

- [docs/pre_existing_issues.md](/home/iadnah/redmine/docs/pre_existing_issues.md:1)

Especially remember:

1. duplicate `id="avatar"` is likely a real long-standing UI bug
2. `attachments/contacts_thumbnail` digest warning is probably log noise
3. `acts_as_list` warning is an upgrade-compatibility note, not a current
   blocker

Do not confuse those with the main search deliverable unless they directly block
the work in front of you.

## Current Strategic State

The project is past the "can we get data out?" phase.

We now know:

- contact JSON access exists
- helpdesk/customer identity can be extracted
- outbox events can be recorded safely
- helpdesk read API routes can be added locally
- full ticket/message export is feasible

The most useful next step is:

```text
Build the external worker/indexer pipeline.
```

Not:

- polishing a local CLI into a full search product
- building a big Redmine admin UI
- chasing unrelated legacy plugin cleanup

## Recommended Next Steps For A Future Session

1. Re-read:
   - `README.md`
   - `docs/event_outbox_spec.md`
   - `docs/redmineup_local_fork_changelog.md`
   - `docs/pre_existing_issues.md`
2. Verify local and remote state:
   - plugin files present
   - LAN host reachable
   - remote routes still load
3. Validate `redmine_outbox_worker.py` end to end against the LAN copy
4. Refine read-only joins that enrich:
   - `helpdesk_tickets`
   - `journal_messages`
   - `contacts`
   - `issues`
   - `journals`
5. Confirm deterministic ticket/message/contact JSONL output
6. Only after that, connect Qdrant and embeddings

## Query/Data Model Notes

Authoritative helpdesk context lives in:

- `helpdesk_tickets`
  - `issue_id`
  - `contact_id`
  - `from_address`
  - `to_address`
  - `cc_address`
  - `message_id`
  - `ticket_date`
  - `source`
- `journal_messages`
  - `journal_id`
  - `contact_id`
  - `from_address`
  - `to_address`
  - `cc_address`
  - `bcc_address`
  - `message_id`
  - `is_incoming`
  - `message_date`
- `contacts`
- `issues`
- `journals`

The worker/indexer should prefer direct DB joins as the source of truth for this
layer.

## What To Be Careful About

- Do not assume Redmine core issue API data is enough for helpdesk search.
- Do not treat old RedmineUP code as untouchable vendor code.
- Do not claim a plugin change is validated just because `ruby -c` passes.
- Do not burn time optimizing local CLI search if the real goal is external
  indexing.
- Do not revert unrelated repo changes; the Redmine tree may be dirty.

## If You Need A One-Paragraph Summary

This project exists to safely extract and search real CRM/helpdesk communication
history from a legacy Redmine 3.4.4 install. We already have a contact CLI, a
local event outbox plugin, local helpdesk read API routes, and a rough
helpdesk-export CLI. The next meaningful milestone is an external worker that
consumes outbox rows, enriches from helpdesk/contact MySQL tables, and builds
deterministic ticket/message documents for a future Qdrant/OpenAI-backed search
index.
