> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dietrich-development.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Developer reference

> DD Pinboard server exports for board state, clearing, showcase presets and viewers, plus the database schema

Everything needed to drive pinboards from another resource: find a board's database id, read its full state, or wipe it from a quest, an admin panel or a scheduled reset.

<Info>
  Every export is framework-neutral. The same call behaves identically on VORP and on RSG, because all framework access goes through the built-in bridge.
</Info>

## Conventions

Server exports are called as `exports.dd_pinboard:Name(...)` from any server script. They take the board's `locationKey`, which is the `key` of the entry in `Config.boardLocations`, not the numeric database id.

A board row is created on first open or by `SeedBoard`. Until then, state and id lookups return `nil`, clearing returns `false`, and viewer lookup returns an empty list. `SeedBoard` can create the row for a configured location.

The client and NUI net events (`dd_pinboard:sv:*`, `dd_pinboard:cl:*`) are internal to the resource, re-validated on every call, and not a supported API. Build on the exports below.

## Server exports

| Export                  | Signature                   | Returns                                                                                  |
| ----------------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
| `GetBoardIdForLocation` | `(locationKey)`             | `number` - the board's database id, or `nil` if the board has never been opened          |
| `GetBoardState`         | `(locationKey)`             | `table` - `{ board, cards, edges }`, or `nil` if the board does not exist yet            |
| `ClearBoard`            | `(locationKey)`             | `boolean` - `true` when a board existed at that key and was wiped, `false` otherwise     |
| `SeedBoard`             | `(locationKey, presetName)` | `ok, errKey, cardCount, edgeCount`; preset defaults to `crime_board`                     |
| `GetBoardViewers`       | `(locationKey)`             | List of `{ src, name, color }`; empty when nobody is viewing or the board does not exist |

### GetBoardIdForLocation

Resolves a config key to the `id` in `dd_pinboard_boards`. Use it when you need to join against the pinboard tables yourself.

```lua theme={null}
local boardId = exports.dd_pinboard:GetBoardIdForLocation('saint_denis_precinct')
if boardId then
    -- the board exists in the database
end
```

### GetBoardState

Loads the full board as the resource itself sees it: the board row, every card with its payload decoded from JSON, and every thread.

```lua theme={null}
local state = exports.dd_pinboard:GetBoardState('saint_denis_precinct')
if state then
    print(('%s: %d cards, %d threads'):format(state.board.title, #state.cards, #state.edges))
end
```

The returned table has this shape:

```lua theme={null}
{
    board = {
        id            = 1,
        location_key  = 'saint_denis_precinct',
        title         = 'Saint Denis Precinct Board',
        bg_style      = 'cork',        -- 'cork' | 'parchment' | 'dark' | 'slate' | 'linen'
        viewport_x    = 0.0,
        viewport_y    = 0.0,
        viewport_zoom = 1.0,
    },
    cards = {                          -- ordered by z, then id
        {
            id       = 12,
            board_id = 1,
            kind     = 'sticky',       -- 'sticky' | 'text' | 'photo' | 'pin' | 'draw'
            x = 120.0, y = 80.0, w = 200.0, h = 120.0,
            z        = 3,
            rotation = -2.5,
            color    = '#d4a017',      -- may be nil
            payload  = { ... },        -- decoded JSON, content depends on kind
        },
    },
    edges = {                          -- ordered by id
        {
            id           = 4,
            board_id     = 1,
            from_card_id = 12,
            to_card_id   = 15,
            label        = nil,        -- up to 128 characters, or nil
            color        = '#c0392b',  -- may be nil
            style        = 'solid',    -- 'solid' | 'dashed' | 'dotted'
        },
    },
}
```

`payload` is the card's content and is owned by the board UI: a note holds its formatted text, a photo holds `url`, a drawing holds its strokes. Treat it as opaque unless you know the current UI format.

<Info>
  This reads the board from the database on every call, so it is not something to run in a loop. Read it when a menu opens or a job is evaluated, not per frame.
</Info>

### ClearBoard

Removes every card and thread on the board, and immediately pushes the empty state to everyone who has it open. The board row, its background and its stored view remain. This is what the `/pinboard_clear` command calls.

```lua theme={null}
-- Reset the gang's planning board at the end of a heist
local ok = exports.dd_pinboard:ClearBoard('gang_hideout')
if not ok then
    print('no board has been created at gang_hideout yet')
end
```

<Warning>
  This is destructive and takes effect at once. There is no confirmation and no undo; the rows are deleted.
</Warning>

### SeedBoard

Replace the contents of a configured board with a preset from `config/showcase.lua`. This creates the board row if necessary and sends the resulting state to current viewers, clearing card reservations and their undo history.

```lua theme={null}
local ok, errKey, cardCount, edgeCount = exports.dd_pinboard:SeedBoard(
    'showcase_case_board', 'crime_board'
)
if not ok then
    print(('Preset rejected: %s'):format(errKey))
end
```

Use an existing configured location key and preset name. Unlike the admin command, invalid names supplied to the export raise a Lua error. Validation failures return `false` with an error key and leave existing content intact. The counts describe the preset's cards and threads.

Fixed backgrounds override the preset's background; bounds, image hosts and content caps still apply. Validation temporarily counts old and new content together, so a nearly full board may reject a preset. Do not automatically clear it on failure if its content must be retained.

<Warning>
  Successful seeding replaces existing content. Session Undo cannot restore it. These server exports are trusted integration APIs; enforce your own caller permissions before invoking ClearBoard or SeedBoard from another resource.
</Warning>

### GetBoardViewers

Read the current viewers, ordered by arrival, without exposing the internal presence state.

```lua theme={null}
local viewers = exports.dd_pinboard:GetBoardViewers('saint_denis_precinct')
for _, viewer in ipairs(viewers) do
    print(('%s (%d), colour %s'):format(viewer.name, viewer.src, viewer.color))
end
```

`src` is the player's server id. `name` is the display name used on the board; with `Config.anonymizeCollaborators` it is a translated visitor alias rather than the character name. This list includes readers as well as editors and does not indicate who currently holds a card.

## Database schema

Three tables are created on start with `CREATE TABLE IF NOT EXISTS`, so the bootstrap is safe to run on every restart. Threads reference cards, and cards reference boards, with `ON DELETE CASCADE` all the way down: deleting a board row removes its cards and threads, deleting a card removes the threads attached to it.

```sql theme={null}
CREATE TABLE IF NOT EXISTS `dd_pinboard_boards` (
    `id`             INT AUTO_INCREMENT PRIMARY KEY,
    `location_key`   VARCHAR(64) NOT NULL,
    `title`          VARCHAR(128) NOT NULL DEFAULT 'Pinboard',
    `bg_style`       VARCHAR(32) NOT NULL DEFAULT 'cork',
    `viewport_x`     FLOAT NOT NULL DEFAULT 0,
    `viewport_y`     FLOAT NOT NULL DEFAULT 0,
    `viewport_zoom`  FLOAT NOT NULL DEFAULT 1.0,
    `created_at`     TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `updated_at`     TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY `uq_location` (`location_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS `dd_pinboard_cards` (
    `id`             INT AUTO_INCREMENT PRIMARY KEY,
    `board_id`       INT NOT NULL,
    `kind`           ENUM('sticky','text','photo','pin','draw') NOT NULL,
    `x`              FLOAT NOT NULL,
    `y`              FLOAT NOT NULL,
    `w`              FLOAT NOT NULL DEFAULT 200,
    `h`              FLOAT NOT NULL DEFAULT 120,
    `z`              INT NOT NULL DEFAULT 0,
    `rotation`       FLOAT NOT NULL DEFAULT 0,
    `color`          VARCHAR(16) NULL,
    `payload`        JSON NOT NULL,
    `created_at`     TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `updated_at`     TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX `idx_board` (`board_id`),
    FOREIGN KEY (`board_id`) REFERENCES `dd_pinboard_boards`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS `dd_pinboard_edges` (
    `id`             INT AUTO_INCREMENT PRIMARY KEY,
    `board_id`       INT NOT NULL,
    `from_card_id`   INT NOT NULL,
    `to_card_id`     INT NOT NULL,
    `label`          VARCHAR(128) NULL,
    `color`          VARCHAR(16) NULL,
    `style`          ENUM('solid','dashed','dotted') NOT NULL DEFAULT 'solid',
    `created_at`     TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX `idx_board` (`board_id`),
    FOREIGN KEY (`board_id`) REFERENCES `dd_pinboard_boards`(`id`) ON DELETE CASCADE,
    FOREIGN KEY (`from_card_id`) REFERENCES `dd_pinboard_cards`(`id`) ON DELETE CASCADE,
    FOREIGN KEY (`to_card_id`)   REFERENCES `dd_pinboard_cards`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```

`location_key` is unique, so there is exactly one board per config key. The `payload` column uses the `JSON` type and needs MySQL 5.7.8+ or MariaDB 10.2.7+.

<Note>
  Card reservations and viewer presence are held in memory, not in the database. A resource restart clears them. There is no board-wide editing lock. Undo history belongs to the client session and is not persisted.
</Note>

## Moving or renaming a board

The config `key` is the board's identity. To rename a location without losing its content, update the row instead of the config alone:

```sql theme={null}
UPDATE dd_pinboard_boards SET location_key = 'new_key' WHERE location_key = 'old_key';
```

Then change the `key` in `Config.boardLocations` to match. Changing only the config creates a fresh empty board under the new key on first open.
