The code and data behind xeiaso.net
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Xe Iaso and committed by
GitHub
ad6fba4c 7541df77

+481 -134
+7 -15
blog/my-career-in-dates-titles-salaries-2019-03-14.markdown
··· 3 3 date: 2019-03-14 4 4 --- 5 5 6 + <div class="warning"><xeblog-conv name="Cadey" mood="coffee">This post is 7 + outdated, see <a href="/salary-transparency">here</a> for more context on why 8 + this data is made public. The table on this page will be automatically updated 9 + to contain the data on my salary transparency page, but you should prefer that 10 + page over this one when possible.</xeblog-conv></div> 11 + 6 12 Let this be inspiration to whoever is afraid of trying, failing and being fired. 7 13 Every single one of these jobs has taught me lessons I've used daily in my 8 14 career. ··· 26 32 The following table is a history of my software career by title, date and salary 27 33 (company names are omitted). 28 34 29 - | Title | Start Date | End Date | Days Worked | Days Between Jobs | Salary | How I Left | 30 - |:----- |:---------- |:-------- |:----------- |:----------------- |:------ |:---------- | 31 - | Junior Systems Administrator | November 11, 2013 | January 06, 2014 | 56 days | n/a | $50,000/year | Terminated | 32 - | Software Engineering Intern | July 14, 2014 | August 27, 2014 | 44 days | 189 days | $35,000/year | Terminated | 33 - | Consultant | September 17, 2014 | October 15, 2014 | 28 days | 21 days | $90/hour | Contract Lapsed | 34 - | Consultant | October 27, 2014 | Feburary 9, 2015 | 105 days | 12 days | $90/hour | Contract Lapsed | 35 - | Site Reliability Engineer | March 30, 2015 | March 7, 2016 | 343 days | 49 days | $125,000/year | Demoted | 36 - | Systems Administrator | March 8, 2016 | April 1, 2016 | 24 days | 1 day | $105,000/year | Bad terms | 37 - | Member of Technical Staff | April 4, 2016 | August 3, 2016 | 121 days | 3 days | $135,000/year | Bad terms | 38 - | Software Engineer | August 24, 2016 | November 22, 2016 | 90 days | 21 days | $105,000/year | Terminated | 39 - | Consultant | Feburary 13, 2017 | November 13, 2017 | 273 days | 83 days | don't remember | Hired | 40 - | Senior Software Engineer | November 13, 2017 | March 8, 2019 | 480 days | 0 days | $150,000/year | Voulntary quit | 41 - | 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 | 42 - | Software Designer | December 14, 2020 | *current* | n/a | n/a | CAD$135,000/year (about USD$ 105k and change) | raise | 43 - | Archmage of Infrastructure | March 1, 2022 | *current* | n/a | n/a | CAD$147,150/year (about USD$ 115k and change) | n/a | 35 + <xeblog-salary-history></xeblog-salary-history> 44 36 45 37 Even though I've been fired three times, I don't regret my career as it's been 46 38 thus far. I've been able to work on experimental technology integrating into
+42
blog/site-update-salary-transparency.markdown
··· 1 + --- 2 + title: "Site Update: Salary Transparency Page Added" 3 + date: 2022-06-14 4 + author: Sephie 5 + --- 6 + 7 + <xeblog-hero file="miku-dark-souls" prompt="hatsune miku, elden ring, dark souls, concept art, crowbar"></xeblog-hero> 8 + 9 + I have added a [salary transparency 10 + page](https://xeiaso.net/salary-transparency) to the blog. This page lists my 11 + salary for every job I've had in tech. I have had this data open to the public 12 + for years, but I feel this should be more prominently displayed on my website. 13 + 14 + As someone who has seen pay discrimination work in action first-hand, data is 15 + one of the ways that we can end this pointless hiding of information that leads 16 + to people being uninformed and hirt by their lack of knowledge. By laying my 17 + hand out in the open like this, I hope to ensure that people are better informed 18 + about how much money they can make, so that they can be paid equally for equal 19 + work. 20 + 21 + Raw, machine processable data (including employer names) is available at 22 + `/api/salary_transparency.json`. The JSON format is not stable. Do not treat it as 23 + such. I reserve the right to change the formatting or semantics of the JSON 24 + format at any time without warning. The raw data is in `/dhall/jobHistory.dhall` 25 + in my site's git repository. 26 + 27 + I have also taken the time to make sure that the [old 28 + post](https://xeiaso.net/blog/my-career-in-dates-titles-salaries-2019-03-14) 29 + maintains an up-to-date list. I do not want to break semantics on my website 30 + without a very good reason. By leaving the old post un-updated, I feel it would 31 + be doing a disservice to the community. 32 + 33 + Please consider publishing your salary data like this as well. By open, 34 + voulntary transparency we can help to end stigmas around discussing pay and help 35 + ensure that the next generations of people in tech are treated fairly. Stigmas 36 + thrive in darkness but die in the light of day. You can help end the stigma by 37 + playing your cards out in the open like this. 38 + 39 + It can be scary to do this; however every person that does it will make it that 40 + much more easy for the next person to do it. 41 + 42 + Don't be afraid.
+10 -64
config.dhall
··· 1 - let Person = 2 - { Type = 3 - { name : Text 4 - , tags : List Text 5 - , gitLink : Optional Text 6 - , twitter : Optional Text 7 - } 8 - , default = 9 - { name = "" 10 - , tags = [] : List Text 11 - , gitLink = None Text 12 - , twitter = None Text 13 - } 14 - } 1 + let Person = ./dhall/types/Person.dhall 15 2 16 - let Author = 17 - { Type = 18 - { name : Text 19 - , handle : Text 20 - , picUrl : Optional Text 21 - , link : Optional Text 22 - , twitter : Optional Text 23 - , default : Bool 24 - , inSystem : Bool 25 - } 26 - , default = 27 - { name = "" 28 - , handle = "" 29 - , picUrl = None Text 30 - , link = None Text 31 - , twitter = None Text 32 - , default = False 33 - , inSystem = False 34 - } 35 - } 3 + let Author = ./dhall/types/Author.dhall 4 + 5 + let Job = ./dhall/types/Job.dhall 36 6 37 7 let defaultPort = env:PORT ? 3030 38 8 ··· 49 19 , resumeFname : Text 50 20 , webMentionEndpoint : Text 51 21 , miToken : Text 22 + , jobHistory : List Job.Type 52 23 } 53 24 , default = 54 25 { signalboost = [] : List Person.Type 55 - , authors = 56 - [ Author::{ 57 - , name = "Xe Iaso" 58 - , handle = "xe" 59 - , picUrl = Some "/static/img/avatar.png" 60 - , link = Some "https://christine.website" 61 - , twitter = Some "theprincessxena" 62 - , default = True 63 - , inSystem = True 64 - } 65 - , Author::{ 66 - , name = "Jessie" 67 - , handle = "Heartmender" 68 - , picUrl = Some 69 - "https://cdn.christine.website/file/christine-static/img/UPRcp1pO_400x400.jpg" 70 - , link = Some "https://heartmender.writeas.com" 71 - , twitter = Some "BeJustFine" 72 - , inSystem = True 73 - } 74 - , Author::{ 75 - , name = "Ashe" 76 - , handle = "ectamorphic" 77 - , picUrl = Some 78 - "https://cdn.christine.website/file/christine-static/img/FFVV1InX0AkDX3f_cropped_smol.jpg" 79 - , inSystem = True 80 - } 81 - , Author::{ name = "Nicole", handle = "Twi", inSystem = True } 82 - , Author::{ name = "Mai", handle = "Mai", inSystem = True } 83 - ] 26 + , authors = [] : List Author.Type 84 27 , port = defaultPort 85 28 , clackSet = [ "Ashlynn" ] 86 29 , resumeFname = "./static/resume/resume.md" 87 30 , webMentionEndpoint = defaultWebMentionEndpoint 88 31 , miToken = "${env:MI_TOKEN as Text ? ""}" 32 + , jobHistory = [] : List Job.Type 89 33 } 90 34 } 91 35 92 36 in Config::{ 93 - , signalboost = ./signalboost.dhall 37 + , signalboost = ./dhall/signalboost.dhall 38 + , authors = ./dhall/authors.dhall 94 39 , clackSet = 95 40 [ "Ashlynn", "Terry Davis", "Dennis Ritchie", "Steven Hawking" ] 41 + , jobHistory = ./dhall/jobHistory.dhall 96 42 }
-1
default.nix
··· 48 48 cp -rf $src/blog $out/blog 49 49 cp -rf $src/css $out/css 50 50 cp -rf $src/gallery $out/gallery 51 - cp -rf $src/signalboost.dhall $out/signalboost.dhall 52 51 cp -rf $src/static $out/static 53 52 cp -rf $src/talks $out/talks 54 53
+31
dhall/authors.dhall
··· 1 + let Author = ./types/Author.dhall 2 + 3 + in [ Author::{ 4 + , name = "Xe Iaso" 5 + , handle = "xe" 6 + , picUrl = Some "/static/img/avatar.png" 7 + , link = Some "https://christine.website" 8 + , twitter = Some "theprincessxena" 9 + , default = True 10 + , inSystem = True 11 + } 12 + , Author::{ 13 + , name = "Jessie" 14 + , handle = "Heartmender" 15 + , picUrl = Some 16 + "https://cdn.christine.website/file/christine-static/img/UPRcp1pO_400x400.jpg" 17 + , link = Some "https://heartmender.writeas.com" 18 + , twitter = Some "BeJustFine" 19 + , inSystem = True 20 + } 21 + , Author::{ 22 + , name = "Ashe" 23 + , handle = "ectamorphic" 24 + , picUrl = Some 25 + "https://cdn.christine.website/file/christine-static/img/FFVV1InX0AkDX3f_cropped_smol.jpg" 26 + , inSystem = True 27 + } 28 + , Author::{ name = "Nicole", handle = "Twi", inSystem = True } 29 + , Author::{ name = "Mai", handle = "Mai", inSystem = True } 30 + , Author::{ name = "Sephira", handle = "Sephie", inSystem = True } 31 + ]
+136
dhall/jobHistory.dhall
··· 1 + let Job = ./types/Job.dhall 2 + 3 + let Salary = ./types/Salary.dhall 4 + 5 + let annual = \(rate : Natural) -> Salary::{ amount = rate } 6 + 7 + let hourly = \(rate : Natural) -> Salary::{ amount = rate, per = "hour" } 8 + 9 + let annualCAD = \(rate : Natural) -> Salary::{ amount = rate, currency = "CAD" } 10 + 11 + in [ Job::{ 12 + , company = "Symplicity" 13 + , title = "Junior Systems Administrator" 14 + , startDate = "2013-11-11" 15 + , endDate = Some "2014-01-06" 16 + , daysWorked = Some 56 17 + , salary = annual 50000 18 + , leaveReason = Some "terminated" 19 + } 20 + , Job::{ 21 + , company = "OpDemand" 22 + , title = "Software Engineering Intern" 23 + , startDate = "2014-07-14" 24 + , endDate = Some "2014-08-27" 25 + , daysWorked = Some 44 26 + , daysBetween = Some 189 27 + , salary = annual 35000 28 + , leaveReason = Some "terminated" 29 + } 30 + , Job::{ 31 + , company = "Crowdflower (contract)" 32 + , title = "Consultant" 33 + , startDate = "2014-09-17" 34 + , endDate = Some "2014-10-15" 35 + , daysWorked = Some 28 36 + , daysBetween = Some 21 37 + , salary = hourly 90 38 + , leaveReason = Some "contract not renewed" 39 + } 40 + , Job::{ 41 + , company = "VTCSecure (contract)" 42 + , title = "Consultant" 43 + , startDate = "2014-10-27" 44 + , endDate = Some "2015-02-09" 45 + , daysWorked = Some 105 46 + , daysBetween = Some 12 47 + , salary = hourly 90 48 + , leaveReason = Some "contract not renewed" 49 + } 50 + , Job::{ 51 + , company = "IMVU" 52 + , title = "Site Reliability Engineer" 53 + , startDate = "2015-03-30" 54 + , endDate = Some "2016-03-07" 55 + , daysWorked = Some 343 56 + , daysBetween = Some 49 57 + , salary = annual 125000 58 + , leaveReason = Some "demoted" 59 + } 60 + , Job::{ 61 + , company = "IMVU" 62 + , title = "Systems Administrator" 63 + , startDate = "2016-03-08" 64 + , endDate = Some "2016-04-01" 65 + , daysWorked = Some 24 66 + , daysBetween = Some 1 67 + , salary = annual 105000 68 + , leaveReason = Some "quit" 69 + } 70 + , Job::{ 71 + , company = "Pure Storage" 72 + , title = "Member of Technical Staff" 73 + , startDate = "2016-04-04" 74 + , endDate = Some "2016-08-03" 75 + , daysWorked = Some 121 76 + , daysBetween = Some 3 77 + , salary = annual 135000 78 + , leaveReason = Some "quit" 79 + } 80 + , Job::{ 81 + , company = "Backplane.io (defunct)" 82 + , title = "Software Engineer" 83 + , startDate = "2016-08-24" 84 + , endDate = Some "2016-11-22" 85 + , daysWorked = Some 90 86 + , daysBetween = Some 21 87 + , salary = annual 105000 88 + , leaveReason = Some "terminated" 89 + } 90 + , Job::{ 91 + , company = "Heroku (contract)" 92 + , title = "Consultant" 93 + , startDate = "2017-02-13" 94 + , endDate = Some "2017-11-13" 95 + , daysWorked = Some 273 96 + , daysBetween = Some 83 97 + , salary = hourly 120 98 + , leaveReason = Some "hired" 99 + } 100 + , Job::{ 101 + , company = "Heroku" 102 + , title = "Senior Software Engineer" 103 + , startDate = "2017-11-13" 104 + , endDate = Some "2019-03-08" 105 + , daysWorked = Some 480 106 + , daysBetween = Some 0 107 + , salary = annual 150000 108 + , leaveReason = Some "quit" 109 + } 110 + , Job::{ 111 + , company = "Lightspeed POS" 112 + , title = "Expert principal en fiabilité du site" 113 + , startDate = "2019-05-06" 114 + , endDate = Some "2020-11-27" 115 + , daysWorked = Some 540 116 + , daysBetween = Some 48 117 + , salary = annualCAD 115000 118 + , leaveReason = Some "quit" 119 + } 120 + , Job::{ 121 + , company = "Tailscale" 122 + , title = "Software Designer" 123 + , startDate = "2020-12-14" 124 + , endDate = Some "2022-03-01" 125 + , daysWorked = Some 442 126 + , daysBetween = Some 0 127 + , salary = annualCAD 135000 128 + , leaveReason = Some "raise" 129 + } 130 + , Job::{ 131 + , company = "Tailscale" 132 + , title = "Archmage of Infrastructure" 133 + , startDate = "2022-03-01" 134 + , salary = annualCAD 147150 135 + } 136 + ]
+19
dhall/types/Author.dhall
··· 1 + { Type = 2 + { name : Text 3 + , handle : Text 4 + , picUrl : Optional Text 5 + , link : Optional Text 6 + , twitter : Optional Text 7 + , default : Bool 8 + , inSystem : Bool 9 + } 10 + , default = 11 + { name = "" 12 + , handle = "" 13 + , picUrl = None Text 14 + , link = None Text 15 + , twitter = None Text 16 + , default = False 17 + , inSystem = False 18 + } 19 + }
+23
dhall/types/Job.dhall
··· 1 + let Salary = ./Salary.dhall 2 + 3 + in { Type = 4 + { company : Text 5 + , title : Text 6 + , startDate : Text 7 + , endDate : Optional Text 8 + , daysWorked : Optional Natural 9 + , daysBetween : Optional Natural 10 + , salary : Salary.Type 11 + , leaveReason : Optional Text 12 + } 13 + , default = 14 + { company = "Unknown" 15 + , title = "Unknown" 16 + , startDate = "0000-01-01" 17 + , endDate = None Text 18 + , daysWorked = None Natural 19 + , daysBetween = None Natural 20 + , salary = Salary::{=} 21 + , leaveReason = None Text 22 + } 23 + }
+9
dhall/types/Person.dhall
··· 1 + { Type = 2 + { name : Text 3 + , tags : List Text 4 + , gitLink : Optional Text 5 + , twitter : Optional Text 6 + } 7 + , default = 8 + { name = "", tags = [] : List Text, gitLink = None Text, twitter = None Text } 9 + }
+3
dhall/types/Salary.dhall
··· 1 + { Type = { amount : Natural, currency : Text, per : Text } 2 + , default = { amount = 0, currency = "USD", per = "year" } 3 + }
+16 -29
signalboost.dhall dhall/signalboost.dhall
··· 1 - let Person = 2 - { Type = 3 - { name : Text 4 - , tags : List Text 5 - , gitLink : Optional Text 6 - , twitter : Optional Text 7 - } 8 - , default = 9 - { name = "" 10 - , tags = [] : List Text 11 - , gitLink = None Text 12 - , twitter = None Text 13 - } 14 - } 1 + let Person = ./types/Person.dhall 15 2 16 3 in [ Person::{ 17 4 , name = "Christian Sullivan" ··· 278 265 , gitLink = Some "https://github.com/henri" 279 266 , twitter = Some "https://twitter.com/henri_shustak" 280 267 } 281 - , Person::{ 282 - , name = "Gabriel Simmer" 283 - , tags = 284 - [ "golang" 285 - , "backend" 286 - , "javascript" 287 - , "python" 288 - , "software" 289 - , "full-stack" 290 - , "linux" 291 - , "devops" 292 - , "developer tooling" 293 - ] 294 - , gitLink = Some "https://github.com/gmemstr" 295 - , twitter = Some "https://twitter.com/gmem_" 268 + , Person::{ 269 + , name = "Gabriel Simmer" 270 + , tags = 271 + [ "golang" 272 + , "backend" 273 + , "javascript" 274 + , "python" 275 + , "software" 276 + , "full-stack" 277 + , "linux" 278 + , "devops" 279 + , "developer tooling" 280 + ] 281 + , gitLink = Some "https://github.com/gmemstr" 282 + , twitter = Some "https://twitter.com/gmem_" 296 283 } 297 284 ]
+8 -1
src/app/markdown.rs
··· 1 + use crate::app::Config; 1 2 use crate::templates::Html; 2 3 use color_eyre::eyre::{Result, WrapErr}; 3 4 use comrak::nodes::{Ast, AstNode, NodeValue}; ··· 9 10 use lazy_static::lazy_static; 10 11 use lol_html::{element, html_content::ContentType, rewrite_str, RewriteStrSettings}; 11 12 use std::cell::RefCell; 13 + use std::sync::Arc; 12 14 use url::Url; 13 15 14 16 lazy_static! { 15 17 static ref SYNTECT_ADAPTER: SyntectAdapter<'static> = SyntectAdapter::new("base16-mocha.dark"); 16 18 } 17 19 18 - pub fn render(inp: &str) -> Result<String> { 20 + pub fn render(cfg: Arc<Config>, inp: &str) -> Result<String> { 19 21 let mut options = ComrakOptions::default(); 20 22 21 23 options.extension.autolink = true; ··· 99 101 element!("xeblog-hero", |el| { 100 102 let file = el.get_attribute("file").expect("wanted xeblog-hero to contain file"); 101 103 el.replace(&crate::tmpl::xeblog_hero(file, el.get_attribute("prompt")).0, ContentType::Html); 104 + Ok(()) 105 + }), 106 + element!("xeblog-salary-history", |el| { 107 + el.replace(&crate::tmpl::xeblog_salary_history(cfg.clone()).0, ContentType::Html); 108 + 102 109 Ok(()) 103 110 }) 104 111 ],
+65 -12
src/app/mod.rs
··· 1 1 use crate::{post::Post, signalboost::Person}; 2 + use chrono::prelude::*; 2 3 use color_eyre::eyre::Result; 3 - use chrono::prelude::*; 4 - use serde::Deserialize; 4 + use maud::{html, Markup}; 5 + use serde::{Deserialize, Serialize}; 5 6 use std::{ 7 + fmt::{self, Display}, 6 8 fs, 7 9 path::PathBuf, 10 + sync::Arc, 8 11 }; 9 12 use tracing::{error, instrument}; 10 13 11 14 pub mod markdown; 12 15 pub mod poke; 13 16 14 - #[derive(Clone, Deserialize)] 17 + #[derive(Clone, Deserialize, Default)] 15 18 pub struct Config { 16 19 pub(crate) signalboost: Vec<Person>, 17 20 #[serde(rename = "resumeFname")] 18 21 pub(crate) resume_fname: PathBuf, 19 22 #[serde(rename = "miToken")] 20 23 pub(crate) mi_token: String, 24 + #[serde(rename = "jobHistory")] 25 + pub(crate) job_history: Vec<Job>, 26 + } 27 + 28 + #[derive(Clone, Deserialize, Serialize, Default)] 29 + pub struct Salary { 30 + pub amount: i32, 31 + pub per: String, 32 + pub currency: String, 33 + } 34 + 35 + impl Display for Salary { 36 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 37 + write!(f, "{}${}/{}", self.currency, self.amount, self.per) 38 + } 39 + } 40 + 41 + #[derive(Clone, Deserialize, Serialize, Default)] 42 + pub struct Job { 43 + pub company: String, 44 + pub title: String, 45 + #[serde(rename = "startDate")] 46 + pub start_date: String, 47 + #[serde(rename = "endDate")] 48 + pub end_date: Option<String>, 49 + #[serde(rename = "daysWorked")] 50 + pub days_worked: Option<i32>, 51 + #[serde(rename = "daysBetween")] 52 + pub days_between: Option<i32>, 53 + pub salary: Salary, 54 + #[serde(rename = "leaveReason")] 55 + pub leave_reason: Option<String>, 56 + } 57 + 58 + impl Job { 59 + pub fn pay_history_row(&self) -> Markup { 60 + html! { 61 + tr { 62 + td { (self.title) } 63 + td { (self.start_date) } 64 + td { (self.end_date.as_ref().unwrap_or(&"current".to_string())) } 65 + td { (if self.days_worked.is_some() { self.days_worked.as_ref().unwrap().to_string() } else { "n/a".to_string() }) } 66 + td { (self.salary) } 67 + td { (self.leave_reason.as_ref().unwrap_or(&"n/a".to_string())) } 68 + } 69 + } 70 + } 21 71 } 22 72 23 73 #[instrument] ··· 57 107 pub const ICON: &'static str = "https://xeiaso.net/static/img/avatar.png"; 58 108 59 109 pub struct State { 60 - pub cfg: Config, 110 + pub cfg: Arc<Config>, 61 111 pub signalboost: Vec<Person>, 62 112 pub resume: String, 63 113 pub blog: Vec<Post>, ··· 71 121 } 72 122 73 123 pub async fn init(cfg: PathBuf) -> Result<State> { 74 - let cfg: Config = serde_dhall::from_file(cfg).parse()?; 124 + let cfg: Arc<Config> = Arc::new(serde_dhall::from_file(cfg).parse()?); 75 125 let sb = cfg.signalboost.clone(); 76 - let resume = fs::read_to_string(cfg.resume_fname.clone())?; 77 - let resume: String = markdown::render(&resume)?; 78 - let mi = mi::Client::new(cfg.mi_token.clone(), crate::APPLICATION_NAME.to_string())?; 79 - let blog = crate::post::load("blog").await?; 80 - let gallery = crate::post::load("gallery").await?; 81 - let talks = crate::post::load("talks").await?; 126 + let resume = fs::read_to_string(cfg.clone().resume_fname.clone())?; 127 + let resume: String = markdown::render(cfg.clone(), &resume)?; 128 + let mi = mi::Client::new( 129 + cfg.clone().mi_token.clone(), 130 + crate::APPLICATION_NAME.to_string(), 131 + )?; 132 + let blog = crate::post::load(cfg.clone(), "blog").await?; 133 + let gallery = crate::post::load(cfg.clone(), "gallery").await?; 134 + let talks = crate::post::load(cfg.clone(), "talks").await?; 82 135 let mut everything: Vec<Post> = vec![]; 83 136 84 137 { ··· 99 152 .filter(|p| today.num_days_from_ce() >= p.date.num_days_from_ce()) 100 153 .take(5) 101 154 .collect(); 102 - 155 + 103 156 let mut jfb = jsonfeed::Feed::builder() 104 157 .title("Xe's Blog") 105 158 .description("My blog posts and rants about various technology things.")
+27 -1
src/handlers/mod.rs
··· 1 - use crate::{app::State, templates}; 1 + use crate::{ 2 + app::{Job, State}, 3 + templates, 4 + }; 2 5 use axum::{ 3 6 body, 4 7 extract::Extension, 5 8 http::StatusCode, 6 9 response::{Html, IntoResponse, Response}, 10 + Json, 7 11 }; 8 12 use chrono::{Datelike, Timelike, Utc, Weekday}; 9 13 use lazy_static::lazy_static; ··· 70 74 let mut result: Vec<u8> = vec![]; 71 75 templates::feeds_html(&mut result)?; 72 76 Ok(Html(result)) 77 + } 78 + 79 + #[axum_macros::debug_handler] 80 + #[instrument(skip(state))] 81 + pub async fn salary_transparency(Extension(state): Extension<Arc<State>>) -> Result { 82 + HIT_COUNTER 83 + .with_label_values(&["salary_transparency"]) 84 + .inc(); 85 + let state = state.clone(); 86 + let mut result: Vec<u8> = vec![]; 87 + templates::salary_transparency(&mut result, state.cfg.clone())?; 88 + Ok(Html(result)) 89 + } 90 + 91 + #[axum_macros::debug_handler] 92 + #[instrument(skip(state))] 93 + pub async fn salary_transparency_json(Extension(state): Extension<Arc<State>>) -> Json<Vec<Job>> { 94 + HIT_COUNTER 95 + .with_label_values(&["salary_transparency_json"]) 96 + .inc(); 97 + 98 + Json(state.clone().cfg.clone().job_history.clone()) 73 99 } 74 100 75 101 #[axum_macros::debug_handler]
+6
src/main.rs
··· 155 155 }, 156 156 ), 157 157 ) 158 + // api 159 + .route( 160 + "/api/salary_transparency.json", 161 + get(handlers::salary_transparency_json), 162 + ) 158 163 // static pages 159 164 .route("/", get(handlers::index)) 160 165 .route("/contact", get(handlers::contact)) ··· 162 167 .route("/resume", get(handlers::resume)) 163 168 .route("/patrons", get(handlers::patrons)) 164 169 .route("/signalboost", get(handlers::signalboost)) 170 + .route("/salary-transparency", get(handlers::salary_transparency)) 165 171 // feeds 166 172 .route("/blog.json", get(handlers::feeds::jsonfeed)) 167 173 .route("/blog.atom", get(handlers::feeds::atom))
+19 -8
src/post/mod.rs
··· 1 + use crate::app::Config; 1 2 use chrono::prelude::*; 2 3 use color_eyre::eyre::{eyre, Result, WrapErr}; 3 4 use glob::glob; 4 5 use serde::{Deserialize, Serialize}; 5 - use std::{borrow::Borrow, cmp::Ordering, path::PathBuf}; 6 + use std::{borrow::Borrow, cmp::Ordering, path::PathBuf, sync::Arc}; 6 7 use tokio::fs; 7 8 8 9 pub mod frontmatter; ··· 81 82 } 82 83 } 83 84 84 - async fn read_post(dir: &str, fname: PathBuf, cli: &Option<mi::Client>) -> Result<Post> { 85 + async fn read_post( 86 + cfg: Arc<Config>, 87 + dir: &str, 88 + fname: PathBuf, 89 + cli: &Option<mi::Client>, 90 + ) -> Result<Post> { 85 91 debug!( 86 92 "loading {}", 87 93 fname.clone().into_os_string().into_string().unwrap() ··· 96 102 let date = NaiveDate::parse_from_str(&front_matter.clone().date, "%Y-%m-%d") 97 103 .map_err(|why| eyre!("error parsing date in {:?}: {}", fname, why))?; 98 104 let link = format!("{}/{}", dir, fname.file_stem().unwrap().to_str().unwrap()); 99 - let body_html = crate::app::markdown::render(&body) 105 + let body_html = crate::app::markdown::render(cfg.clone(), &body) 100 106 .wrap_err_with(|| format!("can't parse markdown for {:?}", fname))?; 101 107 let date: DateTime<FixedOffset> = 102 108 DateTime::<Utc>::from_utc(NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)), Utc) ··· 144 150 }) 145 151 } 146 152 147 - pub async fn load(dir: &str) -> Result<Vec<Post>> { 153 + pub async fn load(cfg: Arc<Config>, dir: &str) -> Result<Vec<Post>> { 148 154 let cli = match std::env::var("MI_TOKEN") { 149 155 Ok(token) => mi::Client::new(token.to_string(), crate::APPLICATION_NAME.to_string()).ok(), 150 156 Err(_) => None, ··· 152 158 153 159 let futs = glob(&format!("{}/*.markdown", dir))? 154 160 .filter_map(Result::ok) 155 - .map(|fname| read_post(dir, fname, cli.borrow())); 161 + .map(|fname| read_post(cfg.clone(), dir, fname, cli.borrow())); 156 162 157 163 let mut result: Vec<Post> = futures::future::join_all(futs) 158 164 .await ··· 172 178 #[cfg(test)] 173 179 mod tests { 174 180 use super::*; 181 + use crate::app::Config; 175 182 use color_eyre::eyre::Result; 183 + use std::sync::Arc; 176 184 177 185 #[tokio::test] 178 186 async fn blog() { 179 187 let _ = pretty_env_logger::try_init(); 180 - load("blog").await.expect("posts to load"); 188 + let cfg = Arc::new(Config::default()); 189 + load(cfg, "blog").await.expect("posts to load"); 181 190 } 182 191 183 192 #[tokio::test] 184 193 async fn gallery() -> Result<()> { 185 194 let _ = pretty_env_logger::try_init(); 186 - load("gallery").await?; 195 + let cfg = Arc::new(Config::default()); 196 + load(cfg, "gallery").await?; 187 197 Ok(()) 188 198 } 189 199 190 200 #[tokio::test] 191 201 async fn talks() -> Result<()> { 192 202 let _ = pretty_env_logger::try_init(); 193 - load("talks").await?; 203 + let cfg = Arc::new(Config::default()); 204 + load(cfg, "talks").await?; 194 205 Ok(()) 195 206 } 196 207 }
+2 -1
src/signalboost.rs
··· 16 16 use color_eyre::eyre::Result; 17 17 #[test] 18 18 fn load() -> Result<()> { 19 - let _people: Vec<super::Person> = serde_dhall::from_file("./signalboost.dhall").parse()?; 19 + let _people: Vec<super::Person> = 20 + serde_dhall::from_file("./dhall/signalboost.dhall").parse()?; 20 21 21 22 Ok(()) 22 23 }
+20
src/tmpl/mod.rs
··· 1 + use crate::app::Config; 1 2 use maud::{html, Markup}; 3 + use std::sync::Arc; 2 4 3 5 pub mod nag; 6 + 7 + pub fn xeblog_salary_history(cfg: Arc<Config>) -> Markup { 8 + html! { 9 + table.salary_history { 10 + tr { 11 + th { "Title" } 12 + th { "Start Date" } 13 + th { "End Date" } 14 + th { "Days Worked" } 15 + th { "Salary" } 16 + th { "How I Left" } 17 + } 18 + @for job in &cfg.clone().job_history { 19 + (job.pay_history_row()) 20 + } 21 + } 22 + } 23 + } 4 24 5 25 pub fn xeblog_hero(file: String, prompt: Option<String>) -> Markup { 6 26 html! {
+1
templates/footer.rs.html
··· 6 6 <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> 7 7 <p>Looking for someone for your team? Take a look <a href="/signalboost">here</a>.</p> 8 8 <p>Served by @env!("out")/bin/xesite</a>, see <a href="https://github.com/Xe/site">source code here</a>.</p> 9 + <p>See my <a href="/salary-transparency">salary transparency data here</a>.</p> 9 10 </footer> 10 11 11 12 </div>
+1 -1
templates/header.rs.html
··· 93 93 <div class="container"> 94 94 <header> 95 95 <span class="logo"></span> 96 - <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> 96 + <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> 97 97 </header> 98 98 99 99 <br />
+35
templates/salary_transparency.rs.html
··· 1 + @use super::{header_html, footer_html}; 2 + @use crate::{app::Config, tmpl::xeblog_salary_history}; 3 + @use std::sync::Arc; 4 + 5 + @(cfg: Arc<Config>) 6 + 7 + @:header_html(Some("Salary Transparency"), None) 8 + 9 + <h1>Salary Transparency</h1> 10 + 11 + <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> 12 + 13 + <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> 14 + 15 + <h2>Salary Data</h2> 16 + 17 + <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> 18 + 19 + <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> 20 + 21 + @Html(xeblog_salary_history(cfg.clone()).0) 22 + 23 + <p>I typically update this page once any of the following things happens:</p> 24 + 25 + <ul> 26 + <li>I quit a job.</li> 27 + <li>I get a raise/title change at the same company.</li> 28 + <li>I get terminated from a job.</li> 29 + <li>I get converted from a contracter to a full-time employee.</li> 30 + <li>Other unspecified extranormal events happen.</li> 31 + </ul> 32 + 33 + <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> 34 + 35 + @:footer_html()
+1 -1
templates/signalboost.rs.html
··· 9 9 10 10 <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> 11 11 12 - <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> 12 + <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> 13 13 14 14 <!-- TODO(Xe): Remove this after COVID-19 is less of a thing --> 15 15 <p>With COVID-19 raging across the world, these people are in need of a job now more than ever.</p>