Add salary transparency page (#492)
* Move dhall data and types into `/dhall` folder * Reformat salary transparency data into Dhall * Wire up the old salary transparency page with a custom element * Wire up a new salary transparency page * Expose raw data as JSON * Make dhall types more portable * Remove gallery from the navbar * Make signal boost page point to the new data location * Add salary transparency page to the footer of the site * Add site update post for this Signed-off-by: Xe <me@xeiaso.net>
This commit is contained in:
parent
7541df7781
commit
ad6fba4c79
|
@ -3,6 +3,12 @@ title: My Career So Far in Dates/Titles/Salaries
|
|||
date: 2019-03-14
|
||||
---
|
||||
|
||||
<div class="warning"><xeblog-conv name="Cadey" mood="coffee">This post is
|
||||
outdated, see <a href="/salary-transparency">here</a> for more context on why
|
||||
this data is made public. The table on this page will be automatically updated
|
||||
to contain the data on my salary transparency page, but you should prefer that
|
||||
page over this one when possible.</xeblog-conv></div>
|
||||
|
||||
Let this be inspiration to whoever is afraid of trying, failing and being fired.
|
||||
Every single one of these jobs has taught me lessons I've used daily in my
|
||||
career.
|
||||
|
@ -26,21 +32,7 @@ might not want.
|
|||
The following table is a history of my software career by title, date and salary
|
||||
(company names are omitted).
|
||||
|
||||
| Title | Start Date | End Date | Days Worked | Days Between Jobs | Salary | How I Left |
|
||||
|:----- |:---------- |:-------- |:----------- |:----------------- |:------ |:---------- |
|
||||
| Junior Systems Administrator | November 11, 2013 | January 06, 2014 | 56 days | n/a | $50,000/year | Terminated |
|
||||
| Software Engineering Intern | July 14, 2014 | August 27, 2014 | 44 days | 189 days | $35,000/year | Terminated |
|
||||
| Consultant | September 17, 2014 | October 15, 2014 | 28 days | 21 days | $90/hour | Contract Lapsed |
|
||||
| Consultant | October 27, 2014 | Feburary 9, 2015 | 105 days | 12 days | $90/hour | Contract Lapsed |
|
||||
| Site Reliability Engineer | March 30, 2015 | March 7, 2016 | 343 days | 49 days | $125,000/year | Demoted |
|
||||
| Systems Administrator | March 8, 2016 | April 1, 2016 | 24 days | 1 day | $105,000/year | Bad terms |
|
||||
| Member of Technical Staff | April 4, 2016 | August 3, 2016 | 121 days | 3 days | $135,000/year | Bad terms |
|
||||
| Software Engineer | August 24, 2016 | November 22, 2016 | 90 days | 21 days | $105,000/year | Terminated |
|
||||
| Consultant | Feburary 13, 2017 | November 13, 2017 | 273 days | 83 days | don't remember | Hired |
|
||||
| Senior Software Engineer | November 13, 2017 | March 8, 2019 | 480 days | 0 days | $150,000/year | Voulntary quit |
|
||||
| Senior Site Reliability Expert | May 6, 2019 | October 27, 2020 | 540 days | 48 days | CAD$115,000/year (about USD$ 80k and change) | Voluntary quit |
|
||||
| Software Designer | December 14, 2020 | *current* | n/a | n/a | CAD$135,000/year (about USD$ 105k and change) | raise |
|
||||
| Archmage of Infrastructure | March 1, 2022 | *current* | n/a | n/a | CAD$147,150/year (about USD$ 115k and change) | n/a |
|
||||
<xeblog-salary-history></xeblog-salary-history>
|
||||
|
||||
Even though I've been fired three times, I don't regret my career as it's been
|
||||
thus far. I've been able to work on experimental technology integrating into
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
title: "Site Update: Salary Transparency Page Added"
|
||||
date: 2022-06-14
|
||||
author: Sephie
|
||||
---
|
||||
|
||||
<xeblog-hero file="miku-dark-souls" prompt="hatsune miku, elden ring, dark souls, concept art, crowbar"></xeblog-hero>
|
||||
|
||||
I have added a [salary transparency
|
||||
page](https://xeiaso.net/salary-transparency) to the blog. This page lists my
|
||||
salary for every job I've had in tech. I have had this data open to the public
|
||||
for years, but I feel this should be more prominently displayed on my website.
|
||||
|
||||
As someone who has seen pay discrimination work in action first-hand, data is
|
||||
one of the ways that we can end this pointless hiding of information that leads
|
||||
to people being uninformed and hirt by their lack of knowledge. By laying my
|
||||
hand out in the open like this, I hope to ensure that people are better informed
|
||||
about how much money they can make, so that they can be paid equally for equal
|
||||
work.
|
||||
|
||||
Raw, machine processable data (including employer names) is available at
|
||||
`/api/salary_transparency.json`. The JSON format is not stable. Do not treat it as
|
||||
such. I reserve the right to change the formatting or semantics of the JSON
|
||||
format at any time without warning. The raw data is in `/dhall/jobHistory.dhall`
|
||||
in my site's git repository.
|
||||
|
||||
I have also taken the time to make sure that the [old
|
||||
post](https://xeiaso.net/blog/my-career-in-dates-titles-salaries-2019-03-14)
|
||||
maintains an up-to-date list. I do not want to break semantics on my website
|
||||
without a very good reason. By leaving the old post un-updated, I feel it would
|
||||
be doing a disservice to the community.
|
||||
|
||||
Please consider publishing your salary data like this as well. By open,
|
||||
voulntary transparency we can help to end stigmas around discussing pay and help
|
||||
ensure that the next generations of people in tech are treated fairly. Stigmas
|
||||
thrive in darkness but die in the light of day. You can help end the stigma by
|
||||
playing your cards out in the open like this.
|
||||
|
||||
It can be scary to do this; however every person that does it will make it that
|
||||
much more easy for the next person to do it.
|
||||
|
||||
Don't be afraid.
|
74
config.dhall
74
config.dhall
|
@ -1,38 +1,8 @@
|
|||
let Person =
|
||||
{ Type =
|
||||
{ name : Text
|
||||
, tags : List Text
|
||||
, gitLink : Optional Text
|
||||
, twitter : Optional Text
|
||||
}
|
||||
, default =
|
||||
{ name = ""
|
||||
, tags = [] : List Text
|
||||
, gitLink = None Text
|
||||
, twitter = None Text
|
||||
}
|
||||
}
|
||||
let Person = ./dhall/types/Person.dhall
|
||||
|
||||
let Author =
|
||||
{ Type =
|
||||
{ name : Text
|
||||
, handle : Text
|
||||
, picUrl : Optional Text
|
||||
, link : Optional Text
|
||||
, twitter : Optional Text
|
||||
, default : Bool
|
||||
, inSystem : Bool
|
||||
}
|
||||
, default =
|
||||
{ name = ""
|
||||
, handle = ""
|
||||
, picUrl = None Text
|
||||
, link = None Text
|
||||
, twitter = None Text
|
||||
, default = False
|
||||
, inSystem = False
|
||||
}
|
||||
}
|
||||
let Author = ./dhall/types/Author.dhall
|
||||
|
||||
let Job = ./dhall/types/Job.dhall
|
||||
|
||||
let defaultPort = env:PORT ? 3030
|
||||
|
||||
|
@ -49,48 +19,24 @@ let Config =
|
|||
, resumeFname : Text
|
||||
, webMentionEndpoint : Text
|
||||
, miToken : Text
|
||||
, jobHistory : List Job.Type
|
||||
}
|
||||
, default =
|
||||
{ signalboost = [] : List Person.Type
|
||||
, authors =
|
||||
[ Author::{
|
||||
, name = "Xe Iaso"
|
||||
, handle = "xe"
|
||||
, picUrl = Some "/static/img/avatar.png"
|
||||
, link = Some "https://christine.website"
|
||||
, twitter = Some "theprincessxena"
|
||||
, default = True
|
||||
, inSystem = True
|
||||
}
|
||||
, Author::{
|
||||
, name = "Jessie"
|
||||
, handle = "Heartmender"
|
||||
, picUrl = Some
|
||||
"https://cdn.christine.website/file/christine-static/img/UPRcp1pO_400x400.jpg"
|
||||
, link = Some "https://heartmender.writeas.com"
|
||||
, twitter = Some "BeJustFine"
|
||||
, inSystem = True
|
||||
}
|
||||
, Author::{
|
||||
, name = "Ashe"
|
||||
, handle = "ectamorphic"
|
||||
, picUrl = Some
|
||||
"https://cdn.christine.website/file/christine-static/img/FFVV1InX0AkDX3f_cropped_smol.jpg"
|
||||
, inSystem = True
|
||||
}
|
||||
, Author::{ name = "Nicole", handle = "Twi", inSystem = True }
|
||||
, Author::{ name = "Mai", handle = "Mai", inSystem = True }
|
||||
]
|
||||
, authors = [] : List Author.Type
|
||||
, port = defaultPort
|
||||
, clackSet = [ "Ashlynn" ]
|
||||
, resumeFname = "./static/resume/resume.md"
|
||||
, webMentionEndpoint = defaultWebMentionEndpoint
|
||||
, miToken = "${env:MI_TOKEN as Text ? ""}"
|
||||
, jobHistory = [] : List Job.Type
|
||||
}
|
||||
}
|
||||
|
||||
in Config::{
|
||||
, signalboost = ./signalboost.dhall
|
||||
, signalboost = ./dhall/signalboost.dhall
|
||||
, authors = ./dhall/authors.dhall
|
||||
, clackSet =
|
||||
[ "Ashlynn", "Terry Davis", "Dennis Ritchie", "Steven Hawking" ]
|
||||
, jobHistory = ./dhall/jobHistory.dhall
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ in pkgs.stdenv.mkDerivation {
|
|||
cp -rf $src/blog $out/blog
|
||||
cp -rf $src/css $out/css
|
||||
cp -rf $src/gallery $out/gallery
|
||||
cp -rf $src/signalboost.dhall $out/signalboost.dhall
|
||||
cp -rf $src/static $out/static
|
||||
cp -rf $src/talks $out/talks
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
let Author = ./types/Author.dhall
|
||||
|
||||
in [ Author::{
|
||||
, name = "Xe Iaso"
|
||||
, handle = "xe"
|
||||
, picUrl = Some "/static/img/avatar.png"
|
||||
, link = Some "https://christine.website"
|
||||
, twitter = Some "theprincessxena"
|
||||
, default = True
|
||||
, inSystem = True
|
||||
}
|
||||
, Author::{
|
||||
, name = "Jessie"
|
||||
, handle = "Heartmender"
|
||||
, picUrl = Some
|
||||
"https://cdn.christine.website/file/christine-static/img/UPRcp1pO_400x400.jpg"
|
||||
, link = Some "https://heartmender.writeas.com"
|
||||
, twitter = Some "BeJustFine"
|
||||
, inSystem = True
|
||||
}
|
||||
, Author::{
|
||||
, name = "Ashe"
|
||||
, handle = "ectamorphic"
|
||||
, picUrl = Some
|
||||
"https://cdn.christine.website/file/christine-static/img/FFVV1InX0AkDX3f_cropped_smol.jpg"
|
||||
, inSystem = True
|
||||
}
|
||||
, Author::{ name = "Nicole", handle = "Twi", inSystem = True }
|
||||
, Author::{ name = "Mai", handle = "Mai", inSystem = True }
|
||||
, Author::{ name = "Sephira", handle = "Sephie", inSystem = True }
|
||||
]
|
|
@ -0,0 +1,136 @@
|
|||
let Job = ./types/Job.dhall
|
||||
|
||||
let Salary = ./types/Salary.dhall
|
||||
|
||||
let annual = \(rate : Natural) -> Salary::{ amount = rate }
|
||||
|
||||
let hourly = \(rate : Natural) -> Salary::{ amount = rate, per = "hour" }
|
||||
|
||||
let annualCAD = \(rate : Natural) -> Salary::{ amount = rate, currency = "CAD" }
|
||||
|
||||
in [ Job::{
|
||||
, company = "Symplicity"
|
||||
, title = "Junior Systems Administrator"
|
||||
, startDate = "2013-11-11"
|
||||
, endDate = Some "2014-01-06"
|
||||
, daysWorked = Some 56
|
||||
, salary = annual 50000
|
||||
, leaveReason = Some "terminated"
|
||||
}
|
||||
, Job::{
|
||||
, company = "OpDemand"
|
||||
, title = "Software Engineering Intern"
|
||||
, startDate = "2014-07-14"
|
||||
, endDate = Some "2014-08-27"
|
||||
, daysWorked = Some 44
|
||||
, daysBetween = Some 189
|
||||
, salary = annual 35000
|
||||
, leaveReason = Some "terminated"
|
||||
}
|
||||
, Job::{
|
||||
, company = "Crowdflower (contract)"
|
||||
, title = "Consultant"
|
||||
, startDate = "2014-09-17"
|
||||
, endDate = Some "2014-10-15"
|
||||
, daysWorked = Some 28
|
||||
, daysBetween = Some 21
|
||||
, salary = hourly 90
|
||||
, leaveReason = Some "contract not renewed"
|
||||
}
|
||||
, Job::{
|
||||
, company = "VTCSecure (contract)"
|
||||
, title = "Consultant"
|
||||
, startDate = "2014-10-27"
|
||||
, endDate = Some "2015-02-09"
|
||||
, daysWorked = Some 105
|
||||
, daysBetween = Some 12
|
||||
, salary = hourly 90
|
||||
, leaveReason = Some "contract not renewed"
|
||||
}
|
||||
, Job::{
|
||||
, company = "IMVU"
|
||||
, title = "Site Reliability Engineer"
|
||||
, startDate = "2015-03-30"
|
||||
, endDate = Some "2016-03-07"
|
||||
, daysWorked = Some 343
|
||||
, daysBetween = Some 49
|
||||
, salary = annual 125000
|
||||
, leaveReason = Some "demoted"
|
||||
}
|
||||
, Job::{
|
||||
, company = "IMVU"
|
||||
, title = "Systems Administrator"
|
||||
, startDate = "2016-03-08"
|
||||
, endDate = Some "2016-04-01"
|
||||
, daysWorked = Some 24
|
||||
, daysBetween = Some 1
|
||||
, salary = annual 105000
|
||||
, leaveReason = Some "quit"
|
||||
}
|
||||
, Job::{
|
||||
, company = "Pure Storage"
|
||||
, title = "Member of Technical Staff"
|
||||
, startDate = "2016-04-04"
|
||||
, endDate = Some "2016-08-03"
|
||||
, daysWorked = Some 121
|
||||
, daysBetween = Some 3
|
||||
, salary = annual 135000
|
||||
, leaveReason = Some "quit"
|
||||
}
|
||||
, Job::{
|
||||
, company = "Backplane.io (defunct)"
|
||||
, title = "Software Engineer"
|
||||
, startDate = "2016-08-24"
|
||||
, endDate = Some "2016-11-22"
|
||||
, daysWorked = Some 90
|
||||
, daysBetween = Some 21
|
||||
, salary = annual 105000
|
||||
, leaveReason = Some "terminated"
|
||||
}
|
||||
, Job::{
|
||||
, company = "Heroku (contract)"
|
||||
, title = "Consultant"
|
||||
, startDate = "2017-02-13"
|
||||
, endDate = Some "2017-11-13"
|
||||
, daysWorked = Some 273
|
||||
, daysBetween = Some 83
|
||||
, salary = hourly 120
|
||||
, leaveReason = Some "hired"
|
||||
}
|
||||
, Job::{
|
||||
, company = "Heroku"
|
||||
, title = "Senior Software Engineer"
|
||||
, startDate = "2017-11-13"
|
||||
, endDate = Some "2019-03-08"
|
||||
, daysWorked = Some 480
|
||||
, daysBetween = Some 0
|
||||
, salary = annual 150000
|
||||
, leaveReason = Some "quit"
|
||||
}
|
||||
, Job::{
|
||||
, company = "Lightspeed POS"
|
||||
, title = "Expert principal en fiabilité du site"
|
||||
, startDate = "2019-05-06"
|
||||
, endDate = Some "2020-11-27"
|
||||
, daysWorked = Some 540
|
||||
, daysBetween = Some 48
|
||||
, salary = annualCAD 115000
|
||||
, leaveReason = Some "quit"
|
||||
}
|
||||
, Job::{
|
||||
, company = "Tailscale"
|
||||
, title = "Software Designer"
|
||||
, startDate = "2020-12-14"
|
||||
, endDate = Some "2022-03-01"
|
||||
, daysWorked = Some 442
|
||||
, daysBetween = Some 0
|
||||
, salary = annualCAD 135000
|
||||
, leaveReason = Some "raise"
|
||||
}
|
||||
, Job::{
|
||||
, company = "Tailscale"
|
||||
, title = "Archmage of Infrastructure"
|
||||
, startDate = "2022-03-01"
|
||||
, salary = annualCAD 147150
|
||||
}
|
||||
]
|
|
@ -1,17 +1,4 @@
|
|||
let Person =
|
||||
{ Type =
|
||||
{ name : Text
|
||||
, tags : List Text
|
||||
, gitLink : Optional Text
|
||||
, twitter : Optional Text
|
||||
}
|
||||
, default =
|
||||
{ name = ""
|
||||
, tags = [] : List Text
|
||||
, gitLink = None Text
|
||||
, twitter = None Text
|
||||
}
|
||||
}
|
||||
let Person = ./types/Person.dhall
|
||||
|
||||
in [ Person::{
|
||||
, name = "Christian Sullivan"
|
||||
|
@ -278,20 +265,20 @@ in [ Person::{
|
|||
, gitLink = Some "https://github.com/henri"
|
||||
, twitter = Some "https://twitter.com/henri_shustak"
|
||||
}
|
||||
, Person::{
|
||||
, name = "Gabriel Simmer"
|
||||
, tags =
|
||||
[ "golang"
|
||||
, "backend"
|
||||
, "javascript"
|
||||
, "python"
|
||||
, "software"
|
||||
, "full-stack"
|
||||
, "linux"
|
||||
, "devops"
|
||||
, "developer tooling"
|
||||
]
|
||||
, gitLink = Some "https://github.com/gmemstr"
|
||||
, twitter = Some "https://twitter.com/gmem_"
|
||||
, Person::{
|
||||
, name = "Gabriel Simmer"
|
||||
, tags =
|
||||
[ "golang"
|
||||
, "backend"
|
||||
, "javascript"
|
||||
, "python"
|
||||
, "software"
|
||||
, "full-stack"
|
||||
, "linux"
|
||||
, "devops"
|
||||
, "developer tooling"
|
||||
]
|
||||
, gitLink = Some "https://github.com/gmemstr"
|
||||
, twitter = Some "https://twitter.com/gmem_"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,19 @@
|
|||
{ Type =
|
||||
{ name : Text
|
||||
, handle : Text
|
||||
, picUrl : Optional Text
|
||||
, link : Optional Text
|
||||
, twitter : Optional Text
|
||||
, default : Bool
|
||||
, inSystem : Bool
|
||||
}
|
||||
, default =
|
||||
{ name = ""
|
||||
, handle = ""
|
||||
, picUrl = None Text
|
||||
, link = None Text
|
||||
, twitter = None Text
|
||||
, default = False
|
||||
, inSystem = False
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
let Salary = ./Salary.dhall
|
||||
|
||||
in { Type =
|
||||
{ company : Text
|
||||
, title : Text
|
||||
, startDate : Text
|
||||
, endDate : Optional Text
|
||||
, daysWorked : Optional Natural
|
||||
, daysBetween : Optional Natural
|
||||
, salary : Salary.Type
|
||||
, leaveReason : Optional Text
|
||||
}
|
||||
, default =
|
||||
{ company = "Unknown"
|
||||
, title = "Unknown"
|
||||
, startDate = "0000-01-01"
|
||||
, endDate = None Text
|
||||
, daysWorked = None Natural
|
||||
, daysBetween = None Natural
|
||||
, salary = Salary::{=}
|
||||
, leaveReason = None Text
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{ Type =
|
||||
{ name : Text
|
||||
, tags : List Text
|
||||
, gitLink : Optional Text
|
||||
, twitter : Optional Text
|
||||
}
|
||||
, default =
|
||||
{ name = "", tags = [] : List Text, gitLink = None Text, twitter = None Text }
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{ Type = { amount : Natural, currency : Text, per : Text }
|
||||
, default = { amount = 0, currency = "USD", per = "year" }
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
use crate::app::Config;
|
||||
use crate::templates::Html;
|
||||
use color_eyre::eyre::{Result, WrapErr};
|
||||
use comrak::nodes::{Ast, AstNode, NodeValue};
|
||||
|
@ -9,13 +10,14 @@ use comrak::{
|
|||
use lazy_static::lazy_static;
|
||||
use lol_html::{element, html_content::ContentType, rewrite_str, RewriteStrSettings};
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
lazy_static! {
|
||||
static ref SYNTECT_ADAPTER: SyntectAdapter<'static> = SyntectAdapter::new("base16-mocha.dark");
|
||||
}
|
||||
|
||||
pub fn render(inp: &str) -> Result<String> {
|
||||
pub fn render(cfg: Arc<Config>, inp: &str) -> Result<String> {
|
||||
let mut options = ComrakOptions::default();
|
||||
|
||||
options.extension.autolink = true;
|
||||
|
@ -99,6 +101,11 @@ pub fn render(inp: &str) -> Result<String> {
|
|||
element!("xeblog-hero", |el| {
|
||||
let file = el.get_attribute("file").expect("wanted xeblog-hero to contain file");
|
||||
el.replace(&crate::tmpl::xeblog_hero(file, el.get_attribute("prompt")).0, ContentType::Html);
|
||||
Ok(())
|
||||
}),
|
||||
element!("xeblog-salary-history", |el| {
|
||||
el.replace(&crate::tmpl::xeblog_salary_history(cfg.clone()).0, ContentType::Html);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
],
|
||||
|
|
|
@ -1,23 +1,73 @@
|
|||
use crate::{post::Post, signalboost::Person};
|
||||
use color_eyre::eyre::Result;
|
||||
use chrono::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use color_eyre::eyre::Result;
|
||||
use maud::{html, Markup};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
fs,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use tracing::{error, instrument};
|
||||
|
||||
pub mod markdown;
|
||||
pub mod poke;
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Config {
|
||||
pub(crate) signalboost: Vec<Person>,
|
||||
#[serde(rename = "resumeFname")]
|
||||
pub(crate) resume_fname: PathBuf,
|
||||
#[serde(rename = "miToken")]
|
||||
pub(crate) mi_token: String,
|
||||
#[serde(rename = "jobHistory")]
|
||||
pub(crate) job_history: Vec<Job>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize, Default)]
|
||||
pub struct Salary {
|
||||
pub amount: i32,
|
||||
pub per: String,
|
||||
pub currency: String,
|
||||
}
|
||||
|
||||
impl Display for Salary {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}${}/{}", self.currency, self.amount, self.per)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize, Default)]
|
||||
pub struct Job {
|
||||
pub company: String,
|
||||
pub title: String,
|
||||
#[serde(rename = "startDate")]
|
||||
pub start_date: String,
|
||||
#[serde(rename = "endDate")]
|
||||
pub end_date: Option<String>,
|
||||
#[serde(rename = "daysWorked")]
|
||||
pub days_worked: Option<i32>,
|
||||
#[serde(rename = "daysBetween")]
|
||||
pub days_between: Option<i32>,
|
||||
pub salary: Salary,
|
||||
#[serde(rename = "leaveReason")]
|
||||
pub leave_reason: Option<String>,
|
||||
}
|
||||
|
||||
impl Job {
|
||||
pub fn pay_history_row(&self) -> Markup {
|
||||
html! {
|
||||
tr {
|
||||
td { (self.title) }
|
||||
td { (self.start_date) }
|
||||
td { (self.end_date.as_ref().unwrap_or(&"current".to_string())) }
|
||||
td { (if self.days_worked.is_some() { self.days_worked.as_ref().unwrap().to_string() } else { "n/a".to_string() }) }
|
||||
td { (self.salary) }
|
||||
td { (self.leave_reason.as_ref().unwrap_or(&"n/a".to_string())) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
|
@ -57,7 +107,7 @@ async fn patrons() -> Result<Option<patreon::Users>> {
|
|||
pub const ICON: &'static str = "https://xeiaso.net/static/img/avatar.png";
|
||||
|
||||
pub struct State {
|
||||
pub cfg: Config,
|
||||
pub cfg: Arc<Config>,
|
||||
pub signalboost: Vec<Person>,
|
||||
pub resume: String,
|
||||
pub blog: Vec<Post>,
|
||||
|
@ -71,14 +121,17 @@ pub struct State {
|
|||
}
|
||||
|
||||
pub async fn init(cfg: PathBuf) -> Result<State> {
|
||||
let cfg: Config = serde_dhall::from_file(cfg).parse()?;
|
||||
let cfg: Arc<Config> = Arc::new(serde_dhall::from_file(cfg).parse()?);
|
||||
let sb = cfg.signalboost.clone();
|
||||
let resume = fs::read_to_string(cfg.resume_fname.clone())?;
|
||||
let resume: String = markdown::render(&resume)?;
|
||||
let mi = mi::Client::new(cfg.mi_token.clone(), crate::APPLICATION_NAME.to_string())?;
|
||||
let blog = crate::post::load("blog").await?;
|
||||
let gallery = crate::post::load("gallery").await?;
|
||||
let talks = crate::post::load("talks").await?;
|
||||
let resume = fs::read_to_string(cfg.clone().resume_fname.clone())?;
|
||||
let resume: String = markdown::render(cfg.clone(), &resume)?;
|
||||
let mi = mi::Client::new(
|
||||
cfg.clone().mi_token.clone(),
|
||||
crate::APPLICATION_NAME.to_string(),
|
||||
)?;
|
||||
let blog = crate::post::load(cfg.clone(), "blog").await?;
|
||||
let gallery = crate::post::load(cfg.clone(), "gallery").await?;
|
||||
let talks = crate::post::load(cfg.clone(), "talks").await?;
|
||||
let mut everything: Vec<Post> = vec![];
|
||||
|
||||
{
|
||||
|
@ -99,7 +152,7 @@ pub async fn init(cfg: PathBuf) -> Result<State> {
|
|||
.filter(|p| today.num_days_from_ce() >= p.date.num_days_from_ce())
|
||||
.take(5)
|
||||
.collect();
|
||||
|
||||
|
||||
let mut jfb = jsonfeed::Feed::builder()
|
||||
.title("Xe's Blog")
|
||||
.description("My blog posts and rants about various technology things.")
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
use crate::{app::State, templates};
|
||||
use crate::{
|
||||
app::{Job, State},
|
||||
templates,
|
||||
};
|
||||
use axum::{
|
||||
body,
|
||||
extract::Extension,
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::{Datelike, Timelike, Utc, Weekday};
|
||||
use lazy_static::lazy_static;
|
||||
|
@ -72,6 +76,28 @@ pub async fn feeds() -> Result {
|
|||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[axum_macros::debug_handler]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn salary_transparency(Extension(state): Extension<Arc<State>>) -> Result {
|
||||
HIT_COUNTER
|
||||
.with_label_values(&["salary_transparency"])
|
||||
.inc();
|
||||
let state = state.clone();
|
||||
let mut result: Vec<u8> = vec![];
|
||||
templates::salary_transparency(&mut result, state.cfg.clone())?;
|
||||
Ok(Html(result))
|
||||
}
|
||||
|
||||
#[axum_macros::debug_handler]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn salary_transparency_json(Extension(state): Extension<Arc<State>>) -> Json<Vec<Job>> {
|
||||
HIT_COUNTER
|
||||
.with_label_values(&["salary_transparency_json"])
|
||||
.inc();
|
||||
|
||||
Json(state.clone().cfg.clone().job_history.clone())
|
||||
}
|
||||
|
||||
#[axum_macros::debug_handler]
|
||||
#[instrument(skip(state))]
|
||||
pub async fn resume(Extension(state): Extension<Arc<State>>) -> Result {
|
||||
|
|
|
@ -155,6 +155,11 @@ async fn main() -> Result<()> {
|
|||
},
|
||||
),
|
||||
)
|
||||
// api
|
||||
.route(
|
||||
"/api/salary_transparency.json",
|
||||
get(handlers::salary_transparency_json),
|
||||
)
|
||||
// static pages
|
||||
.route("/", get(handlers::index))
|
||||
.route("/contact", get(handlers::contact))
|
||||
|
@ -162,6 +167,7 @@ async fn main() -> Result<()> {
|
|||
.route("/resume", get(handlers::resume))
|
||||
.route("/patrons", get(handlers::patrons))
|
||||
.route("/signalboost", get(handlers::signalboost))
|
||||
.route("/salary-transparency", get(handlers::salary_transparency))
|
||||
// feeds
|
||||
.route("/blog.json", get(handlers::feeds::jsonfeed))
|
||||
.route("/blog.atom", get(handlers::feeds::atom))
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use crate::app::Config;
|
||||
use chrono::prelude::*;
|
||||
use color_eyre::eyre::{eyre, Result, WrapErr};
|
||||
use glob::glob;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{borrow::Borrow, cmp::Ordering, path::PathBuf};
|
||||
use std::{borrow::Borrow, cmp::Ordering, path::PathBuf, sync::Arc};
|
||||
use tokio::fs;
|
||||
|
||||
pub mod frontmatter;
|
||||
|
@ -81,7 +82,12 @@ impl Post {
|
|||
}
|
||||
}
|
||||
|
||||
async fn read_post(dir: &str, fname: PathBuf, cli: &Option<mi::Client>) -> Result<Post> {
|
||||
async fn read_post(
|
||||
cfg: Arc<Config>,
|
||||
dir: &str,
|
||||
fname: PathBuf,
|
||||
cli: &Option<mi::Client>,
|
||||
) -> Result<Post> {
|
||||
debug!(
|
||||
"loading {}",
|
||||
fname.clone().into_os_string().into_string().unwrap()
|
||||
|
@ -96,7 +102,7 @@ async fn read_post(dir: &str, fname: PathBuf, cli: &Option<mi::Client>) -> Resul
|
|||
let date = NaiveDate::parse_from_str(&front_matter.clone().date, "%Y-%m-%d")
|
||||
.map_err(|why| eyre!("error parsing date in {:?}: {}", fname, why))?;
|
||||
let link = format!("{}/{}", dir, fname.file_stem().unwrap().to_str().unwrap());
|
||||
let body_html = crate::app::markdown::render(&body)
|
||||
let body_html = crate::app::markdown::render(cfg.clone(), &body)
|
||||
.wrap_err_with(|| format!("can't parse markdown for {:?}", fname))?;
|
||||
let date: DateTime<FixedOffset> =
|
||||
DateTime::<Utc>::from_utc(NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)), Utc)
|
||||
|
@ -144,7 +150,7 @@ async fn read_post(dir: &str, fname: PathBuf, cli: &Option<mi::Client>) -> Resul
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn load(dir: &str) -> Result<Vec<Post>> {
|
||||
pub async fn load(cfg: Arc<Config>, dir: &str) -> Result<Vec<Post>> {
|
||||
let cli = match std::env::var("MI_TOKEN") {
|
||||
Ok(token) => mi::Client::new(token.to_string(), crate::APPLICATION_NAME.to_string()).ok(),
|
||||
Err(_) => None,
|
||||
|
@ -152,7 +158,7 @@ pub async fn load(dir: &str) -> Result<Vec<Post>> {
|
|||
|
||||
let futs = glob(&format!("{}/*.markdown", dir))?
|
||||
.filter_map(Result::ok)
|
||||
.map(|fname| read_post(dir, fname, cli.borrow()));
|
||||
.map(|fname| read_post(cfg.clone(), dir, fname, cli.borrow()));
|
||||
|
||||
let mut result: Vec<Post> = futures::future::join_all(futs)
|
||||
.await
|
||||
|
@ -172,25 +178,30 @@ pub async fn load(dir: &str) -> Result<Vec<Post>> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app::Config;
|
||||
use color_eyre::eyre::Result;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn blog() {
|
||||
let _ = pretty_env_logger::try_init();
|
||||
load("blog").await.expect("posts to load");
|
||||
let cfg = Arc::new(Config::default());
|
||||
load(cfg, "blog").await.expect("posts to load");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gallery() -> Result<()> {
|
||||
let _ = pretty_env_logger::try_init();
|
||||
load("gallery").await?;
|
||||
let cfg = Arc::new(Config::default());
|
||||
load(cfg, "gallery").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn talks() -> Result<()> {
|
||||
let _ = pretty_env_logger::try_init();
|
||||
load("talks").await?;
|
||||
let cfg = Arc::new(Config::default());
|
||||
load(cfg, "talks").await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@ mod tests {
|
|||
use color_eyre::eyre::Result;
|
||||
#[test]
|
||||
fn load() -> Result<()> {
|
||||
let _people: Vec<super::Person> = serde_dhall::from_file("./signalboost.dhall").parse()?;
|
||||
let _people: Vec<super::Person> =
|
||||
serde_dhall::from_file("./dhall/signalboost.dhall").parse()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,7 +1,27 @@
|
|||
use crate::app::Config;
|
||||
use maud::{html, Markup};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod nag;
|
||||
|
||||
pub fn xeblog_salary_history(cfg: Arc<Config>) -> Markup {
|
||||
html! {
|
||||
table.salary_history {
|
||||
tr {
|
||||
th { "Title" }
|
||||
th { "Start Date" }
|
||||
th { "End Date" }
|
||||
th { "Days Worked" }
|
||||
th { "Salary" }
|
||||
th { "How I Left" }
|
||||
}
|
||||
@for job in &cfg.clone().job_history {
|
||||
(job.pay_history_row())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn xeblog_hero(file: String, prompt: Option<String>) -> Markup {
|
||||
html! {
|
||||
figure.hero style="margin:0" {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<p>Like what you see? Donate on <a href="https://www.patreon.com/cadey">Patreon</a> like <a href="/patrons">these awesome people</a>!</p>
|
||||
<p>Looking for someone for your team? Take a look <a href="/signalboost">here</a>.</p>
|
||||
<p>Served by @env!("out")/bin/xesite</a>, see <a href="https://github.com/Xe/site">source code here</a>.</p>
|
||||
<p>See my <a href="/salary-transparency">salary transparency data here</a>.</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -93,7 +93,7 @@ la budza pu cusku lu
|
|||
<div class="container">
|
||||
<header>
|
||||
<span class="logo"></span>
|
||||
<nav><a href="/">Xe</a> - <a href="/blog">Blog</a> - <a href="/contact">Contact</a> - <a href="/gallery">Gallery</a> - <a href="/resume">Resume</a> - <a href="/talks">Talks</a> - <a href="/signalboost">Signal Boost</a> - <a href="/feeds">Feeds</a> | <a target="_blank" rel="noopener noreferrer" href="https://graphviz.christine.website">GraphViz</a> - <a target="_blank" rel="noopener noreferrer" href="https://when-then-zen.christine.website/">When Then Zen</a></nav>
|
||||
<nav><a href="/">Xe</a> - <a href="/blog">Blog</a> - <a href="/contact">Contact</a> - <a href="/resume">Resume</a> - <a href="/talks">Talks</a> - <a href="/signalboost">Signal Boost</a> - <a href="/feeds">Feeds</a> | <a target="_blank" rel="noopener noreferrer" href="https://graphviz.christine.website">GraphViz</a> - <a target="_blank" rel="noopener noreferrer" href="https://when-then-zen.christine.website/">When Then Zen</a></nav>
|
||||
</header>
|
||||
|
||||
<br />
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
@use super::{header_html, footer_html};
|
||||
@use crate::{app::Config, tmpl::xeblog_salary_history};
|
||||
@use std::sync::Arc;
|
||||
|
||||
@(cfg: Arc<Config>)
|
||||
|
||||
@:header_html(Some("Salary Transparency"), None)
|
||||
|
||||
<h1>Salary Transparency</h1>
|
||||
|
||||
<p>This page lists my salary for every job I've had in tech. I have had this data open to the public <a href="https://xeiaso.net/blog/my-career-in-dates-titles-salaries-2019-03-14">for years</a>, but I feel this should be more prominently displayed on my website. Other people have copied my approach of having a list of every salary they have ever been payed on their websites, and I would like to set the example by making it prominent on my website.</p>
|
||||
|
||||
<p>As someone who has seen pay discrimination work in action first-hand, data is one of the ways that we can end this pointless hiding of information that leads to people being uninformed and hirt by their lack of knowledge. By laying my hand out in the open like this, I hope to ensure that people are better informed about how much money they <i>can</i> make, so that they can be paid equally for equal work.</p>
|
||||
|
||||
<h2>Salary Data</h2>
|
||||
|
||||
<p>To get this data, I have scoured over past emails, contracts and everything so that I can be sure that this information is as accurate as possible. The data on this page intentionally omits employer names.</p>
|
||||
|
||||
<p>Raw data (including employer names) is available at <code>/api/salary_transparency.json</code>. The JSON format is not stable. Do not treat it as such. I reserve the right to change the formatting or semantics of the JSON format at any time without warning. The raw data is in <code>/dhall/jobHistory.dhall</code> in my site's git repository.</p>
|
||||
|
||||
@Html(xeblog_salary_history(cfg.clone()).0)
|
||||
|
||||
<p>I typically update this page once any of the following things happens:</p>
|
||||
|
||||
<ul>
|
||||
<li>I quit a job.</li>
|
||||
<li>I get a raise/title change at the same company.</li>
|
||||
<li>I get terminated from a job.</li>
|
||||
<li>I get converted from a contracter to a full-time employee.</li>
|
||||
<li>Other unspecified extranormal events happen.</li>
|
||||
</ul>
|
||||
|
||||
<p>Please consider publishing your salary data like this as well. By open, voulntary transparency we can help to end stigmas around discussing pay and help ensure that the next generations of people in tech are treated fairly. Stigmas thrive in darkness but die in the light of day. You can help end the stigma by playing your cards out in the open like this.</p>
|
||||
|
||||
@:footer_html()
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<p>These awesome people are currently looking for a job. If you are looking for anyone with these skills, please feel free to reach out to them.</p>
|
||||
|
||||
<p>To add yourself to this list, fork <a href="https://github.com/Xe/site">this website's source code</a> and send a pull request with edits to <code>signalboost.dhall</code>.</p>
|
||||
<p>To add yourself to this list, fork <a href="https://github.com/Xe/site">this website's source code</a> and send a pull request with edits to <code>/dhall/signalboost.dhall</code>.</p>
|
||||
|
||||
<!-- TODO(Xe): Remove this after COVID-19 is less of a thing -->
|
||||
<p>With COVID-19 raging across the world, these people are in need of a job now more than ever.</p>
|
||||
|
|
Loading…
Reference in New Issue