README.md

# Voile Locker & Luggage Plugin

A visitor locker management plugin for [Voile](https://github.com/curatorian/voile), the open-source GLAM library management system.

Provides per-node locker assignment during visitor check-in, staff management UI, session history, and auto-expiry.

## Features

- Per-node locker enable/disable toggle
- Configurable locker count per node
- Auto-assign available locker during visitor check-in
- Manual locker assignment and release by staff
- Locker statuses: `available`, `occupied`, `maintenance`, `reserved`
- Session history with timestamps and release tracking
- Admin UI at `/manage/plugins/locker_luggage/`

---

## Requirements

| Requirement      | Version    |
| ---------------- | ---------- |
| Elixir           | `~> 1.18`  |
| OTP              | `~> 27`    |
| Phoenix LiveView | `~> 1.1`   |
| Voile            | `>= 0.1.2` |
| PostgreSQL       | `>= 14`    |

---

## Installation

### Option A — From Hex _(coming soon)_

```elixir
# mix.exs
{:voile_locker_luggage, "~> 0.1.0"}
```

### Option B — From GitHub (recommended for now)

You can pin to a specific release tag:

```elixir
# mix.exs
{:voile_locker_luggage,
  github: "curatorian/voile_locker_luggage",
  tag: "v0.1.0",
  sparse: "."
}
```

Or track the latest commit on main (not recommended for production):

```elixir
{:voile_locker_luggage,
  github: "curatorian/voile_locker_luggage",
  branch: "main",
  sparse: "."
}
```

### Option C — From a local path (development / self-hosted)

If you have cloned this repository alongside your Voile installation:

```elixir
# mix.exs — inside the host Voile app
{:voile_locker_luggage, path: "plugins/voile_locker_luggage"}
```

This is the recommended approach if you are running Voile from source in a
monorepo-style setup (all plugins live under `plugins/` inside the Voile repo).

---

## Setup Steps

After adding the dependency, follow these steps inside your **Voile host app**:

### 1. Fetch dependencies

```bash
mix deps.get
```

### 2. Compile

```bash
mix compile
```

The plugin is a separate OTP application and will be compiled alongside Voile.

### 3. Install the plugin from the admin UI

Start Voile and navigate to:

```
/manage/plugins
```

Click **"Install"** next to _Locker & Luggage_. This runs the plugin's database
migrations and registers it in `voile_plugins`.

Alternatively, install via IEx:

```elixir
Voile.PluginManager.install("Elixir.VoileLockerLuggage")
```

### 4. Activate the plugin

After installing, click **"Activate"** in the admin UI, or via IEx:

```elixir
Voile.PluginManager.activate("Elixir.VoileLockerLuggage")
```

Activation registers the check-in hook so the plugin participates in visitor flows.

### 5. Configure per-node

Navigate to `/manage/plugins/locker_luggage/nodes` and enable the locker system for
each node (library branch / service desk) that has physical lockers.

### 6. Configure plugin settings

Navigate to `/manage/plugins/locker_luggage/settings` to configure:

| Setting                       | Default | Description                                                |
| ----------------------------- | ------- | ---------------------------------------------------------- |
| Allow Self-Release            | `true`  | Visitors can release their own locker at check-out         |
| Auto-Expire After (hours)     | `24`    | Expire sessions automatically after N hours (0 = disabled) |
| Show Locker Number on Receipt | `true`  | Print locker number on check-in receipt                    |
| Notify Staff on Expiry        | `false` | Dashboard notification when a session expires              |

---

## Database Migrations

Migrations are applied automatically when you **Install** the plugin from the
admin UI. They run against the same PostgreSQL database as the Voile host app.

Tables created by this plugin are prefixed with `plugin_locker_luggage_`:

- `plugin_locker_luggage_lockers` — individual locker records per node
- `plugin_locker_luggage_sessions` — assignment history (active + closed)
- `plugin_locker_luggage_node_configs` — per-node enable/disable and locker count

To roll back all migrations (performed when you **Uninstall** the plugin):

```elixir
Voile.PluginManager.uninstall("Elixir.VoileLockerLuggage")
```

> **Warning:** Uninstalling drops all plugin tables and their data permanently.

---

## Production Installation

### Using `mix release`

Elixir releases bake in all compiled code and dependencies at build time.
Because the plugin is a dependency in `mix.exs`, it is automatically included
in the release artifact.

**Build steps:**

```bash
# 1. Add the dep to mix.exs (see Installation above), then:
mix deps.get --only prod
MIX_ENV=prod mix compile

# 2. Build the release
MIX_ENV=prod mix release

# 3. Deploy the release artifact to the server
# (copy _build/prod/rel/voile/ to the target machine)

# 4. On the server, start the app
./bin/voile start

# 5. Install and activate the plugin (one-time, via remote IEx)
./bin/voile remote
iex> Voile.PluginManager.install("Elixir.VoileLockerLuggage")
iex> Voile.PluginManager.activate("Elixir.VoileLockerLuggage")
```

From the second deployment onward, the plugin is already registered in the
database — just redeploying the release is enough. If there are new migrations,
call `on_update/2` or re-run install from the admin UI.

### Using Podman / Docker (container-based)

The plugin must be included in the container image at **build time** — there is
no dynamic plugin loading at container runtime. The image acts as the release
artifact.

**Example `Containerfile` / `Dockerfile` additions:**

```dockerfile
# If using Option C (local path dep):
# Make sure the plugin dir is copied before mix deps.get
COPY plugins/ plugins/

# Standard Phoenix build steps — no changes needed
RUN mix deps.get --only prod
RUN MIX_ENV=prod mix compile
RUN MIX_ENV=prod mix release
```

If using Option B (GitHub dep), no extra COPY step is needed — `mix deps.get`
fetches the plugin automatically during the image build.

**Plugin activation in a containerised environment:**

Since you cannot do an interactive IEx session easily in production containers,
you can activate the plugin via a release eval command in your startup script
or Docker entrypoint:

```bash
# In entrypoint.sh or deploy script — runs once on first deploy
./bin/voile eval "Voile.PluginManager.install(\"Elixir.VoileLockerLuggage\")"
./bin/voile eval "Voile.PluginManager.activate(\"Elixir.VoileLockerLuggage\")"
```

Or add an idempotent task to your Voile deployment hooks so it is safe to run
on every container restart:

```bash
# Safe to run every time — install/activate are no-ops if already done
./bin/voile eval "
  case Voile.Plugins.get_plugin_by_plugin_id(\"locker_luggage\") do
    nil -> Voile.PluginManager.install(\"Elixir.VoileLockerLuggage\")
    _ -> :ok
  end
"
```

> **Key rule:** Adding or removing a plugin always requires rebuilding the
> container image and redeploying. This is by design — plugins are trusted
> compiled Elixir code, not dynamic bytecode.

---

## Updating the Plugin

### 1. Update the version in `mix.exs`

```elixir
# Change tag to the new release
{:voile_locker_luggage, github: "your-org/voile_locker_luggage", tag: "v1.1.0"}
```

### 2. Fetch and rebuild

```bash
mix deps.update voile_locker_luggage
mix deps.get
MIX_ENV=prod mix release   # or rebuild the container image
```

### 3. Run update migrations

From the admin UI at `/manage/plugins`, click **"Update"**. This calls
`on_update/2` which runs any new migrations.

Or via release eval:

```bash
./bin/voile eval "Voile.PluginManager.update(\"Elixir.VoileLockerLuggage\")"
```

---

## Version Checking

This plugin exposes its current version via `VoileLockerLuggage.metadata/0`:

```elixir
VoileLockerLuggage.metadata().version  # "1.0.0"
```

Voile's plugin system is designed to support automatic version-checking against
GitHub Releases. When implemented, the check works as follows:

1. Voile periodically fetches `https://api.github.com/repos/curatorian/voile_locker_luggage/releases/latest`
2. Compares the `tag_name` against the installed plugin's `metadata().version`
3. Shows an **"Update available"** badge in `/manage/plugins` if a newer version exists

This is an upcoming feature of the core Voile plugin manager. Until then, check
the [GitHub Releases page](https://github.com/curatorian/voile_locker_luggage/releases)
manually for new versions.

---

## Uninstalling

1. Navigate to `/manage/plugins`
2. Click **"Deactivate"** — removes all hooks from the running system
3. Click **"Uninstall"** — rolls back all database migrations

> Warning: Uninstalling permanently drops all locker and session data.

4. Remove the dependency from `mix.exs`:

```elixir
# Remove or comment out:
# {:voile_locker_luggage, ...}
```

5. Rebuild and redeploy.

---

## Development

Clone this repository alongside Voile for local development:

```bash
git clone https://github.com/curatorian/voile_locker_luggage \
  /path/to/voile/plugins/voile_locker_luggage
```

Ensure Voile's `mix.exs` references the plugin as a path dep (see Option C above).

Plugin code hot-reloads automatically in dev mode — changes to `.ex` and `.heex`
files under `plugins/` are picked up by Phoenix's live reload without restarting
the server.

### Running plugin migrations in dev

```bash
cd /path/to/voile
mix ecto.migrate   # runs both core and plugin migrations
```

Or trigger via the admin UI after installing the plugin in dev.

---

## Architecture

This plugin follows the [Voile Plugin System](https://github.com/curatorian/voile/blob/main/plans/voile-plugin-system.md) contract:

- Implements the `Voile.Plugin` behaviour (`metadata/0`, `on_install/0`, `on_activate/0`, `on_deactivate/0`, `on_uninstall/0`, `on_update/2`, `hooks/0`, `routes/0`, `nav/0`, `settings_schema/0`)
- Ships its own Ecto migrations under `priv/migrations/`
- Uses `Voile.Repo` for all database operations
- Registers hooks via `Voile.Hooks` to extend core behaviour
- Mounts LiveViews under `/manage/plugins/locker_luggage/`

---

## License

Apache 2.0 — see [LICENSE](LICENSE).