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:
-
STEP1 — Date conditions and interviewers
- Specify the scheduling window (start / end date), time range (start / end time), and weekday condition
- Pick main and sub interviewers (multiple allowed) and enter the target headcount
- "Calculate slots" calls
GET /projects/{id}/interviewers/availability, which derives slots from the interviewers' free/busy information - Review the result and "Place tentative holds" calls
POST /projects/{id}/interviewers/tentative-slotsto create provisional events on the project calendar - 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
primaryorreserve PUT /projects/{id}/candidates/selectionsaves 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:
- Upload a TSV (UTF-16LE, tab-separated) to
POST /projects/{id}/candidates/import/upload - Build a mapping in the UI using the returned column info, sample values, and suggested mapping
- Execute with
POST /projects/{id}/candidates/import/{fileId}/execute(asynchronous) - Poll
GET /projects/{id}/candidates/import/{importId}/statusfor progress and results
Survey import use case:
- Dry-run the result with
POST /projects/{id}/candidates/import/survey/preview, specifying a mapping and filter - If it looks right, execute with
POST /projects/{id}/candidates/import/survey - 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 conditionis one ofequals/not_equals/in/not_in/contains/between/greater_than/less_thanfieldmay be prefixed asattributes.xxx/survey_responses.xxx. Without a prefix,attributesis searched first, thensurvey_responsesmax_scorecaps the totalPOST /projects/{id}/candidates/scorerecalculates 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 tofalse, 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_idis NULL) or project-specific typeis one ofinvitation/reminder/confirmation/cancellation- Subject and body support placeholders such as
{{candidate_name}}and{{project_name}} POST /projects/{id}/emails/previewrenders the result for a single candidatePOST /projects/{id}/emails/sendsends to multiple candidates and returnssent_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.tsas 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.