From ad6fba4c79e8b5ab08e2f0db8bc4087f03151f7f Mon Sep 17 00:00:00 2001 From: Xe Iaso Date: Tue, 14 Jun 2022 15:04:17 -0400 Subject: [PATCH] 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 --- ...-dates-titles-salaries-2019-03-14.markdown | 22 +-- blog/site-update-salary-transparency.markdown | 42 ++++++ config.dhall | 74 ++-------- default.nix | 1 - dhall/authors.dhall | 31 ++++ dhall/jobHistory.dhall | 136 ++++++++++++++++++ signalboost.dhall => dhall/signalboost.dhall | 45 +++--- dhall/types/Author.dhall | 19 +++ dhall/types/Job.dhall | 23 +++ dhall/types/Person.dhall | 9 ++ dhall/types/Salary.dhall | 3 + src/app/markdown.rs | 9 +- src/app/mod.rs | 77 ++++++++-- src/handlers/mod.rs | 28 +++- src/main.rs | 6 + src/post/mod.rs | 27 ++-- src/signalboost.rs | 3 +- src/tmpl/mod.rs | 20 +++ templates/footer.rs.html | 1 + templates/header.rs.html | 2 +- templates/salary_transparency.rs.html | 35 +++++ templates/signalboost.rs.html | 2 +- 22 files changed, 481 insertions(+), 134 deletions(-) create mode 100644 blog/site-update-salary-transparency.markdown create mode 100644 dhall/authors.dhall create mode 100644 dhall/jobHistory.dhall rename signalboost.dhall => dhall/signalboost.dhall (89%) create mode 100644 dhall/types/Author.dhall create mode 100644 dhall/types/Job.dhall create mode 100644 dhall/types/Person.dhall create mode 100644 dhall/types/Salary.dhall create mode 100644 templates/salary_transparency.rs.html diff --git a/blog/my-career-in-dates-titles-salaries-2019-03-14.markdown b/blog/my-career-in-dates-titles-salaries-2019-03-14.markdown index b962ec9..97548ea 100644 --- a/blog/my-career-in-dates-titles-salaries-2019-03-14.markdown +++ b/blog/my-career-in-dates-titles-salaries-2019-03-14.markdown @@ -3,6 +3,12 @@ title: My Career So Far in Dates/Titles/Salaries date: 2019-03-14 --- +
This post is +outdated, see here 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.
+ 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 | + 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 diff --git a/blog/site-update-salary-transparency.markdown b/blog/site-update-salary-transparency.markdown new file mode 100644 index 0000000..c274cbc --- /dev/null +++ b/blog/site-update-salary-transparency.markdown @@ -0,0 +1,42 @@ +--- +title: "Site Update: Salary Transparency Page Added" +date: 2022-06-14 +author: Sephie +--- + + + +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. diff --git a/config.dhall b/config.dhall index 4d86a88..8f4a9e4 100644 --- a/config.dhall +++ b/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 } diff --git a/default.nix b/default.nix index 0fa38c5..1f8021b 100644 --- a/default.nix +++ b/default.nix @@ -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 diff --git a/dhall/authors.dhall b/dhall/authors.dhall new file mode 100644 index 0000000..38c459e --- /dev/null +++ b/dhall/authors.dhall @@ -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 } + ] diff --git a/dhall/jobHistory.dhall b/dhall/jobHistory.dhall new file mode 100644 index 0000000..1148587 --- /dev/null +++ b/dhall/jobHistory.dhall @@ -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 + } + ] diff --git a/signalboost.dhall b/dhall/signalboost.dhall similarity index 89% rename from signalboost.dhall rename to dhall/signalboost.dhall index 6ec366f..39f41f3 100644 --- a/signalboost.dhall +++ b/dhall/signalboost.dhall @@ -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_" } ] diff --git a/dhall/types/Author.dhall b/dhall/types/Author.dhall new file mode 100644 index 0000000..dc0b1c3 --- /dev/null +++ b/dhall/types/Author.dhall @@ -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 + } +} diff --git a/dhall/types/Job.dhall b/dhall/types/Job.dhall new file mode 100644 index 0000000..a079d3c --- /dev/null +++ b/dhall/types/Job.dhall @@ -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 + } + } diff --git a/dhall/types/Person.dhall b/dhall/types/Person.dhall new file mode 100644 index 0000000..b1628eb --- /dev/null +++ b/dhall/types/Person.dhall @@ -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 } +} diff --git a/dhall/types/Salary.dhall b/dhall/types/Salary.dhall new file mode 100644 index 0000000..37dfce7 --- /dev/null +++ b/dhall/types/Salary.dhall @@ -0,0 +1,3 @@ +{ Type = { amount : Natural, currency : Text, per : Text } +, default = { amount = 0, currency = "USD", per = "year" } +} diff --git a/src/app/markdown.rs b/src/app/markdown.rs index f6ae342..d73a5c5 100644 --- a/src/app/markdown.rs +++ b/src/app/markdown.rs @@ -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 { +pub fn render(cfg: Arc, inp: &str) -> Result { let mut options = ComrakOptions::default(); options.extension.autolink = true; @@ -99,6 +101,11 @@ pub fn render(inp: &str) -> Result { 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(()) }) ], diff --git a/src/app/mod.rs b/src/app/mod.rs index 7125cf8..a12d1c6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -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, #[serde(rename = "resumeFname")] pub(crate) resume_fname: PathBuf, #[serde(rename = "miToken")] pub(crate) mi_token: String, + #[serde(rename = "jobHistory")] + pub(crate) job_history: Vec, +} + +#[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, + #[serde(rename = "daysWorked")] + pub days_worked: Option, + #[serde(rename = "daysBetween")] + pub days_between: Option, + pub salary: Salary, + #[serde(rename = "leaveReason")] + pub leave_reason: Option, +} + +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> { pub const ICON: &'static str = "https://xeiaso.net/static/img/avatar.png"; pub struct State { - pub cfg: Config, + pub cfg: Arc, pub signalboost: Vec, pub resume: String, pub blog: Vec, @@ -71,14 +121,17 @@ pub struct State { } pub async fn init(cfg: PathBuf) -> Result { - let cfg: Config = serde_dhall::from_file(cfg).parse()?; + let cfg: Arc = 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 = vec![]; { @@ -99,7 +152,7 @@ pub async fn init(cfg: PathBuf) -> Result { .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.") diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index fa8203c..fc2a154 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -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>) -> Result { + HIT_COUNTER + .with_label_values(&["salary_transparency"]) + .inc(); + let state = state.clone(); + let mut result: Vec = 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>) -> Json> { + 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>) -> Result { diff --git a/src/main.rs b/src/main.rs index 7611176..eb4b9e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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)) diff --git a/src/post/mod.rs b/src/post/mod.rs index 3e4cb8a..96c3e73 100644 --- a/src/post/mod.rs +++ b/src/post/mod.rs @@ -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) -> Result { +async fn read_post( + cfg: Arc, + dir: &str, + fname: PathBuf, + cli: &Option, +) -> Result { debug!( "loading {}", fname.clone().into_os_string().into_string().unwrap() @@ -96,7 +102,7 @@ async fn read_post(dir: &str, fname: PathBuf, cli: &Option) -> 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 = DateTime::::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) -> Resul }) } -pub async fn load(dir: &str) -> Result> { +pub async fn load(cfg: Arc, dir: &str) -> Result> { 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> { 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 = futures::future::join_all(futs) .await @@ -172,25 +178,30 @@ pub async fn load(dir: &str) -> Result> { #[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(()) } } diff --git a/src/signalboost.rs b/src/signalboost.rs index a57a976..3d1b534 100644 --- a/src/signalboost.rs +++ b/src/signalboost.rs @@ -16,7 +16,8 @@ mod tests { use color_eyre::eyre::Result; #[test] fn load() -> Result<()> { - let _people: Vec = serde_dhall::from_file("./signalboost.dhall").parse()?; + let _people: Vec = + serde_dhall::from_file("./dhall/signalboost.dhall").parse()?; Ok(()) } diff --git a/src/tmpl/mod.rs b/src/tmpl/mod.rs index 29d75f6..5391e9f 100644 --- a/src/tmpl/mod.rs +++ b/src/tmpl/mod.rs @@ -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) -> 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) -> Markup { html! { figure.hero style="margin:0" { diff --git a/templates/footer.rs.html b/templates/footer.rs.html index 4f0147f..038e6df 100644 --- a/templates/footer.rs.html +++ b/templates/footer.rs.html @@ -6,6 +6,7 @@

Like what you see? Donate on Patreon like these awesome people!

Looking for someone for your team? Take a look here.

Served by @env!("out")/bin/xesite, see source code here.

+

See my salary transparency data here.

diff --git a/templates/header.rs.html b/templates/header.rs.html index 8ec5cf8..11b16d4 100644 --- a/templates/header.rs.html +++ b/templates/header.rs.html @@ -93,7 +93,7 @@ la budza pu cusku lu
- +

diff --git a/templates/salary_transparency.rs.html b/templates/salary_transparency.rs.html new file mode 100644 index 0000000..394477b --- /dev/null +++ b/templates/salary_transparency.rs.html @@ -0,0 +1,35 @@ +@use super::{header_html, footer_html}; +@use crate::{app::Config, tmpl::xeblog_salary_history}; +@use std::sync::Arc; + +@(cfg: Arc) + +@:header_html(Some("Salary Transparency"), None) + +

Salary Transparency

+ +

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. 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.

+ +

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.

+ +

Salary Data

+ +

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.

+ +

Raw 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.

+ +@Html(xeblog_salary_history(cfg.clone()).0) + +

I typically update this page once any of the following things happens:

+ +
    +
  • I quit a job.
  • +
  • I get a raise/title change at the same company.
  • +
  • I get terminated from a job.
  • +
  • I get converted from a contracter to a full-time employee.
  • +
  • Other unspecified extranormal events happen.
  • +
+ +

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.

+ +@:footer_html() diff --git a/templates/signalboost.rs.html b/templates/signalboost.rs.html index 91211b3..91fa6a3 100644 --- a/templates/signalboost.rs.html +++ b/templates/signalboost.rs.html @@ -9,7 +9,7 @@

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.

-

To add yourself to this list, fork this website's source code and send a pull request with edits to signalboost.dhall.

+

To add yourself to this list, fork this website's source code and send a pull request with edits to /dhall/signalboost.dhall.

With COVID-19 raging across the world, these people are in need of a job now more than ever.