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>
stanley
Cadey Ratio 6 months ago committed by GitHub
parent 7541df7781
commit ad6fba4c79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      blog/my-career-in-dates-titles-salaries-2019-03-14.markdown
  2. 42
      blog/site-update-salary-transparency.markdown
  3. 74
      config.dhall
  4. 1
      default.nix
  5. 31
      dhall/authors.dhall
  6. 136
      dhall/jobHistory.dhall
  7. 45
      dhall/signalboost.dhall
  8. 19
      dhall/types/Author.dhall
  9. 23
      dhall/types/Job.dhall
  10. 9
      dhall/types/Person.dhall
  11. 3
      dhall/types/Salary.dhall
  12. 9
      src/app/markdown.rs
  13. 77
      src/app/mod.rs
  14. 28
      src/handlers/mod.rs
  15. 6
      src/main.rs
  16. 27
      src/post/mod.rs
  17. 3
      src/signalboost.rs
  18. 20
      src/tmpl/mod.rs
  19. 1
      templates/footer.rs.html
  20. 2
      templates/header.rs.html
  21. 35
      templates/salary_transparency.rs.html
  22. 2
      templates/signalboost.rs.html

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

@ -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…
Cancel
Save