Skip to content

Features

Feature List

Feature Summary Roles Status
Google OAuth login Log in to the console with a Google account and obtain a JWT All users Implemented
Project management Create / update / delete / list projects and view the dashboard Read: viewer / Write: member Implemented
Arrange flow (STEP1–3) Date conditions, interviewers and tentative holds → segments and candidate selection → additional questions member Implemented
Interviewer management Add / remove interviewers on a project Read: viewer / Write: member Implemented
Availability calculation Compute bookable slots from interviewers' Google Calendars viewer Implemented
Tentative holds Bulk create / delete provisional events on the project calendar member Implemented
Candidate import (TSV) Upload a TSV file and import it with a column mapping member Implemented
Candidate import (survey) Import from Databricks survey answers with a mapping and filter member Implemented
Candidate list & filtering Filter by status, selection type, segment, score, and answer content viewer Implemented
Scoring Score and recalculate candidates using the project's scoring rules member Implemented
Candidate selection Bulk update selection type (primary / reserve) and segment member Implemented
Status management Candidate status transitions and history recording member Implemented
Email template management CRUD for system-wide and project-specific templates Read: viewer / Write: member Implemented
Email sending Bulk send from a template with preview and delivery logs member Implemented
Scheduling (public candidate page) View the request, fetch slots, and submit preferred times from a token URL Unauthenticated Implemented
Schedule confirmation Create a reservation from the candidate's preferred slots and convert the tentative event member Implemented
Reservation management List, view, and cancel reservations Read: viewer / Write: member Implemented
Survey data browsing Browse surveys, questions, panels (respondents) and answers viewer Implemented
PII cleanup Anonymize personal data past its retention period via batch Batch Implemented

Feature Details

Arrange Flow (STEP1–STEP3)

The staged wizard at the heart of the project detail screen. Its state is stored wholesale in projects.arrange_settings (a single JSON column), and the frontend logic is concentrated in use-project-arrange.ts.

Roles: member

Use case:

  1. STEP1 — Date conditions and interviewers

    1. Specify the scheduling window (start / end date), time range (start / end time), and weekday condition
    2. Pick main and sub interviewers (multiple allowed) and enter the target headcount
    3. "Calculate slots" calls GET /projects/{id}/interviewers/availability, which derives slots from the interviewers' free/busy information
    4. Review the result and "Place tentative holds" calls POST /projects/{id}/interviewers/tentative-slots to create provisional events on the project calendar
    5. The created event info is stored in arrange_settings.step1.tentativeEventIds

    How slots are laid out

    • Business hours are 9:00–18:00 JST on weekdays; weekends are excluded
    • Start times are multiples of the interview length. 90 minutes gives six slots — 9:00-10:30 / 10:30-12:00 / … / 16:30-18:00 — that never overlap
    • Slots that would run past business hours are not produced (with 90 minutes, 16:30 is the last start)
    • Lunch is not excluded. If an interviewer has an entry on their calendar, the free/busy lookup removes that slot anyway
    • The duration picked on screen is sent to the API (durationMinutes), so the preview reflects it without saving the project first
    • STEP2 — Segmentation and candidate selection
    • Define segments from survey questions and their options (arrange_settings.step2.segments)
    • List candidates per segment and mark them primary or reserve
    • PUT /projects/{id}/candidates/selection saves selection types and segment IDs in bulk
    • STEP3 — Additional questions
    • Register per-segment / per-candidate questions to ask during the interview
    • Stored in arrange_settings.step3.questions

Duplicated type definition

The ArrangeSettings type is defined twice — in the backend's src/db/schema.ts and the frontend's src/types/arrange-settings.ts (OpenAPI does not express the internal structure of a JSON column). Change one and you must change the other.

Candidate Import

Roles: member

Two paths exist: TSV and survey. Both leave a record in import_logs (import_type: manual_tsv / manual_survey / auto_survey).

TSV import use case:

  1. Upload a TSV (UTF-16LE, tab-separated) to POST /projects/{id}/candidates/import/upload
  2. Build a mapping in the UI using the returned column info, sample values, and suggested mapping
  3. Execute with POST /projects/{id}/candidates/import/{fileId}/execute (asynchronous)
  4. Poll GET /projects/{id}/candidates/import/{importId}/status for progress and results

Survey import use case:

  1. Dry-run the result with POST /projects/{id}/candidates/import/survey/preview, specifying a mapping and filter
  2. If it looks right, execute with POST /projects/{id}/candidates/import/survey
  3. Check history via GET /projects/{id}/candidates/import/history

Scoring

Roles: member

The rules defined in projects.scoring_rules are applied to each candidate's attributes and survey_responses to produce a total score.

  • A rule has four fields: field, condition, values, score
  • condition is one of equals / not_equals / in / not_in / contains / between / greater_than / less_than
  • field may be prefixed as attributes.xxx / survey_responses.xxx. Without a prefix, attributes is searched first, then survey_responses
  • max_score caps the total
  • POST /projects/{id}/candidates/score recalculates every candidate in the project

Candidate Status Management

Roles: member

project_candidates.status is a 10-value enum, and the allowed transitions are defined in VALID_STATUS_TRANSITIONS. An invalid transition raises InvalidStatusTransitionError (400 / INVALID_STATUS_TRANSITION).

stateDiagram-v2
  [*] --> not_contacted
  not_contacted --> contacted
  not_contacted --> declined
  not_contacted --> cancelled
  contacted --> waiting_response
  contacted --> bounced
  contacted --> declined
  contacted --> cancelled
  waiting_response --> scheduling
  waiting_response --> declined
  waiting_response --> cancelled
  scheduling --> scheduled
  scheduling --> declined
  scheduling --> cancelled
  scheduled --> interviewed
  scheduled --> cancelled
  interviewed --> completed
  interviewed --> cancelled
  bounced --> contacted
  cancelled --> not_contacted
  completed --> [*]
  declined --> [*]
Status Meaning
not_contacted Not contacted yet
contacted Contacted (invitation sent)
waiting_response Awaiting a reply
scheduling Scheduling in progress (candidate submitted preferred slots)
scheduled Schedule confirmed (reservation created)
interviewed Interview conducted
completed Completed
declined Declined
bounced Email bounced
cancelled Cancelled

Every transition appends a row to status_histories with old_status / new_status / changed_by / note.

Scheduling

Roles: Candidate (unauthenticated) + member

sequenceDiagram
    participant C as Candidate
    participant FE as Public page<br/>/scheduling/{token}
    participant API as Backend API
    participant DB as PostgreSQL
    participant GC as Google Calendar
    participant M as Admin (member)

    M->>API: Send invitation email
    API->>DB: Issue scheduling_tokens
    API-->>C: Email with token URL
    C->>FE: Open the token URL
    FE->>API: GET /scheduling/{token}
    API-->>FE: Request info, expiry, reservation state
    FE->>API: GET /scheduling/{token}/slots
    API-->>FE: Bookable slots
    C->>FE: Select preferred slots (1-20)
    FE->>API: POST /scheduling/{token}/submit-availability
    API->>DB: Replace candidate_available_dates
    API->>DB: Update status to scheduling
    API-->>C: Acknowledgement email
    M->>API: POST /projects/{id}/candidates/{cid}/confirm-schedule
    API->>DB: Create reservations
    API->>GC: Convert tentative event to confirmed
    API-->>C: Confirmation email
  • Scheduling tokens expire after SCHEDULING_TOKEN_TTL_DAYS (default 14 days)
  • Preferred slots are fully replaced on each submission (not appended)
  • Submitting when a confirmed reservation already exists returns 409 SLOT_NOT_AVAILABLE
  • While PROTOTYPE_MODE=true (the default on dev), email sending and calendar writes are skipped. stg sets it to false, so both take effect

Google Calendar Integration

The project calendar

Creating a project also creates a dedicated calendar named [インタビュー] {project name}, stored in projects.google_calendar_id. It belongs to the project creator, and the service account impersonates that person to read and write it. If creation failed earlier, or the project predates the calendar, it is created when tentative holds are first placed.

Where events go, and who attends

Everything from tentative holds to confirmed interviews lives on this calendar. Nothing is written to an interviewer's personal calendar.

Operation Calendar Attendees Google notification
Free/busy lookup The interviewer's own (free/busy only) - -
Creating / deleting tentative holds Project calendar The assigned interviewer Not sent
Confirming a schedule Project calendar Main + sub interviewers Sent
Cancelling a reservation Project calendar Same Sent
  • Interviewers are attendees from the tentative stage onward. Otherwise the held slot would not show up in their free/busy information and the next calculation would hand out the same time again
  • Tentative holds are created dozens at a time, so they are silent. Only confirmations and cancellations notify
  • Candidates are never attendees. They are contacted through the application's own email

Event names

State Format Example
Tentative [仮] {project} インタビュー枠 [仮] 新商品調査 インタビュー枠
Confirmed {candidate}様 インタビュー(担当:{interviewer})/ {project} 山田太郎様 インタビュー(担当:渡辺)/ 新商品調査

The candidate is unknown while the slot is only held, so the name is rewritten on confirmation.

Email Sending

Roles: member

  • Templates are either system-wide (project_id is NULL) or project-specific
  • type is one of invitation / reminder / confirmation / cancellation
  • Subject and body support placeholders such as {{candidate_name}} and {{project_name}}
  • POST /projects/{id}/emails/preview renders the result for a single candidate
  • POST /projects/{id}/emails/send sends to multiple candidates and returns sent_count / failed_count / errors
  • Every sent message is stored in email_logs, body included

Survey Data Integration (Creative Survey / Ask One)

Roles: viewer (read-only)

Answers are read straight from Databricks Unity Catalog. The only thing cached in Postgres is the metadata that backs the survey list.

Source Purpose
cs.cs_dm.dm_answers_<survey_id> Answers for one survey, already joined with panels
databricks_surveys (Postgres) Survey list and detail, refreshed by a daily batch
  • The metadata aggregation scans roughly 68M rows and takes about 15 seconds, so it never runs per request
  • The refresh runs src/batch/databricks-meta-sync.ts as an ECS Fargate task
  • See Databricks integration for details

S3 CSVs used to be cached here

The application ingested the answers.csv.gz / panels.csv.gz files Creative Survey publishes to S3, but a Databricks pipeline was ingesting the same files, so the data was managed twice. The sync feature was removed.

PII Cleanup

Roles: Batch

The pii-cleanup batch anonymizes personal data past its retention period (3 months by default). Related records themselves are retained for aggregation.

bun run src/batch/pii-cleanup.ts '{"dryRun":true}'