jobless 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -49
- package/package.json +4 -3
- package/src/cli.js +107 -202
- package/src/clip.js +18 -0
- package/src/data.js +106 -105
- package/src/diff.js +103 -0
- package/src/fit.js +41 -0
- package/src/hiring.js +70 -0
- package/src/identity.js +5 -1
- package/src/interactive.js +73 -0
- package/src/lint-cli.js +128 -0
- package/src/lint-screens.js +114 -0
- package/src/lint.js +443 -18
- package/src/profile-screens.js +248 -0
- package/src/reject.js +89 -18
- package/src/screens.js +57 -176
- package/src/style.js +16 -8
- package/src/work-data.js +152 -0
package/src/screens.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
1
|
+
import { copyToClipboard } from './clip.js';
|
|
2
|
+
import { tweetFromReport } from './fit.js';
|
|
3
|
+
import { letterText } from './reject.js';
|
|
4
|
+
export * from './profile-screens.js';
|
|
5
|
+
export * from './lint-screens.js';
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
+
cases,
|
|
8
|
+
claims,
|
|
7
9
|
experience,
|
|
8
10
|
hireMailto,
|
|
9
11
|
lookingFor,
|
|
10
12
|
menu,
|
|
13
|
+
mentorship,
|
|
11
14
|
profile,
|
|
12
|
-
quotes,
|
|
13
15
|
rants,
|
|
14
16
|
signature,
|
|
15
17
|
skills,
|
|
16
18
|
stats,
|
|
17
|
-
work,
|
|
18
19
|
} from './data.js';
|
|
19
20
|
import {
|
|
20
21
|
banner,
|
|
@@ -26,106 +27,26 @@ import {
|
|
|
26
27
|
dim,
|
|
27
28
|
green,
|
|
28
29
|
inner,
|
|
29
|
-
kv,
|
|
30
|
-
link,
|
|
31
|
-
magenta,
|
|
32
30
|
pad,
|
|
33
31
|
print,
|
|
34
|
-
red,
|
|
35
32
|
section,
|
|
36
33
|
wrap,
|
|
37
34
|
yellow,
|
|
38
35
|
} from './style.js';
|
|
39
36
|
|
|
40
|
-
const copyToClipboard = (text) => {
|
|
41
|
-
try {
|
|
42
|
-
if (process.platform === 'darwin') {
|
|
43
|
-
execFileSync('pbcopy', { input: text });
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
if (process.platform === 'win32') {
|
|
47
|
-
execFileSync('clip', { input: text, windowsHide: true });
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
execFileSync('xclip', ['-selection', 'clipboard'], { input: text });
|
|
51
|
-
return true;
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
export const printHeader = () => {
|
|
58
|
-
print(banner());
|
|
59
|
-
blank();
|
|
60
|
-
print(` ${bold(cyan(profile.name))}`);
|
|
61
|
-
print(` ${profile.title}`);
|
|
62
|
-
print(` ${dim(`${profile.location} · ${profile.availability}`)}`);
|
|
63
|
-
blank();
|
|
64
|
-
const statLine = (items) =>
|
|
65
|
-
items.map((item) => `${bold(green(item.value))} ${dim(item.label)}`).join(dim(' '));
|
|
66
|
-
print(` ${statLine(stats.slice(0, 2))}`);
|
|
67
|
-
print(` ${statLine(stats.slice(2))}`);
|
|
68
|
-
blank();
|
|
69
|
-
print(
|
|
70
|
-
` ${link(profile.email, hireMailto)}${dim(' · ')}${link(profile.website, profile.website)}${dim(' · ')}${link(
|
|
71
|
-
profile.github.replace('https://', ''),
|
|
72
|
-
profile.github,
|
|
73
|
-
)}`,
|
|
74
|
-
);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
export const printSignature = () => {
|
|
78
|
-
blank();
|
|
79
|
-
section('PROOF');
|
|
80
|
-
blank();
|
|
81
|
-
print(` ${bold(signature.title)}`);
|
|
82
|
-
blank();
|
|
83
|
-
for (const line of signature.lines) bullet(line);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
37
|
export const printHints = () => {
|
|
87
38
|
blank();
|
|
88
39
|
print(
|
|
89
|
-
` ${bold(green('h'))} hire ${dim('·')} ${bold('1')} proof ${dim('·')} ${bold('l')} lint a posting ${dim('·')} ${bold('q')} quit`,
|
|
40
|
+
` ${bold(green('h'))} hire ${dim('·')} ${bold('1')} proof ${dim('·')} ${bold('l')} lint a posting ${dim('·')} ${bold('c')} copy letter ${dim('·')} ${bold('q')} quit`,
|
|
90
41
|
);
|
|
91
42
|
};
|
|
92
43
|
|
|
93
|
-
export const printWho = () => {
|
|
94
|
-
blank();
|
|
95
|
-
section('WHO');
|
|
96
|
-
blank();
|
|
97
|
-
for (const paragraph of about) {
|
|
98
|
-
bullet(paragraph);
|
|
99
|
-
blank();
|
|
100
|
-
}
|
|
101
|
-
kv('Now', profile.currently);
|
|
102
|
-
kv('Study', profile.study);
|
|
103
|
-
kv('Site', link(profile.website, profile.website));
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
export const printLooking = () => {
|
|
107
|
-
blank();
|
|
108
|
-
section('WHAT I WANT');
|
|
109
|
-
blank();
|
|
110
|
-
print(` ${dim('roles')}`);
|
|
111
|
-
for (const role of lookingFor.roles) bullet(role, green);
|
|
112
|
-
blank();
|
|
113
|
-
print(` ${dim('setup')}`);
|
|
114
|
-
for (const item of lookingFor.setup) bullet(item);
|
|
115
|
-
blank();
|
|
116
|
-
print(` ${dim('yes')}`);
|
|
117
|
-
for (const item of lookingFor.wants) bullet(item);
|
|
118
|
-
blank();
|
|
119
|
-
print(` ${dim('hard no')}`);
|
|
120
|
-
for (const item of lookingFor.no) bullet(item, yellow);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
44
|
export const printRant = () => {
|
|
124
45
|
blank();
|
|
125
46
|
section('THE MARKET');
|
|
126
47
|
blank();
|
|
127
48
|
for (const line of wrap(
|
|
128
|
-
'
|
|
49
|
+
'A hiring incident report, with coping mechanisms.',
|
|
129
50
|
)) {
|
|
130
51
|
print(` ${dim(line)}`);
|
|
131
52
|
}
|
|
@@ -138,96 +59,29 @@ export const printRant = () => {
|
|
|
138
59
|
print(` ${dim('If you want the work, press')} ${bold('1')}${dim('. If you want to hire, press')} ${bold(green('h'))}${dim('.')}`);
|
|
139
60
|
};
|
|
140
61
|
|
|
141
|
-
export const
|
|
142
|
-
const { findings, source, errors, warnings, letter, ok } = report;
|
|
143
|
-
|
|
144
|
-
blank();
|
|
145
|
-
section('JOBLESS LINT');
|
|
146
|
-
blank();
|
|
147
|
-
print(` ${dim(source)}`);
|
|
148
|
-
blank();
|
|
149
|
-
|
|
150
|
-
if (ok && !findings.length) {
|
|
151
|
-
print(` ${green('clean')} this posting might be a real job.`);
|
|
152
|
-
blank();
|
|
153
|
-
print(` ${dim('If you still want the engineer who wrote the linter:')} ${bold(green('h'))}`);
|
|
154
|
-
return report;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
for (const finding of findings) {
|
|
158
|
-
const tag = finding.level === 'error' ? red('error') : yellow('warn ');
|
|
159
|
-
print(` ${tag} ${finding.message}`);
|
|
160
|
-
print(` ${dim(finding.id)}`);
|
|
161
|
-
blank();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
printCompanyReject(letter);
|
|
165
|
-
blank();
|
|
166
|
-
print(
|
|
167
|
-
` ${errors} error${errors === 1 ? '' : 's'} ${warnings} warning${warnings === 1 ? '' : 's'} ${dim('·')} ${dim('hire the person who wrote the compiler:')} ${bold(green('h'))}`,
|
|
168
|
-
);
|
|
169
|
-
return report;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
export const printWork = () => {
|
|
173
|
-
printSignature();
|
|
62
|
+
export const printTweet = (report) => {
|
|
174
63
|
blank();
|
|
175
|
-
print(
|
|
64
|
+
print(tweetFromReport(report));
|
|
176
65
|
blank();
|
|
177
|
-
|
|
178
|
-
print(` ${bold(job.role)}`);
|
|
179
|
-
print(` ${cyan(job.company)} ${dim(`${job.dates} · ${job.place}`)}`);
|
|
180
|
-
blank();
|
|
181
|
-
for (const point of job.points) bullet(point);
|
|
182
|
-
print(` ${dim(job.stack.join(' · '))}`);
|
|
183
|
-
blank();
|
|
184
|
-
}
|
|
185
|
-
print(` ${dim('selected work')}`);
|
|
186
|
-
blank();
|
|
187
|
-
for (const item of work) {
|
|
188
|
-
print(` ${bold(item.title)} ${dim(item.company)}`);
|
|
189
|
-
for (const line of wrap(item.blurb)) print(` ${line}`);
|
|
190
|
-
blank();
|
|
191
|
-
}
|
|
192
|
-
print(` ${dim('people who were not bots')}`);
|
|
193
|
-
blank();
|
|
194
|
-
for (const quote of quotes) {
|
|
195
|
-
print(` ${magenta(`"${quote.text}"`)}`);
|
|
196
|
-
print(` ${dim(`— ${quote.name}${quote.role ? `, ${quote.role}` : ''}`)}`);
|
|
197
|
-
blank();
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
export const printSkills = () => {
|
|
202
|
-
blank();
|
|
203
|
-
section('STACK');
|
|
204
|
-
blank();
|
|
205
|
-
for (const group of skills) {
|
|
206
|
-
print(` ${bold(group.name)}`);
|
|
207
|
-
print(` ${group.items.join(dim(' · '))}`);
|
|
208
|
-
blank();
|
|
209
|
-
}
|
|
66
|
+
print(` ${dim('Copy it, remix it, send it to someone waiting for a reply.')}`);
|
|
210
67
|
};
|
|
211
68
|
|
|
212
|
-
export const
|
|
69
|
+
export const printWhy = () => {
|
|
213
70
|
blank();
|
|
214
|
-
section('
|
|
71
|
+
section('WHY');
|
|
215
72
|
blank();
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (copy && process.stdout.isTTY && copyToClipboard(profile.email)) {
|
|
226
|
-
print(` ${green('copied')} ${profile.email} ${dim('to clipboard')}`);
|
|
73
|
+
for (const line of [
|
|
74
|
+
'I got invited to interview and rejected at the same time.',
|
|
75
|
+
'So I turned the hiring process into an npm package.',
|
|
76
|
+
'They asked how I handle rejection. Apparently: semantic versioning.',
|
|
77
|
+
'The second act is a reverse ATS: lint the posting, then reject it yourself.',
|
|
78
|
+
'The hiring simulator is satire. The engineer behind it is real.',
|
|
79
|
+
'Try npx jobless lint --demo. Meet me with npx jobless hire.',
|
|
80
|
+
]) {
|
|
81
|
+
bullet(line);
|
|
227
82
|
}
|
|
228
|
-
print(` ${dim('or')} ${link('open a prefilled email', hireMailto)}`);
|
|
229
83
|
blank();
|
|
230
|
-
print(` ${dim('
|
|
84
|
+
print(` ${dim('Hire is')} ${bold(green('h'))}${dim('.')}`);
|
|
231
85
|
};
|
|
232
86
|
|
|
233
87
|
export const printMenu = () => {
|
|
@@ -245,25 +99,49 @@ export const printMenu = () => {
|
|
|
245
99
|
export const printHelp = () => {
|
|
246
100
|
print(banner());
|
|
247
101
|
blank();
|
|
248
|
-
print(` ${bold('npx jobless')}
|
|
249
|
-
print(` ${bold('npx jobless
|
|
102
|
+
print(` ${bold('npx jobless')} reproduce a hiring race condition`);
|
|
103
|
+
print(` ${bold('npx jobless share --copy')} copy a compact anonymous result`);
|
|
104
|
+
print(` ${bold('npx jobless --record')} repeatable, anonymous, no prompt`);
|
|
105
|
+
print(` ${bold('npx jobless --personalize')} opt in to your local Git name`);
|
|
106
|
+
print(` ${bold('npx jobless lint --demo')} reject a fictional posting`);
|
|
107
|
+
print(` ${bold('npx jobless lint [file]')} lint a job posting or jobs/`);
|
|
250
108
|
print(` ${bold('npx jobless lint --json')} findings + rejection letter`);
|
|
251
|
-
print(` ${bold('npx jobless
|
|
109
|
+
print(` ${bold('npx jobless lint --fix')} unified diff (dry-run)`);
|
|
110
|
+
print(` ${bold('npx jobless lint --write')} write the rewrite to the file`);
|
|
111
|
+
print(` ${bold('npx jobless lint --copy')} copy the company letter`);
|
|
112
|
+
print(` ${bold('npx jobless lint --rules')} print the rule table`);
|
|
113
|
+
print(` ${bold('npx jobless lint --format=github')} CI annotations`);
|
|
114
|
+
print(` ${bold('npx jobless work')} three case studies + timeline`);
|
|
115
|
+
print(` ${bold('npx jobless proof')} the 17-cron write-up`);
|
|
116
|
+
print(` ${bold('npx jobless card')} one-screen hire card`);
|
|
252
117
|
print(` ${bold('npx jobless hire')} email + links`);
|
|
118
|
+
print(` ${bold('npx jobless hire --to=manager')} hiring-manager subject`);
|
|
119
|
+
print(` ${bold('npx jobless vcf')} vCard`);
|
|
120
|
+
print(` ${bold('npx jobless why')} why this exists`);
|
|
121
|
+
print(` ${bold('npx jobless tweet')} the origin story, ready to share`);
|
|
253
122
|
print(` ${bold('npx jobless who')} bio`);
|
|
254
123
|
print(` ${bold('npx jobless looking')} roles`);
|
|
255
124
|
print(` ${bold('npx jobless skills')} stack`);
|
|
256
125
|
print(` ${bold('npx jobless rant')} the market`);
|
|
257
126
|
print(` ${bold('npx jobless --resume')} skip the rejection`);
|
|
127
|
+
print(` ${bold('npx jobless --seed=42')} choose a repeatable scenario`);
|
|
258
128
|
print(` ${bold('npx jobless --json')} machine readable resume`);
|
|
129
|
+
print(` ${bold('npx jobless --no-color')} plain text output`);
|
|
259
130
|
blank();
|
|
260
|
-
print(` ${dim('In the prompt:')} ${bold(green('h'))} ${dim('hires,')} ${bold('l')} ${dim('lints
|
|
131
|
+
print(` ${dim('In the prompt:')} ${bold(green('h'))} ${dim('hires,')} ${bold('l')} ${dim('lints,')} ${bold('c')} ${dim('copies the last result,')} ${bold('q')} ${dim('quits.')}`);
|
|
132
|
+
print(` ${dim('Post the command, not the punchline:')} ${bold('npx jobless')}`);
|
|
261
133
|
print(` ${dim(profile.website)}`);
|
|
262
134
|
};
|
|
263
135
|
|
|
264
136
|
export const printLandingTeaser = () => {
|
|
265
137
|
blank();
|
|
266
|
-
print(` ${dim('Paste
|
|
138
|
+
print(` ${dim('Paste the posting that just rejected you:')} ${bold('l')}${dim('.')}`);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const copyLetter = (letter) => {
|
|
142
|
+
const text = letterText(letter);
|
|
143
|
+
if (!text) return false;
|
|
144
|
+
return copyToClipboard(text);
|
|
267
145
|
};
|
|
268
146
|
|
|
269
147
|
export const toJson = () =>
|
|
@@ -286,6 +164,9 @@ export const toJson = () =>
|
|
|
286
164
|
lookingFor,
|
|
287
165
|
rants,
|
|
288
166
|
stats,
|
|
167
|
+
claims,
|
|
168
|
+
cases,
|
|
169
|
+
mentorship,
|
|
289
170
|
experience,
|
|
290
171
|
skills,
|
|
291
172
|
},
|
package/src/style.js
CHANGED
|
@@ -31,7 +31,7 @@ export const brightRed = paint('91');
|
|
|
31
31
|
|
|
32
32
|
export const width = () => {
|
|
33
33
|
const columns = stdout.columns || 80;
|
|
34
|
-
return Math.min(Math.max(columns,
|
|
34
|
+
return Math.min(Math.max(columns, 10), 86);
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
export const inner = () => width() - 4;
|
|
@@ -46,7 +46,12 @@ export const wrap = (text, max = width() - 8) => {
|
|
|
46
46
|
continue;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const words = paragraph.split(/\s+/)
|
|
49
|
+
const words = paragraph.split(/\s+/).flatMap((word) => {
|
|
50
|
+
const chars = Array.from(word);
|
|
51
|
+
const chunks = [];
|
|
52
|
+
for (let i = 0; i < chars.length; i += max) chunks.push(chars.slice(i, i + max).join(''));
|
|
53
|
+
return chunks;
|
|
54
|
+
});
|
|
50
55
|
let line = '';
|
|
51
56
|
|
|
52
57
|
for (const word of words) {
|
|
@@ -87,11 +92,13 @@ export const clear = () => {
|
|
|
87
92
|
if (stdout.isTTY) stdout.write(`${CSI}2J${CSI}H`);
|
|
88
93
|
};
|
|
89
94
|
|
|
90
|
-
const
|
|
95
|
+
const plainText = (text) =>
|
|
91
96
|
String(text)
|
|
92
97
|
.replace(/\x1b\]8;;[^\x07]*\x07/g, '')
|
|
93
98
|
.replace(/\x1b\]8;;\x07/g, '')
|
|
94
|
-
.replace(/\x1b\[[0-9;]*m/g, '')
|
|
99
|
+
.replace(/\x1b\[[0-9;]*m/g, '');
|
|
100
|
+
|
|
101
|
+
const visibleLength = (text) => Array.from(plainText(text)).length;
|
|
95
102
|
|
|
96
103
|
export const pad = (text, size = inner(), align = 'left') => {
|
|
97
104
|
const length = visibleLength(text);
|
|
@@ -106,10 +113,11 @@ export const pad = (text, size = inner(), align = 'left') => {
|
|
|
106
113
|
|
|
107
114
|
export const box = (lines, { title } = {}) => {
|
|
108
115
|
const max = inner();
|
|
109
|
-
const
|
|
110
|
-
const top = dim(
|
|
111
|
-
const bottom = dim(`└${'─'.repeat(max)}┘`);
|
|
112
|
-
const
|
|
116
|
+
const label = title ? ` ${title} `.slice(0, max) : '';
|
|
117
|
+
const top = dim(`┌─${label}${'─'.repeat(max - label.length + 1)}┐`);
|
|
118
|
+
const bottom = dim(`└${'─'.repeat(max + 2)}┘`);
|
|
119
|
+
const rows = lines.flatMap((line) => visibleLength(line) > max ? wrap(plainText(line), max) : [line]);
|
|
120
|
+
const body = rows.map((line) => `${dim('│')} ${pad(line, max)} ${dim('│')}`);
|
|
113
121
|
return [top, ...body, bottom].join('\n');
|
|
114
122
|
};
|
|
115
123
|
|
package/src/work-data.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export const experience = [
|
|
2
|
+
{
|
|
3
|
+
role: 'Senior Full Stack Engineer',
|
|
4
|
+
company: 'TPK Synergy Limited',
|
|
5
|
+
dates: 'Dec 2025 – Present',
|
|
6
|
+
place: 'Remote',
|
|
7
|
+
points: [
|
|
8
|
+
'Amazon seller analytics: marketplace analytics, FBA, finance, PPC, and inventory.',
|
|
9
|
+
'Diagnosed sync jobs that died after every deploy; shutdown handling across 17 cron processes. See `proof`.',
|
|
10
|
+
'Amazon SP-API and Ads auth end to end: OAuth, LWA, encrypted credentials.',
|
|
11
|
+
'UK and EU marketplaces with VAT-aware margins. Moved expensive aggregations into SQL.',
|
|
12
|
+
],
|
|
13
|
+
stack: ['Node.js', 'PostgreSQL', 'BullMQ', 'Redis', 'React', 'Amazon SP-API'],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
role: 'Senior Software Engineer',
|
|
17
|
+
company: 'Mereb Technologies',
|
|
18
|
+
dates: 'Apr 2022 – Nov 2025',
|
|
19
|
+
place: 'Addis Ababa',
|
|
20
|
+
points: [
|
|
21
|
+
'Owned the production gaming platform end to end in these domains: loyalty, payouts, fraud, reporting — not the whole company.',
|
|
22
|
+
'~30% platform performance: indexing, read/write separation, caching, pagination, redesigned status-checking.',
|
|
23
|
+
'~35% infra cost, about $100K a year: database, cache, and resource optimization. Engineering estimate.',
|
|
24
|
+
'Reports on ~1M-row datasets: 2 hours to under 30 minutes. Mentored 3 juniors on APIs, debugging, and design.',
|
|
25
|
+
],
|
|
26
|
+
stack: ['PostgreSQL', 'gRPC', 'PHP', 'React', 'AWS', 'Datadog'],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
role: 'Full Stack Developer (Contract)',
|
|
30
|
+
company: 'Affiliate.com',
|
|
31
|
+
dates: 'Feb 2023 – Aug 2023',
|
|
32
|
+
place: 'Remote · alongside Mereb',
|
|
33
|
+
points: [
|
|
34
|
+
'Migrated a legacy PHP API to Node.js and tRPC without breaking the React UI.',
|
|
35
|
+
'Reimplemented legacy SQL in Prisma while both platforms ran in parallel.',
|
|
36
|
+
'Cut report generation ~40% by moving it onto Google Cloud Storage. ~85% coverage on migrated code.',
|
|
37
|
+
],
|
|
38
|
+
stack: ['Node.js', 'tRPC', 'Prisma', 'React', 'Zod', 'GCS'],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
role: 'Backend Engineer (Contract)',
|
|
42
|
+
company: 'Autobridge Systems',
|
|
43
|
+
dates: 'Contract',
|
|
44
|
+
place: 'Addis Ababa · government constituent platform',
|
|
45
|
+
points: [
|
|
46
|
+
'Backend for complaints, document approvals, and internal communication, with access control and encryption.',
|
|
47
|
+
'Traced multi-minute email-record loads to missing indexes. Those operations improved more than 10x.',
|
|
48
|
+
],
|
|
49
|
+
stack: ['PostgreSQL', 'Node.js', 'Encryption'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
role: 'Frontend Developer',
|
|
53
|
+
company: 'Eaglelion Systems Technology',
|
|
54
|
+
dates: 'Feb 2021 – Apr 2022',
|
|
55
|
+
place: 'Addis Ababa',
|
|
56
|
+
points: [
|
|
57
|
+
'Client payment workflows on products with 100K+ downloads that have handled 3B+ ETB. I integrated the rails, not the ledger.',
|
|
58
|
+
'Mastercard, Visa, Telebirr, bank APIs: states, callbacks, failures, retries, verification. SOAP→REST against incomplete docs.',
|
|
59
|
+
'GTmetrix 45 → 97. Page load ~25% faster: code splitting, lazy loading, assets.',
|
|
60
|
+
],
|
|
61
|
+
stack: ['React', 'Next.js', 'Tailwind', 'Payment APIs'],
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export const cases = [
|
|
66
|
+
{
|
|
67
|
+
id: 'crons',
|
|
68
|
+
title: 'The deploy that killed 17 cron jobs',
|
|
69
|
+
company: 'TPK Synergy Limited',
|
|
70
|
+
role: 'Senior Full Stack Engineer',
|
|
71
|
+
kind: 'Reliability · private production',
|
|
72
|
+
problem:
|
|
73
|
+
'After every deploy, recurring Amazon sync jobs died quietly. Dashboards looked fine until the data was a day old.',
|
|
74
|
+
trace: [
|
|
75
|
+
'BullMQ job state — work marked active, then gone',
|
|
76
|
+
'ioredis connections closing with the Node process',
|
|
77
|
+
'Node shutdown: no drain, no SIGTERM handler that waited',
|
|
78
|
+
'Railway scheduler: the next tick assumed the last run had finished',
|
|
79
|
+
],
|
|
80
|
+
change: 'Explicit shutdown handling across 17 cron processes so a deploy could not leave work stranded.',
|
|
81
|
+
result: 'Sync jobs survive deploys instead of going silent until a seller notices bad numbers.',
|
|
82
|
+
still:
|
|
83
|
+
'A worker that exits without draining still looks healthy on the dashboard. The failure mode is silence, not a page. That is what I watch for.',
|
|
84
|
+
undo: 'I would put a queue-depth and last-success timestamp on the dashboard the same week. Shutdown handling without a visible lag is how the next silent failure hides.',
|
|
85
|
+
ask: [
|
|
86
|
+
'What did SIGTERM actually do before the patch?',
|
|
87
|
+
'How did you know it was 17 processes, not one flaky job?',
|
|
88
|
+
'What still pages you, and what still does not?',
|
|
89
|
+
],
|
|
90
|
+
stack: ['Node.js', 'BullMQ', 'Redis', 'Railway'],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'reports',
|
|
94
|
+
title: 'Reports that used to take two hours',
|
|
95
|
+
company: 'Mereb Technologies',
|
|
96
|
+
role: 'Senior Software Engineer',
|
|
97
|
+
kind: 'Performance · private production',
|
|
98
|
+
problem: 'Reporting on about a million records ran for two hours and blocked people who needed the numbers.',
|
|
99
|
+
trace: [
|
|
100
|
+
'Ad-hoc application paths doing aggregation the database should do',
|
|
101
|
+
'Missing indexes, no read replica, cache that did not cover the hot report',
|
|
102
|
+
'Status-checking that polled work already in flight',
|
|
103
|
+
],
|
|
104
|
+
change:
|
|
105
|
+
'Indexing, read/write separation, caching, and a redesigned status-checking flow so the database did the aggregation.',
|
|
106
|
+
result:
|
|
107
|
+
'Same reports, under 30 minutes. Platform performance up about 30%. Infra cost down about 35% (~$100K/year, engineering estimate).',
|
|
108
|
+
still: 'I cannot show the queries here. They are in a private repo. I will walk through them on a call.',
|
|
109
|
+
undo: 'I would have split the hottest report into an async job on day one instead of waiting until it was a two-hour meeting blocker.',
|
|
110
|
+
ask: [
|
|
111
|
+
'Which index moved the needle, and how did you measure it?',
|
|
112
|
+
'What did you refuse to cache?',
|
|
113
|
+
'Where does the 30% / $100K estimate come from?',
|
|
114
|
+
],
|
|
115
|
+
stack: ['PostgreSQL', 'PHP', 'Redis', 'AWS'],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: 'nedaj',
|
|
119
|
+
title: 'Nedaj payments',
|
|
120
|
+
company: 'Eaglelion Systems Technology',
|
|
121
|
+
role: 'Frontend Developer',
|
|
122
|
+
kind: 'Shipped product · client rails',
|
|
123
|
+
problem:
|
|
124
|
+
'A fuel-purchase app had to talk to Mastercard, Visa, Telebirr, and commercial banks, with incomplete provider docs and a SOAP past.',
|
|
125
|
+
trace: [
|
|
126
|
+
'I built the client payment workflows: states, callbacks, failures, retries, verification',
|
|
127
|
+
'Not the acquiring bank, not the ledger, not the 3B ETB as a personal metric — the products handled that volume',
|
|
128
|
+
'SOAP replaced with REST against docs that did not match production',
|
|
129
|
+
],
|
|
130
|
+
change: 'Reliable payment states on a product with 100K+ downloads. Live on Google Play.',
|
|
131
|
+
result: 'Payment rails that hold up in production. GTmetrix 45 → 97 on the web surface.',
|
|
132
|
+
still: 'Provider callbacks still fail in ways the docs do not mention. Retries and verification are the job.',
|
|
133
|
+
undo: 'I would have treated provider docs as hostile from the first week and recorded every callback mismatch in one log, not in Slack.',
|
|
134
|
+
ask: [
|
|
135
|
+
'What did you own on the client, and what did you not own?',
|
|
136
|
+
'How did a failed Telebirr callback become a user-visible state?',
|
|
137
|
+
'Why is 3B ETB not your personal metric?',
|
|
138
|
+
],
|
|
139
|
+
stack: ['React', 'Next.js', 'REST', 'Payment APIs'],
|
|
140
|
+
url: 'https://play.google.com/store/apps/details?id=com.eaglelionsystems.nedaj',
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
export const work = cases;
|
|
145
|
+
|
|
146
|
+
export const mentorship = {
|
|
147
|
+
where: 'Mereb Technologies',
|
|
148
|
+
who: '3 junior engineers',
|
|
149
|
+
on: ['API development', 'debugging', 'code quality', 'system design'],
|
|
150
|
+
still: 'They can speak to the reviews. I will not invent a quote for them. Ask me who, and I will name the work.',
|
|
151
|
+
};
|
|
152
|
+
|