Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ node_modules/
dist/
*.log
.DS_Store
assets/web/screenshots/dashboard.png
assets/web/screenshots/reject.png
19 changes: 4 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,30 +78,19 @@ Prefer a screen to a terminal? **`amt serve`** opens a local dashboard at
`http://localhost:4400` — triage the day's arrivals like a stack of files.

<p align="center">
<img src="assets/web/screenshots/hero.png" width="900" alt="The amt dashboarda paper-styled inbox of ranked job postings with a reject dialog">
<img src="assets/web/screenshots/dashboard.png" width="820" alt="The amt dashboard: a paper-styled inbox of ranked job postings with accept/reject/favorite actions">
</p>

- **📥 Inbox** — the day's new postings, ranked; **⭐ favorite**, **accept → shortlist**,
or **reject** (with a reason) right from the row. Rejected/accepted rows leave
the inbox as you clear it.
- **One original-posting click** — each row opens the real ad in a new tab.

<p align="center">
<img src="assets/web/screenshots/board.png" width="820" alt="The Aktenboard: every note, filterable by status, work mode, city, score, favorites, or text">
</p>

- **Aktenboard** — every note, filterable by status, remote/hybrid, city, minimum
score, favorites, or free text; the header stat cards are shortcuts into it.

<p align="center">
<img src="assets/web/screenshots/detail.png" width="820" alt="A job detail page with the full description, assessment, and triage actions">
</p>

- **Detail** — the full description and your assessment, with the same triage actions.
score, favorites, or free text. The header cards are shortcuts into it.
- **One original-posting click** — each row opens the real ad in a new tab.

It's server-rendered (htmx, no build step, no framework), and **every click
writes straight to your markdown notes** — so Obsidian and the dashboard never
disagree. The imagery above is generated from a fictional demo dataset by
disagree. The screenshot is generated from a fictional demo dataset by
`pnpm shoot` ([scripts/shoot.mjs](./scripts/shoot.mjs)); design notes in
[docs/webview-plan.md](./docs/webview-plan.md).

Expand Down
Binary file removed assets/web/screenshots/board.png
Binary file not shown.
Binary file added assets/web/screenshots/dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed assets/web/screenshots/detail.png
Binary file not shown.
Binary file removed assets/web/screenshots/hero.png
Binary file not shown.
62 changes: 3 additions & 59 deletions scripts/shoot.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,6 @@ function waitForServer() {
})
}

const pngSize = name => {
const h = readFileSync(join(SHOTS, `${name}.png`)).subarray(16, 24)
return { width: h.readUInt32BE(0), height: h.readUInt32BE(4) }
}
const inline = name => `data:image/png;base64,${readFileSync(join(SHOTS, `${name}.png`)).toString('base64')}`

async function run() {
mkdirSync(SHOTS, { recursive: true })
const home = await seedDemo()
Expand All @@ -92,66 +86,16 @@ async function run() {
try {
await waitForServer()
const browser = await chromium.launch()
const ctx = await browser.newContext({ viewport: { width: 1200, height: 820 }, deviceScaleFactor: 2, colorScheme: 'light', reducedMotion: 'reduce' })
const ctx = await browser.newContext({ viewport: { width: 1200, height: 900 }, deviceScaleFactor: 2, colorScheme: 'light', reducedMotion: 'reduce' })
const page = await ctx.newPage()

// Full-page windows (opaque — the paper background is part of the look).
await page.goto(`${URL}/`, { waitUntil: 'networkidle' })
await page.screenshot({ path: join(SHOTS, 'dashboard.png'), clip: { x: 0, y: 0, width: 1200, height: 820 } })

await page.goto(`${URL}/jobs?status=new&workMode=remote`, { waitUntil: 'networkidle' })
await page.screenshot({ path: join(SHOTS, 'board.png'), clip: { x: 0, y: 0, width: 1200, height: 640 } })

await page.goto(`${URL}/jobs/nordlicht-senior-vue`, { waitUntil: 'networkidle' })
await page.screenshot({ path: join(SHOTS, 'detail.png'), clip: { x: 0, y: 0, width: 1200, height: 720 } })

// The reject modal as a transparent cut-out: kill the backdrop wash so
// omitBackground can see through to transparency.
await page.goto(`${URL}/`, { waitUntil: 'networkidle' })
await page.addStyleTag({ content: '::backdrop{background:transparent!important}' })
await page.locator('.row .reject').first().click()
await page.waitForSelector('#reject-dialog[open]')
await page.locator('#reject-dialog').screenshot({ path: join(SHOTS, 'reject.png'), omitBackground: true })

await composeHero(ctx)
await page.screenshot({ path: join(SHOTS, 'dashboard.png'), fullPage: true })
await browser.close()
console.log('shots + hero written to assets/web/screenshots/')
console.log('dashboard.png written to assets/web/screenshots/')
} finally {
server.kill('SIGKILL')
rmSync(home, { recursive: true, force: true })
}
}

async function composeHero(ctx) {
const DASH = 640 // rendered width of the dashboard window in the hero
const dScale = DASH / pngSize('dashboard').width
const rScale = dScale * 1.02
const rejectW = Math.round(pngSize('reject').width * rScale)
const html = `<!doctype html><meta charset="utf-8"><style>
*{margin:0;box-sizing:border-box}
html,body{width:1000px;height:640px}
.ground{position:fixed;inset:0;background:
radial-gradient(120% 90% at 50% -25%, #fff 0%, rgb(255 255 255 / 0%) 55%),
linear-gradient(165deg, #ece9e6 0%, #e2ddd3 100%);}
.stage{position:relative;width:1000px;height:640px}
.win{position:absolute;border-radius:14px;overflow:hidden;
box-shadow:0 2px 6px rgb(60 50 40 / 8%), 0 18px 40px rgb(60 50 40 / 14%), 0 50px 90px rgb(60 50 40 / 10%);
outline:1px solid rgb(255 255 255 / 60%);outline-offset:-1px}
.dash{left:70px;top:70px;width:${DASH}px}
.modal{position:absolute;right:78px;bottom:74px;width:${rejectW}px;border-radius:12px;
filter:drop-shadow(0 2px 6px rgb(60 50 40 / 10%)) drop-shadow(0 20px 44px rgb(60 50 40 / 22%));}
img{display:block;width:100%}
</style>
<div class="ground"></div>
<div class="stage">
<div class="win dash"><img src="${inline('dashboard')}"></div>
<img class="modal" src="${inline('reject')}">
</div>`
const page = await ctx.newPage()
await page.setViewportSize({ width: 1000, height: 640 })
await page.setContent(html, { waitUntil: 'load' })
await page.screenshot({ path: join(SHOTS, 'hero.png') })
await page.close()
}

run().catch(e => { console.error(e); process.exit(1) })
Loading