Listings: one place where postings arrive, captured or typed, and are triaged before any becomes an application #25

Closed
opened 2026-09-05 12:58:41 +00:00 by tiagoagueda · 0 comments
Owner

Observation

i don't like the "Capture" "Record" overwhelming the navbar. i want to integrate those two operations in a single dash named "Offers" or something similar and from them, the job applicant decides which ones to pass to the applications phase

What exists today

  • The header carries two buttons on every page, Capture and Record (base.html, lines 58–63). #10 takes them out of the header and parks them on the dashboard. This issue decides where they actually go, and it is not the dashboard.
  • Two ways in, and both end in an application. Record an application is one form — ApplicationIntakeForm — that creates the company, the posting and the application together (its docstring: "an application is almost always entered while looking at a posting"). Capture turns a URL or pasted HTML into a Capture holding parsed data, which waits on the captures page (jobs:capture_list, linked from the dashboard as Captures awaiting review); Review opens the same intake form pre-filled and, on save, creates the application and links the capture to it (jobs/capture_views.py, lines 134–197; the wiki says it plainly: "Accepting one creates the application and links the two").
  • JobPosting is already a full model — company, title, location, remote and employment type, URL, description, salary, posted, closing and closed dates — with create, detail, edit and delete views. But it has no list page, and no way to be created from the interface on its own: jobs:posting_create is linked from no template. A posting only ever exists behind an application or on a company page.
  • Nothing records "I saw this and am thinking about it". The nearest thing is an application in Status.DRAFT, which is a different statement: I have decided, and I am preparing.

In short, Postulo models the moment after the decision. The observation asks for the moment before it.

Shape

Listings is the stage before Applications: every posting the person has noticed, however it arrived, until they decide.

  1. Navigation. Listings sits between Dashboard and Applications. Capture and Record leave the header (#10) and become the two actions of the Listings page: Capture (a URL or pasted page, unchanged) and Add a listing (the posting half of today's intake form: company and posting, no application). Both land in Listings.

  2. A state on the posting. new on arrival; shortlisted or discarded by the person; applied derived from the existence of an application, never set by hand; closed derived from closed_at or a closes_at in the past. Plus noted_at (when it entered Listings) and decided_at. The Listings page shows new and shortlisted by default, with filters for discarded and closed (#20's header filters apply).

  3. The decision. Apply on a listing opens the application half of the intake form — applied date, channel, CV, letter, contacts — creates the application and moves the listing out of the list; the application then follows the existing pipeline, draft included, which regains its proper meaning: preparing. Discard takes an optional reason (not for me, pay, location, closed, other) and keeps the record: what a person passed on, and why, is data — Insights gains selectivity (listings seen against applications made) and "which sources produce listings I actually apply to". A discarded listing can be restored.

  4. Capture review changes its ending. A review lands in Listings as a new listing instead of creating an application. The review screen keeps its correction step — that is what makes captures safe — but its form is the posting half, with one checkbox, I have already applied, that keeps the one-step path for the most common case: recording after the fact. Captures awaiting review appear as a strip at the top of the Listings page; the captures page folds into it and jobs:capture_list redirects there.

  5. Record an application stays, as the one-step action on the Applications page, implemented as "add a listing, then apply" so the data is identical whichever door was used.

  6. Dashboard. Listings to decide on (with closing soon from closes_at) replaces Captures awaiting review. A listing's closing date can raise a reminder, through #4, without any new machinery.

  7. API and extensions. The capture API's surface does not change: POST /captures and review_url keep working, only the page a review ends on differs. #12's read API gains listings, and the "already tracked" badge in #17 and #18 covers listings as well as applications, which is the case where it is most useful.

  8. Board. Unchanged: applications by status. Listings may deserve a small board of its own (new / shortlisted) later; a list with filters comes first.

  9. Export and import. The state fields join the posting section; FORMAT_VERSION becomes 2 and the importer keeps accepting 1, deriving applied where an application exists.

  10. Migration. Every existing posting has an application, so every one becomes applied by derivation. No row changes meaning.

  11. Documentation. Capturing postings and Tracking applications are rewritten around Listings → Applications; docs/PLAN.md gains the stage in the pipeline description. seed_demo gets a handful of undecided and discarded listings so the page is not empty on first sight.

  12. Duplicate detection. Capturing or adding a listing whose URL — normalised: scheme and host folded, tracking parameters stripped — matches a listing or application the person already has says so ("already in your listings since 12 May") and offers to open it instead of creating a twin. The capture API answers the same way in its response, so #17 and #18 can show it in the extension before anything is sent.

The name

Listings, decided on 2026-09-05. It is the one-word form of "job listing"; it collides with nothing in the interface — the pipeline stage Offer (Status.OFFER, event Offer received) keeps its one meaning, an employer's offer at the end of the process — and it has standard equivalents in French (annonces) and Portuguese (anúncios). Considered and set aside: Offers, the observation's original word, because of that collision; Vacancies (formal); Openings (no French equivalent); Postings (American in flavour); Jobs (reads as employment history beside the career record). In code the model stays JobPosting; URL names, views and templates say listing.

Classification

Enhancement. A workflow change, and therefore a paragraph in the release notes, but not a breaking change: the API surface is unchanged, existing data migrates by derivation, the old captures URL redirects, and no operator action is required.

Depends on / relates to

  • #10 — which already removes the buttons from the header; this issue supersedes its "put them on the dashboard".
  • #20 — filters on the Listings list.
  • #4 — reminders from closing dates; #12, #17, #18 — listings in the API and the badge.

Open questions

  1. Should Add a listing accept a bare URL and title with nothing else, so noting a posting takes ten seconds and the details come later? Proposal: yes; a listing can be thin, an application cannot.
  2. Keep discarded listings forever, or let Insights count them and purge after a configurable time? Proposal: keep; they are small and they are the person's history.
  3. Should a shortlisted listing be allowed several applications (the same posting applied to twice, months apart)? The posting-to-application relation is already one-to-many, so nothing prevents it; the question is only whether the interface allows it.
## Observation > i don't like the "Capture" "Record" overwhelming the navbar. i want to integrate those two operations in a single dash named "Offers" or something similar and from them, the job applicant decides which ones to pass to the applications phase ## What exists today - The header carries two buttons on every page, *Capture* and *Record* (`base.html`, lines 58–63). #10 takes them out of the header and parks them on the dashboard. This issue decides where they actually go, and it is not the dashboard. - **Two ways in, and both end in an application.** *Record an application* is one form — `ApplicationIntakeForm` — that creates the company, the posting and the application together (its docstring: "an application is almost always entered while looking at a posting"). *Capture* turns a URL or pasted HTML into a `Capture` holding parsed data, which waits on the captures page (`jobs:capture_list`, linked from the dashboard as *Captures awaiting review*); *Review* opens the same intake form pre-filled and, on save, creates the application and links the capture to it (`jobs/capture_views.py`, lines 134–197; the wiki says it plainly: "Accepting one creates the application and links the two"). - `JobPosting` is already a full model — company, title, location, remote and employment type, URL, description, salary, posted, closing and closed dates — with create, detail, edit and delete views. But it has **no list page, and no way to be created from the interface on its own**: `jobs:posting_create` is linked from no template. A posting only ever exists behind an application or on a company page. - Nothing records "I saw this and am thinking about it". The nearest thing is an application in `Status.DRAFT`, which is a different statement: *I have decided, and I am preparing*. In short, Postulo models the moment *after* the decision. The observation asks for the moment before it. ## Shape **Listings is the stage before Applications**: every posting the person has noticed, however it arrived, until they decide. 1. **Navigation.** *Listings* sits between *Dashboard* and *Applications*. *Capture* and *Record* leave the header (#10) and become the two actions of the Listings page: **Capture** (a URL or pasted page, unchanged) and **Add a listing** (the posting half of today's intake form: company and posting, no application). Both land in Listings. 2. **A state on the posting.** `new` on arrival; `shortlisted` or `discarded` by the person; `applied` derived from the existence of an application, never set by hand; `closed` derived from `closed_at` or a `closes_at` in the past. Plus `noted_at` (when it entered Listings) and `decided_at`. The Listings page shows new and shortlisted by default, with filters for discarded and closed (#20's header filters apply). 3. **The decision.** **Apply** on a listing opens the application half of the intake form — applied date, channel, CV, letter, contacts — creates the application and moves the listing out of the list; the application then follows the existing pipeline, `draft` included, which regains its proper meaning: *preparing*. **Discard** takes an optional reason (not for me, pay, location, closed, other) and keeps the record: what a person passed on, and why, is data — *Insights* gains selectivity (listings seen against applications made) and "which sources produce listings I actually apply to". A discarded listing can be restored. 4. **Capture review changes its ending.** A review lands in Listings as a new listing instead of creating an application. The review screen keeps its correction step — that is what makes captures safe — but its form is the posting half, with one checkbox, *I have already applied*, that keeps the one-step path for the most common case: recording after the fact. Captures awaiting review appear as a strip at the top of the Listings page; the captures page folds into it and `jobs:capture_list` redirects there. 5. **Record an application stays**, as the one-step action on the *Applications* page, implemented as "add a listing, then apply" so the data is identical whichever door was used. 6. **Dashboard.** *Listings to decide on* (with *closing soon* from `closes_at`) replaces *Captures awaiting review*. A listing's closing date can raise a reminder, through #4, without any new machinery. 7. **API and extensions.** The capture API's surface does not change: `POST /captures` and `review_url` keep working, only the page a review ends on differs. #12's read API gains listings, and the "already tracked" badge in #17 and #18 covers listings as well as applications, which is the case where it is most useful. 8. **Board.** Unchanged: applications by status. Listings may deserve a small board of its own (new / shortlisted) later; a list with filters comes first. 9. **Export and import.** The state fields join the posting section; `FORMAT_VERSION` becomes 2 and the importer keeps accepting 1, deriving `applied` where an application exists. 10. **Migration.** Every existing posting has an application, so every one becomes `applied` by derivation. No row changes meaning. 11. **Documentation.** *Capturing postings* and *Tracking applications* are rewritten around Listings → Applications; `docs/PLAN.md` gains the stage in the pipeline description. `seed_demo` gets a handful of undecided and discarded listings so the page is not empty on first sight. 12. **Duplicate detection.** Capturing or adding a listing whose URL — normalised: scheme and host folded, tracking parameters stripped — matches a listing or application the person already has says so ("already in your listings since 12 May") and offers to open it instead of creating a twin. The capture API answers the same way in its response, so #17 and #18 can show it in the extension before anything is sent. ## The name **Listings**, decided on 2026-09-05. It is the one-word form of "job listing"; it collides with nothing in the interface — the pipeline stage *Offer* (`Status.OFFER`, event *Offer received*) keeps its one meaning, an employer's offer at the end of the process — and it has standard equivalents in French (*annonces*) and Portuguese (*anúncios*). Considered and set aside: *Offers*, the observation's original word, because of that collision; *Vacancies* (formal); *Openings* (no French equivalent); *Postings* (American in flavour); *Jobs* (reads as employment history beside the career record). In code the model stays `JobPosting`; URL names, views and templates say `listing`. ## Classification Enhancement. A workflow change, and therefore a paragraph in the release notes, but not a breaking change: the API surface is unchanged, existing data migrates by derivation, the old captures URL redirects, and no operator action is required. ## Depends on / relates to - #10 — which already removes the buttons from the header; this issue supersedes its "put them on the dashboard". - #20 — filters on the Listings list. - #4 — reminders from closing dates; #12, #17, #18 — listings in the API and the badge. ## Open questions 1. Should *Add a listing* accept a bare URL and title with nothing else, so noting a posting takes ten seconds and the details come later? Proposal: yes; a listing can be thin, an application cannot. 2. Keep discarded listings forever, or let *Insights* count them and purge after a configurable time? Proposal: keep; they are small and they are the person's history. 3. Should a shortlisted listing be allowed several applications (the same posting applied to twice, months apart)? The posting-to-application relation is already one-to-many, so nothing prevents it; the question is only whether the interface allows it.
tiagoagueda added this to the 0.2.0 milestone 2026-09-05 12:58:41 +00:00
tiagoagueda changed title from Offers: one place where postings arrive, captured or typed, and are triaged before any becomes an application to Listings: one place where postings arrive, captured or typed, and are triaged before any becomes an application 2026-09-05 13:41:54 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#25
No description provided.