Skip to content

feat(notion): add sync-notion.mjs — push tracker to Notion Kanban dashboard#670

Open
Schlaflied wants to merge 3 commits into
santifer:mainfrom
Schlaflied:feat/sync-notion-dashboard
Open

feat(notion): add sync-notion.mjs — push tracker to Notion Kanban dashboard#670
Schlaflied wants to merge 3 commits into
santifer:mainfrom
Schlaflied:feat/sync-notion-dashboard

Conversation

@Schlaflied
Copy link
Copy Markdown

@Schlaflied Schlaflied commented May 16, 2026

Closes #667

What this adds

sync-notion.mjs reads data/applications.md and creates/updates a Notion database so the job search pipeline is visible as a shareable Kanban board — no manual copy-paste, no screenshots.

How it works

  1. Parses the markdown tracker table (all 9 columns)
  2. On first run: creates the Notion database inside a user-specified parent page; saves database_id back to config/profile.yml
  3. Upserts pages keyed on "{Company} — {Role}" title — creates new entries, updates existing ones
  4. Optionally uploads interview-prep/*.md files as Notion sub-pages under the same parent
  5. After sync: user switches the database to Board view → Group by Status for the Kanban

Database schema created

Property Type
Name title — "{Company} — {Role}"
Company rich_text
Role rich_text
Score number
Status select (colour-coded: Interview=orange, Offer=green, Rejected=red …)
Date date
Has PDF checkbox
Report url
Notes rich_text
# number (tracker row number)

Flags

Flag Behaviour
(none) Full sync
--dry-run Print all create/update operations, no API writes
--since N Only sync entries with date ≥ today − N days
--auth Test token + list accessible Notion pages with their IDs

Setup

# config/profile.yml
notion:
  token: secret_xxx
  parent_page_id: <page ID>   # run --auth to find
  # database_id is auto-written after first run
  sync_interview_prep: true   # optional

Scope

  • 1 new file: sync-notion.mjs (408 lines)
  • 0 existing files modified
  • 0 new npm dependencies — uses js-yaml (already in package.json) + native fetch

Test plan

  • --auth lists accessible pages and shows IDs
  • --dry-run prints create/update plan without API calls
  • First run with valid parent_page_id creates the database and writes database_id to profile
  • Subsequent runs upsert without duplicating pages
  • --since 7 filters to last 7 days only
  • sync_interview_prep: true uploads prep .md files as sub-pages
  • Missing token / parent_page_id exits with a clear error message

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added Notion synchronization for career tracker applications with configurable settings.
    • CLI options: dry-run, authentication validation, and date-based filtering.
    • Automatically creates or reuses a Notion database and syncs application pages (create/update).
    • Optionally syncs interview-prep materials to Notion.
    • Prints completion summaries and error reporting.

Review Change Stack

…hboard

Adds a zero-token script that reads data/applications.md and creates/
updates a Notion database so the job search pipeline is visible as a
shareable Kanban board grouped by application status.

Features:
- Parses markdown tracker table (all columns: company, role, score,
  status, date, PDF flag, report link, notes)
- Creates the Notion database on first run; persists database_id to
  config/profile.yml automatically
- Upserts pages: creates new entries, updates existing ones by title
- Optional: uploads interview-prep/*.md as Notion sub-pages
- --dry-run: preview without any API writes
- --since N: only sync entries from the last N days
- --auth: test token + list accessible parent pages
- Config via config/profile.yml under notion: key (token, parent_page_id,
  database_id, sync_interview_prep)

Closes #(TBD)
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Warning

Rate limit exceeded

@Schlaflied has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 28 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f65bef67-0d2a-417a-a3c4-d8a1577a395b

📥 Commits

Reviewing files that changed from the base of the PR and between e0cd9e8 and 21528be.

📒 Files selected for processing (1)
  • sync-notion.mjs
📝 Walkthrough

Walkthrough

Adds sync-notion.mjs, a CLI Node.js script that parses data/applications.md, ensures/creates a Notion database, upserts pages keyed by company+role, optionally uploads interview-prep/*.md, and provides --dry-run, --since N, and --auth modes.

Changes

Notion Sync Script

Layer / File(s) Summary
Configuration & API Foundation
sync-notion.mjs (1–116)
CLI flags (--dry-run, --auth, --since), profile load/persist from config/profile.yml, Notion token resolution (env fallback), and notionFetch wrapper for API calls.
Applications Table Parsing
sync-notion.mjs (117–165)
Parses data/applications.md to find the tracker table and produce normalized application objects (company, role, status/date defaults, numeric score cleanup, report URL, notes).
Notion Schema & Upsert Preparation
sync-notion.mjs (167–233)
Builds database schema payload and per-application property mapping; fetches existing pages with pagination and builds a title-keyed index used for upsert decisions.
Optional Features: Interview Prep & Auth
sync-notion.mjs (234–306)
Markdown-to-Notion-block conversion and upload for interview-prep/*.md files (with truncation/dry-run handling); --auth mode validates token via /users/me and lists accessible pages via /search.
Sync Orchestration
sync-notion.mjs (308–423)
Main main() orchestration: validates inputs, applies --since filtering, creates or reuses database (writes database_id back to config if created), runs the create/update upsert loop with counters and error tracking, optionally uploads interview-prep pages, and exits with summary or non-zero on fatal errors.

Sequence Diagram

sequenceDiagram
  participant User
  participant CLI as sync-notion.mjs
  participant Config as Config System
  participant Parser as Markdown Parser
  participant NotionAPI as Notion API
  participant DB as Notion Database

  User->>CLI: node sync-notion.mjs
  CLI->>Config: load config/profile.yml
  Config-->>CLI: token, parent_page_id, database_id
  CLI->>Parser: parse data/applications.md
  Parser-->>CLI: array of applications
  CLI->>NotionAPI: POST /databases (create if needed)
  NotionAPI->>DB: create database
  DB-->>NotionAPI: database_id
  NotionAPI-->>CLI: persist database_id to config
  CLI->>NotionAPI: GET /databases/{id}/query
  NotionAPI->>DB: fetch existing pages
  DB-->>NotionAPI: page list
  NotionAPI-->>CLI: build index by title
  CLI->>NotionAPI: PATCH/POST /pages (upsert loop)
  NotionAPI->>DB: update or create pages
  DB-->>NotionAPI: success
  NotionAPI-->>CLI: created/updated counts
  CLI-->>User: print summary and Done
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: adding sync-notion.mjs to push the tracker to Notion Kanban.
Linked Issues check ✅ Passed The implementation fulfills all coding objectives from #667: parses markdown table, creates/persists Notion database, performs upserts keyed by company+role, optionally syncs interview-prep, and supports --dry-run/--since/--auth flags.
Out of Scope Changes check ✅ Passed The changeset adds only sync-notion.mjs with no modifications to existing files, staying within the scope of issue #667.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sync-notion.mjs`:
- Line 125: The findIndex call that assigns tableStart contains a duplicated
condition (l.includes('| # |') || l.includes('| # |')); remove the redundant
check so the predicate only tests unique conditions (e.g., a single
l.includes('| # |') or any intended alternate token). Update the expression
where tableStart is computed (the lines.findIndex(...) invocation) to use the
corrected predicate.
- Line 245: The filter currently excludes files starting with the misspelled
prefix 'archieved', so replace that string with the correct 'archived' in the
files assignment (the readdirSync(...).filter(...) expression that populates the
files variable for INTERVIEW_PREP) to ensure archived interview-prep markdown
files are properly skipped.
- Line 147: The property initializer using parseInt ("num:    parseInt(num) ||
0,") must pass an explicit radix to avoid ambiguous parsing; update the call to
use a decimal radix (e.g., pass 10) or use Number(...) for clarity so the
expression becomes a parseInt call with radix 10 (or an equivalent decimal
conversion) while preserving the fallback to 0.
- Line 320: The filter using apps = apps.filter(a => (a.date || '') >=
cutoffStr) is fragile because it compares date strings lexically; update the
filter to parse a.date and cutoffStr into Date objects (or timestamps) and
compare numerically instead, handling malformed or missing a.date by treating
them as invalid (e.g., exclude or set to new Date(0)); locate the filter
expression in sync-notion.mjs and replace the string comparison with a numeric
comparison using Date.parse or new Date(...) for a.date and cutoffStr (ensure
you check isNaN on parsed values).
- Line 399: The code calls uploadInterviewPrep(parentPageId || databaseId) but a
databaseId cannot be used as a parent page ID; update the logic so when
syncInterviewPrep is enabled you require a valid parentPageId (do not fall back
to databaseId), and if parentPageId is missing either throw/log a clear error
and skip calling uploadInterviewPrep or fail fast; adjust the check where
uploadInterviewPrep is invoked (referencing uploadInterviewPrep, parentPageId,
databaseId, and syncInterviewPrep) to enforce this validation and avoid passing
a databaseId as the parent.
- Around line 186-200: In buildPageProperties, defensively truncate the company
and role strings (used for Company, Role and the Name title concatenation) to
the same safe maximum as Notes (e.g., 2000 chars) before building props so long
values cannot break Notion rich_text limits; update the Name construction to use
the truncated company/role variables and keep existing behavior for Notes,
Score, Report, Status, Date, Has PDF and #.
- Around line 93-109: The notionFetch function currently calls loadConfig() on
every request which causes repeated file I/O; change notionFetch to accept an
optional cfg parameter (e.g., notionFetch(path, opts = {}, cfg)) and use that
when provided, falling back to loadConfig() only if cfg is missing; then update
callers like main() and runAuth() to call loadConfig() once, store the cfg, and
pass it into their notionFetch calls to eliminate repeated config reloads.
- Around line 230-238: The parseMarkdownToBlocks code builds very long block
objects inline (for heading_1/2/3, bulleted_list_item, paragraph); extract a
small helper (e.g., makeRichTextBlock(type, content) or makeBlock(type,
contentPath)) that returns the Notion block shape with rich_text populated from
a plain string, then replace each long inline construction in
parseMarkdownToBlocks with calls to that helper for
heading_1/heading_2/heading_3, bulleted_list_item, and paragraph to collapse the
long lines and improve readability while preserving the same object keys
(object, type, heading_1/heading_2/heading_3, bulleted_list_item, paragraph, and
their rich_text structure).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: db86699e-f6b9-412d-bf03-7bb37224e673

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1f3a3 and 30c22f8.

📒 Files selected for processing (1)
  • sync-notion.mjs

Comment thread sync-notion.mjs Outdated
Comment thread sync-notion.mjs
Comment thread sync-notion.mjs
Comment thread sync-notion.mjs
Comment thread sync-notion.mjs Outdated
Comment thread sync-notion.mjs
Comment thread sync-notion.mjs
Comment thread sync-notion.mjs Outdated
- notionFetch: add module-level config cache (_cachedCfg / getCfg) to
  eliminate repeated file I/O on every API call; accept optional cfg param
- buildPageProperties: truncate company/role to 2000 chars before building
  rich_text and title properties to respect Notion API limits
- parseMarkdownToBlocks: extract makeBlock(type, content) helper to replace
  long inline block object construction for all five block types
- uploadInterviewPrep: require valid parentPageId — warn and skip when
  sync_interview_prep is enabled but parentPageId is missing; never fall
  back to databaseId which cannot be used as a page parent
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sync-notion.mjs`:
- Line 196: The Name field is truncated in buildPageProperties() but the upsert
lookup uses the raw `${app.company} — ${app.role}`, causing mismatches and
duplicates; create a single title-normalization helper (e.g.,
normalizeNotionTitle(title)) that applies the same trimming/slicing logic
(including the 2000 char limit and any other normalization) and replace both the
Name assignment in buildPageProperties() and the key used for lookup/upsert (the
`${app.company} — ${app.role}` expression) to call normalizeNotionTitle(...) so
both lookup and persistence use the identical normalized title.
- Around line 148-150: The extracted reportUrl (from reportMatch/reportUrl) must
be normalized: if reportUrl is already absolute (starts with "http://" or
"https://") keep it; if it is a repo-relative path like "reports/foo.md" convert
it to the repository's absolute GitHub URL (e.g., construct using the repo
owner/name and branch constants used elsewhere in the module) before storing to
Notion; if you cannot resolve a repo base or the path is not transformable, skip
setting the Report field entirely. Update the extraction block that defines
reportMatch and reportUrl to perform this normalization (detect absolute URLs,
resolve relative paths against the repo base, or omit the field) so Notion only
receives shareable absolute links.
- Around line 254-274: The uploadInterviewPrep loop always creates new pages and
should instead detect existing uploads by a stable source key: before creating a
page in uploadInterviewPrep, call notionFetch to query the parentPageId's
children (or search pages) for a page whose identifying property (e.g., a
"Source" or "SourcePath" property or the normalized title) matches the current
file name; if found, skip creation or update that page's children rather than
POSTing a new page. When creating new pages via notionFetch('/pages', ...)
include a dedicated property (e.g., "Source" with the file name) so subsequent
runs can reliably match files to pages; keep using INTERVIEW_PREP,
parseMarkdownToBlocks and DRY_RUN as currently used and ensure the matching uses
the same title normalization logic (firstLine → title) or the new Source
property.
- Around line 361-365: The early return inside the DRY_RUN branch prevents
loading existing pages and computing a true dry-run plan; remove the immediate
"return" after logging apps so the read-only discovery path still runs (load
existing Notion pages and compute creates vs updates using the same logic that
follows), and instead gate actual write calls (the Notion create/update/upsert
calls) behind DRY_RUN so they are suppressed while still producing the
interview-prep preview and full plan; apply the same change to the other DRY_RUN
guard sites noted (the blocks around the other ranges) and keep checks on
DRY_RUN to avoid performing writes while allowing all read/preview logic to
execute.
- Around line 78-83: The syncInterviewPrep flag currently defaults to true when
the key is missing; change its logic so it is opt-in by setting
syncInterviewPrep only when the source value is explicitly true (e.g., replace
the current n.sync_interview_prep !== false check with a strict check like
n.sync_interview_prep === true). Update the returned object (the
token/parentPageId/databaseId/syncInterviewPrep block) so syncInterviewPrep uses
that explicit check against n.sync_interview_prep to avoid creating
interview-prep pages on first sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b2468c36-59b9-484b-a6c2-18a7c683e341

📥 Commits

Reviewing files that changed from the base of the PR and between 30c22f8 and e0cd9e8.

📒 Files selected for processing (1)
  • sync-notion.mjs

Comment thread sync-notion.mjs
Comment thread sync-notion.mjs Outdated
Comment thread sync-notion.mjs
Comment thread sync-notion.mjs
Comment thread sync-notion.mjs
- syncInterviewPrep: change default from opt-out (!== false) to opt-in
  (=== true) so first-time syncs don't unexpectedly upload prep notes
- reportUrl: only pass absolute http/https URLs to Notion's url field;
  skip repo-relative paths like reports/foo.md which Notion would reject
- uploadInterviewPrep: add fetchExistingPrepTitles to check for existing
  child pages before creating — skips files already uploaded on prior runs
- DRY_RUN: remove early return; always fetch existing pages when databaseId
  is known so dry-run shows accurate create vs update breakdown per entry
@Schlaflied
Copy link
Copy Markdown
Author

@coderabbitai resolve

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

✅ Actions performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Notion Kanban dashboard — push applications tracker + interview prep to Notion

1 participant