Polish README and point Network tab at GitHub
CI / skip-ci-check (pull_request) Successful in 38s
CI / secret-scan (pull_request) Successful in 28s
CI / node-ci (pull_request) Successful in 1m46s

This commit is contained in:
Builder
2026-08-05 14:59:19 -04:00
parent bc3124a426
commit 5e98765033
4 changed files with 30 additions and 67 deletions
+9 -37
View File
@@ -1,42 +1,18 @@
# portfolio.spec.ts # portfolio.spec.ts
> A personal portfolio + resume styled as a **Playwright test runner**. Built by an SDET, for SDETs. [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
> [![Live](https://img.shields.io/badge/live-sdet.levkin.ca-0a0)](https://sdet.levkin.ca)
> Gitea remote: `ilia/sdetProfile` (local folder is usually cloned as `portfolio`).
Click the green ▶ next to any test to "run" it — each passing test reveals a portfolio section. Filter by `@tag` chips like a real `--grep`. Includes a career-timeline trace viewer, a Source tab that renders the portfolio as actual-looking Playwright spec code, a **Network** tab that lists public [git.levkin.ca](https://git.levkin.ca/explore/repos) repos as Playwright-style `GET … 200` rows with expandable JSON (descriptions from the Gitea API or each repo's README), and a downloadable PDF resume. Career portfolio styled as a **Playwright test runner** — sidebar explorer, editor tabs, trace viewer, Network tab, keyboard shortcuts.
**Live preview:** deploy locally (see below) or open `index.html` directly. **Live:** [sdet.levkin.ca](https://sdet.levkin.ca) · **Public mirror:** [github.com/Gitilia/sdetProfile](https://github.com/Gitilia/sdetProfile)
---
## Why this exists ## Why this exists
A traditional portfolio doesn't tell a hiring manager you live in test runners all day. This one does — every interaction is a love letter to the tooling SDETs use: Hiring managers who work with SDETs already live in test runners. This site uses
that UI metaphor instead of a generic landing page: sidebar explorer, editor tabs,
- Sidebar **Test Explorer** (400px) with collapsible suites, green run arrows, and tree-view ellipsis for long describe labels status pill, trace viewer, network panel, and keyboard shortcuts modeled on
- **Editor tab strip** (28px, slim) above the report — switch between `portfolio.spec.ts`, `projects.spec.ts`, `skills.spec.ts`, `playground.spec.ts`; each rescopes the explorer, report, source view, and status counts (cookie-persisted) Playwright and VS Code.
- **Status pill** merged into the editor bar with live pass/fail/skip counts per active spec
- **Overflow menu** (⋯) on the right of the editor tabs with `--workers=` and `--headed` controls
- **Skipped test** (`should meet performance budget`) with amber ⊘ icon — a 100% green suite looks fake; mixed outcomes feel authentic
- Progress bars under each test that fill in real time
- **Auto-run** first test on page load so the hero arrives with body content rendered (runner animation still plays)
- **Summary stripe** above the results list: `Last run · 1.4s · 4 passed · 1 skipped`
- **Pending preview** — the first idle test's body is shown expanded with a faded look + "click ▶ to run" overlay
- **Tag bar** with 6 visible tags + "+N more" expand chip + a `× clear` button
- **Count bubbles** right-aligned with consistent width, tabular numerals, outlined — reads as "metric" not "tag"
- VS Code dark+ / Playwright trace viewer palette (Inter + JetBrains Mono)
- A `Trace` tab that draws your career as a Gantt-style waterfall
- A `Source` tab that renders the active spec as a real-looking `.spec.ts` file
- A `Console` tab that logs test run events live
- A `Network` tab modeled on Playwright's network panel — one row per public Gitea repo (`GET https://git.levkin.ca/api/v1/repos/…`), click to expand the faux JSON body
- `--headed` toggle that slows animations down for demo mode
- `--workers=` selector for parallel test execution (1, 2, or 4 workers)
- Cookie-persisted theme cycle: dark → light → high-contrast (WCAG AAA)
- Keyboard shortcuts (`?` to see all): `R` run all, `X` reset, `T` theme, `/` grep, `1–9` run Nth test
- Mobile drawer for the test explorer, fully responsive
---
## Stack ## Stack
@@ -308,8 +284,4 @@ See [IDEAS.md](IDEAS.md) for the backlog. Quick wins on top, ambitious experimen
## License ## License
MIT. Fork it, restyle it, replace the content with your own. If you ship a variation, a wave hello on LinkedIn would be appreciated but is not required. MIT — see [LICENSE](LICENSE). Fork and replace the content as you like.
---
**Built by** [Ilia Dobkin](https://www.linkedin.com/in/ilia-dobkin-8263343/) · Senior SDET · Remote (ET)
+2 -2
View File
@@ -186,11 +186,11 @@
<div class="source" id="source"></div> <div class="source" id="source"></div>
</div> </div>
<!-- Network (Gitea repos as Playwright-style requests) --> <!-- Network (public GitHub repos as Playwright-style requests) -->
<div class="pane" id="pane-network" role="tabpanel"> <div class="pane" id="pane-network" role="tabpanel">
<div class="network-pane"> <div class="network-pane">
<div class="network-head"> <div class="network-head">
<div class="network-head__title">Network · git.levkin.ca</div> <div class="network-head__title">Network · github.com/Gitilia</div>
<div class="network-head__meta"><span class="legend"><i class="sw sw--pass"></i> 200</span><span class="legend mono">application/json</span></div> <div class="network-head__meta"><span class="legend"><i class="sw sw--pass"></i> 200</span><span class="legend mono">application/json</span></div>
</div> </div>
<div class="network-scroll" id="gitea-network" data-repo-list="1"></div> <div class="network-scroll" id="gitea-network" data-repo-list="1"></div>
+1 -1
View File
@@ -677,7 +677,7 @@
} }
const GF = window.PORTFOLIO; const GF = window.PORTFOLIO;
const repos = GF && Array.isArray(GF.giteaRepos) ? GF.giteaRepos : []; const repos = GF && Array.isArray(GF.giteaRepos) ? GF.giteaRepos : [];
const apiRoot = 'https://git.levkin.ca/api/v1/repos/'; const apiRoot = 'https://api.github.com/repos/';
if (repos.length === 0) { if (repos.length === 0) {
wrap.innerHTML = `<div class="network-empty">No <span class="mono">giteaRepos</span> in <span class="mono">js/data.js</span>. Hard refresh the page (<span class="mono">Cmd+Shift+R</span>). If deploying, redeploy after adding the array.</div>`; wrap.innerHTML = `<div class="network-empty">No <span class="mono">giteaRepos</span> in <span class="mono">js/data.js</span>. Hard refresh the page (<span class="mono">Cmd+Shift+R</span>). If deploying, redeploy after adding the array.</div>`;
return; return;
+18 -27
View File
@@ -8,7 +8,8 @@ window.PORTFOLIO = {
workAuth: "Canadian citizen", workAuth: "Canadian citizen",
email: "idobkin@gmail.com", email: "idobkin@gmail.com",
linkedin: "https://www.linkedin.com/in/ilia-dobkin-8263343/", linkedin: "https://www.linkedin.com/in/ilia-dobkin-8263343/",
gitea: "https://git.levkin.ca", gitea: "https://github.com/Gitilia",
github: "https://github.com/Gitilia",
site: "https://sdet.levkin.ca", site: "https://sdet.levkin.ca",
blurb: blurb:
"Senior SDET with 20+ years in audit/financial software and regulated web, including real-money iGaming. Deep across Playwright, Swagger/OpenAPI contract testing, and performance baselines integrated into CI/CD. I treat automation as a personal discipline as much as a job: scripts, shortcuts, and agents that streamline my day so engineering effort goes where it matters. Strong instinct for stabilizing flaky suites, tightening quality gates, and removing manual regression effort wherever it lives.", "Senior SDET with 20+ years in audit/financial software and regulated web, including real-money iGaming. Deep across Playwright, Swagger/OpenAPI contract testing, and performance baselines integrated into CI/CD. I treat automation as a personal discipline as much as a job: scripts, shortcuts, and agents that streamline my day so engineering effort goes where it matters. Strong instinct for stabilizing flaky suites, tightening quality gates, and removing manual regression effort wherever it lives.",
@@ -185,7 +186,7 @@ window.PORTFOLIO = {
tags: ["@api","@perf"], tags: ["@api","@perf"],
duration: 0, duration: 0,
steps: [ steps: [
{ kind: "ok", title: 'navigate to Gitea API endpoint', dur: 45 }, { kind: "ok", title: 'navigate to GitHub API endpoint', dur: 45 },
{ kind: "ok", title: 'send GET /api/v1/repos/ilia/portfolio', dur: 62 }, { kind: "ok", title: 'send GET /api/v1/repos/ilia/portfolio', dur: 62 },
{ kind: "ok", title: 'assert status 200', dur: 8 }, { kind: "ok", title: 'assert status 200', dur: 8 },
{ kind: "fail", title: 'expect(latency).toBeLessThan(200)', dur: 347 }, { kind: "fail", title: 'expect(latency).toBeLessThan(200)', dur: 347 },
@@ -377,30 +378,20 @@ window.PORTFOLIO = {
], ],
/** /**
* Public repos on git.levkin.ca — descriptions from Gitea API when set, * Public repos on github.com/Gitilia — mirrored from Gitea for recruiters.
* otherwise first paragraph of README (see scripts/fetch-gitea-repos.mjs). * Keep this list to public GitHub only (no private forge links).
* API lists all 19 repos on one page (explore UI may paginate).
*/ */
giteaRepos: [ giteaRepos: [
{ full_name: "ilia/ansible", name: "ansible", html_url: "https://git.levkin.ca/ilia/ansible", language: "Makefile", description: "Ansible automation for development machines, service hosts, and Proxmox-managed guests (LXC-first, with a path for KVM VMs)." }, { full_name: "Gitilia/playkit", name: "playkit", html_url: "https://github.com/Gitilia/playkit", language: "TypeScript", description: "Shared Playwright + API test helpers — UI actions, API client, timings, metrics." },
{ full_name: "ilia/AtAnyRate", name: "AtAnyRate", html_url: "https://git.levkin.ca/ilia/AtAnyRate", language: "Python", description: "Local Python application that identifies upcoming Toronto events likely to increase Airbnb demand, sends Telegram alerts, and optionally adjusts nightly prices via Playwright automation." }, { full_name: "Gitilia/context-extractor", name: "context-extractor", html_url: "https://github.com/Gitilia/context-extractor", language: "JavaScript", description: "Browser extension + Playwright package that dumps AI-ready page markdown, console, and network noise." },
{ full_name: "ilia/atlas", name: "atlas", html_url: "https://git.levkin.ca/ilia/atlas", language: "Python", description: "Atlas is a local, privacy-focused home voice agent system — planning, architecture documentation, and kanban tickets for building the system." }, { full_name: "Gitilia/PaperPod", name: "PaperPod", html_url: "https://github.com/Gitilia/PaperPod", language: "Python", description: "Offline overhead video → named PDFs for Paperless (CV + OCR)." },
{ full_name: "ilia/crkl", name: "crkl", html_url: "https://git.levkin.ca/ilia/crkl", language: "Kotlin", description: "Privacy-first Android AI assistant — circle or touch any element on-screen; on-device AI transcribes, summarizes, explains, or drafts responses." }, { full_name: "Gitilia/SwipeAnything", name: "SwipeAnything", html_url: "https://github.com/Gitilia/SwipeAnything", language: "TypeScript", description: "Local-first swipe-to-triage UI with pluggable adapters." },
{ full_name: "ilia/dotfiles", name: "dotfiles", html_url: "https://git.levkin.ca/ilia/dotfiles", language: "", description: "Dotfiles and shell configuration for dev machines." }, { full_name: "Gitilia/maCopy", name: "maCopy", html_url: "https://github.com/Gitilia/maCopy", language: "TypeScript", description: "Native macOS menu-bar clipboard manager (Tauri + React)." },
{ full_name: "ilia/hilitehero", name: "hilitehero", html_url: "https://git.levkin.ca/ilia/hilitehero", language: "Python", description: "Python tool for extracting highlighted text from PDFs with precise ordering and hyphenation handling." }, { full_name: "Gitilia/slack-sieve", name: "slack-sieve", html_url: "https://github.com/Gitilia/slack-sieve", language: "Python", description: "Sieve a Slack workspace export before Mattermost import." },
{ full_name: "ilia/invoice", name: "invoice", html_url: "https://git.levkin.ca/ilia/invoice", language: "JavaScript", description: "CLI for generating professional PDF invoices from JSON — interactive and non-interactive modes with preview-first workflow." }, { full_name: "Gitilia/sdetProfile", name: "sdetProfile", html_url: "https://github.com/Gitilia/sdetProfile", language: "JavaScript", description: "Career portfolio styled as a Playwright test runner." },
{ full_name: "ilia/Jobber", name: "Jobber", html_url: "https://git.levkin.ca/ilia/Jobber", language: "TypeScript", description: "Self-hosted job search orchestration — discover roles, score fit, draft resumes and cover letters, export PDFs, track email; you submit applications yourself." }, { full_name: "Gitilia/portfolio", name: "portfolio", html_url: "https://github.com/Gitilia/portfolio", language: "JavaScript", description: "Personal hub for iliadobkin.com — projects, demos, consulting lanes." },
{ full_name: "ilia/kanban", name: "kanban", html_url: "https://git.levkin.ca/ilia/kanban", language: "", description: "Kanban board project on self-hosted Gitea." }, { full_name: "Gitilia/levkin.ca", name: "levkin.ca", html_url: "https://github.com/Gitilia/levkin.ca", language: "HTML", description: "Levkin Inc. public homepage — static stack site." },
{ full_name: "ilia/linkedout", name: "linkedout", html_url: "https://git.levkin.ca/ilia/linkedout", language: "JavaScript", description: "Job market intelligence platform with integrated AI-powered insights — modular architecture for extensibility." }, { full_name: "Gitilia/mirror_match", name: "mirror_match", html_url: "https://github.com/Gitilia/mirror_match", language: "TypeScript", description: "Photo guessing game — upload photos, others guess who is in the picture for points." },
{ full_name: "ilia/llm_council", name: "llm_council", html_url: "https://git.levkin.ca/ilia/llm_council", language: "Python", description: "Local web UI like ChatGPT but sends each query to multiple LLMs — your \"LLM council\" votes with diverse models." },
{ full_name: "ilia/mirror_match", name: "mirror_match", html_url: "https://git.levkin.ca/ilia/mirror_match", language: "TypeScript", description: "Photo guessing game — upload photos, others guess who is in the picture for points. Next.js, PostgreSQL, NextAuth." },
{ full_name: "ilia/nanobot", name: "nanobot", html_url: "https://git.levkin.ca/ilia/nanobot", language: "Python", description: "Ultra-lightweight personal AI assistant (Python; published on PyPI as nanobot-ai)." },
{ full_name: "ilia/onboarding", name: "onboarding", html_url: "https://git.levkin.ca/ilia/onboarding", language: "Shell", description: "Developer environment setup — automates 60+ apps and tools plus Git and SSH configuration." },
{ full_name: "ilia/outreach", name: "outreach", html_url: "https://git.levkin.ca/ilia/outreach", language: "JavaScript", description: "Node.js email outreach for campaigns to law firms — templates, tracking, and tests." },
{ full_name: "ilia/POTE", name: "POTE", html_url: "https://git.levkin.ca/ilia/POTE", language: "Python", description: "Research-oriented tool for tracking and analyzing public stock trades by government officials." },
{ full_name: "ilia/profile", name: "profile", html_url: "https://git.levkin.ca/ilia/profile", language: "TypeScript", description: "Profile / personal site — TypeScript." },
{ full_name: "ilia/punimtag", name: "punimtag", html_url: "https://git.levkin.ca/ilia/punimtag", language: "TypeScript", description: "Modern photo management and facial recognition system." },
{ full_name: "ilia/resume", name: "resume", html_url: "https://git.levkin.ca/ilia/resume", language: "", description: "Résumé generator based on the best-resume-ever project." }
] ]
}; };
@@ -482,7 +473,7 @@ function renderProjects(){
${_tags(p.tags)} ${_tags(p.tags)}
</div>`).join('') </div>`).join('')
}</div> }</div>
<p class="projects-foot">Public code on <a href="https://git.levkin.ca/explore/repos" target="_blank" rel="noopener">git.levkin.ca</a> — open the <button type="button" class="tab-link" data-tab="network">Network</button> tab for an API-style request list.</p> <p class="projects-foot">Public code on <a href="https://github.com/Gitilia" target="_blank" rel="noopener">github.com/Gitilia</a> — open the <button type="button" class="tab-link" data-tab="network">Network</button> tab for an API-style request list.</p>
</div>`; </div>`;
} }
@@ -537,7 +528,7 @@ function renderContact(){
return `<div class="block"><div class="contact-grid"> return `<div class="block"><div class="contact-grid">
<div class="contact-cell"><label>email</label><a href="mailto:${p.email}">${p.email}</a></div> <div class="contact-cell"><label>email</label><a href="mailto:${p.email}">${p.email}</a></div>
<div class="contact-cell"><label>linkedin</label><a href="${p.linkedin}" target="_blank" rel="noopener">in/idobkin</a></div> <div class="contact-cell"><label>linkedin</label><a href="${p.linkedin}" target="_blank" rel="noopener">in/idobkin</a></div>
<div class="contact-cell"><label>gitea (self-hosted)</label><a href="${p.gitea}" target="_blank" rel="noopener">git.levkin.ca</a></div> <div class="contact-cell"><label>github</label><a href="${p.github || p.gitea}" target="_blank" rel="noopener">github.com/Gitilia</a></div>
<div class="contact-cell"><label>site</label><a href="${p.site}" target="_blank" rel="noopener">sdet.levkin.ca</a></div> <div class="contact-cell"><label>site</label><a href="${p.site}" target="_blank" rel="noopener">sdet.levkin.ca</a></div>
<div class="contact-cell"><label>location</label>${_esc(p.location)}${p.workAuth ? ` · ${_esc(p.workAuth)}` : ''}</div> <div class="contact-cell"><label>location</label>${_esc(p.location)}${p.workAuth ? ` · ${_esc(p.workAuth)}` : ''}</div>
</div></div>`; </div></div>`;
@@ -552,7 +543,7 @@ function renderPerfBudget(){
function renderResponseTime(){ function renderResponseTime(){
return `<div class="block"> return `<div class="block">
<p>The Gitea API endpoint <code>GET /api/v1/repos/ilia/portfolio</code> exceeded the <strong>200 ms</strong> latency budget three times in a row.</p> <p>The GitHub API endpoint <code>GET /repos/Gitilia/portfolio</code> exceeded the <strong>200 ms</strong> latency budget three times in a row.</p>
<h4>What happened</h4> <h4>What happened</h4>
<ul> <ul>
<li>Initial request returned in <strong>347 ms</strong> — likely a cold-start on the VPS.</li> <li>Initial request returned in <strong>347 ms</strong> — likely a cold-start on the VPS.</li>