V112: Project lifecycle + translations + drop unique-name indexes.
Three related changes to the phoenix_kit_projects* tables:
1. archived_at on phoenix_kit_projects
Replaces the dual-purpose status field (which held both lifecycle
state and a soft-hide flag) with a dedicated nullable timestamp.
Mirrors the workspace convention used by phoenix_kit_publishing's
posts.trashed_at and phoenix_kit_files.trashed_at — null = visible,
non-null = soft-hidden, with the timestamp doubling as audit metadata.
The status column is kept (intentional — see
phoenix_kit_projects/AGENTS.md) so a future workflow concept that
legitimately wants a string lifecycle state (e.g. "paused", "blocked",
"on_hold") can reuse the column without another migration. Application
code stops reading or writing it; existing rows whose status is
"archived" get backfilled into archived_at so the dashboard
filters keep working transparently.
2. translations JSONB on the three project tables
Adds translations JSONB NOT NULL DEFAULT '{}' to:
phoenix_kit_projects(Project — translatable:name,description)phoenix_kit_project_tasks(Task — translatable:title,description)phoenix_kit_project_assignments(Assignment — translatable:description)
Storage shape mirrors the entities-module "settings translations"
pattern from PhoenixKitWeb.Components.MultilangForm's
<.translatable_field> (the variant where secondary_name and
lang_data_key are passed explicitly):
%{
"es-ES" => %{"name" => "Proyecto", "description" => "..."},
"fr-FR" => %{"name" => "Projet"}
}The primary-language values stay in their existing columns (name,
title, description) — the JSONB only holds secondary-language
overrides. Empty/missing override falls back to the primary value at
render time. No primary-language marker key (_primary_language) is
needed because the primary lives outside the JSONB.
3. Drop unique-name indexes on projects + tasks
V105 split phoenix_kit_projects_name_index into two partial unique
indexes (one per is_template). V112 drops both of those plus the
unique-title index on phoenix_kit_project_tasks because user-input
display names are policy, not structure: code references resources
by uuid, so duplicate names across projects/templates/tasks is
fine and the unique constraint just made common workflows (clone
twice, two teams' "Onboarding" templates) raise a constraint error.
Indexes removed:
phoenix_kit_projects_name_template_index(V105)phoenix_kit_projects_name_project_index(V105)phoenix_kit_project_tasks_title_index(V101)
4. Retype scheduled_start_date from date to timestamp(0)
The "scheduled start" field originally held only a date — fine for
the daily-cadence projects, awkward for "this campaign starts at
09:00 sharp" or "the announcement at 14:30." V112 promotes it to
timestamp(0) so the form / popup can carry hour-and-minute
precision. Existing date values are preserved at midnight UTC.
The column name is kept (scheduled_start_date) — renaming to
scheduled_start_at would force every call site, the changeset
cast list, and any in-flight URL params to chase. Lying name +
honest type beats a churn pass; future cleanup can rename when a
larger refactor is on the table.
5. Add position to phoenix_kit_project_tasks and phoenix_kit_projects
Drives manual reorder of the task library, project list, and
template list views. NOT NULL with a default of 0; existing rows
fold into the same 0 bucket and the schema's secondary
order-by-inserted_at kicks in until the user actually drags. New
rows should be inserted via next_task_position/0 /
next_project_position/1 so they land at the bottom of their
bucket.
phoenix_kit_projects.position is interpreted per is_template
scope — projects and templates share the same column but order
independently (the LV sorts within is_template = false for the
project list, is_template = true for the template list).
Idempotent: re-running is a no-op once the columns + indexes are in the post-V112 shape.