Compare commits
7 Commits
table-of-c
...
a3c299b057
| Author | SHA1 | Date | |
|---|---|---|---|
| a3c299b057 | |||
| 12aedfce92 | |||
| 65645346a2 | |||
| cb65e89e53 | |||
| 6a2fec8ef4 | |||
| aa59c90810 | |||
| 2b317930a0 |
99
code/typesafe-interpreter/TypesafeIntrV2.idr
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
data ExprType
|
||||||
|
= IntType
|
||||||
|
| BoolType
|
||||||
|
| StringType
|
||||||
|
|
||||||
|
repr : ExprType -> Type
|
||||||
|
repr IntType = Int
|
||||||
|
repr BoolType = Bool
|
||||||
|
repr StringType = String
|
||||||
|
|
||||||
|
intBoolImpossible : IntType = BoolType -> Void
|
||||||
|
intBoolImpossible Refl impossible
|
||||||
|
|
||||||
|
intStringImpossible : IntType = StringType -> Void
|
||||||
|
intStringImpossible Refl impossible
|
||||||
|
|
||||||
|
boolStringImpossible : BoolType = StringType -> Void
|
||||||
|
boolStringImpossible Refl impossible
|
||||||
|
|
||||||
|
decEq : (a : ExprType) -> (b : ExprType) -> Dec (a = b)
|
||||||
|
decEq {a = IntType} {b = IntType} = Yes Refl
|
||||||
|
decEq {a = BoolType} {b = BoolType} = Yes Refl
|
||||||
|
decEq {a = StringType} {b = StringType} = Yes Refl
|
||||||
|
decEq {a = IntType} {b = BoolType} = No intBoolImpossible
|
||||||
|
decEq {a = BoolType} {b = IntType} = No $ intBoolImpossible . sym
|
||||||
|
decEq {a = IntType} {b = StringType} = No intStringImpossible
|
||||||
|
decEq {a = StringType} {b = IntType} = No $ intStringImpossible . sym
|
||||||
|
decEq {a = BoolType} {b = StringType} = No boolStringImpossible
|
||||||
|
decEq {a = StringType} {b = BoolType} = No $ boolStringImpossible . sym
|
||||||
|
|
||||||
|
data Op
|
||||||
|
= Add
|
||||||
|
| Subtract
|
||||||
|
| Multiply
|
||||||
|
| Divide
|
||||||
|
|
||||||
|
data Expr
|
||||||
|
= IntLit Int
|
||||||
|
| BoolLit Bool
|
||||||
|
| StringLit String
|
||||||
|
| BinOp Op Expr Expr
|
||||||
|
| IfElse Expr Expr Expr
|
||||||
|
|
||||||
|
data SafeExpr : ExprType -> Type where
|
||||||
|
IntLiteral : Int -> SafeExpr IntType
|
||||||
|
BoolLiteral : Bool -> SafeExpr BoolType
|
||||||
|
StringLiteral : String -> SafeExpr StringType
|
||||||
|
BinOperation : (repr a -> repr b -> repr c) -> SafeExpr a -> SafeExpr b -> SafeExpr c
|
||||||
|
IfThenElse : { t : ExprType } -> SafeExpr BoolType -> SafeExpr t -> SafeExpr t -> SafeExpr t
|
||||||
|
|
||||||
|
typecheckOp : Op -> (a : ExprType) -> (b : ExprType) -> Either String (c : ExprType ** repr a -> repr b -> repr c)
|
||||||
|
typecheckOp Add IntType IntType = Right (IntType ** (+))
|
||||||
|
typecheckOp Subtract IntType IntType = Right (IntType ** (-))
|
||||||
|
typecheckOp Multiply IntType IntType = Right (IntType ** (*))
|
||||||
|
typecheckOp Divide IntType IntType = Right (IntType ** div)
|
||||||
|
typecheckOp _ _ _ = Left "Invalid binary operator application"
|
||||||
|
|
||||||
|
requireBool : (n : ExprType ** SafeExpr n) -> Either String (SafeExpr BoolType)
|
||||||
|
requireBool (BoolType ** e) = Right e
|
||||||
|
requireBool _ = Left "Not a boolean."
|
||||||
|
|
||||||
|
typecheck : Expr -> Either String (n : ExprType ** SafeExpr n)
|
||||||
|
typecheck (IntLit i) = Right (_ ** IntLiteral i)
|
||||||
|
typecheck (BoolLit b) = Right (_ ** BoolLiteral b)
|
||||||
|
typecheck (StringLit s) = Right (_ ** StringLiteral s)
|
||||||
|
typecheck (BinOp o l r) = do
|
||||||
|
(lt ** le) <- typecheck l
|
||||||
|
(rt ** re) <- typecheck r
|
||||||
|
(ot ** f) <- typecheckOp o lt rt
|
||||||
|
pure (_ ** BinOperation f le re)
|
||||||
|
typecheck (IfElse c t e) =
|
||||||
|
do
|
||||||
|
ce <- typecheck c >>= requireBool
|
||||||
|
(tt ** te) <- typecheck t
|
||||||
|
(et ** ee) <- typecheck e
|
||||||
|
case decEq tt et of
|
||||||
|
Yes p => pure (_ ** IfThenElse ce (replace p te) ee)
|
||||||
|
No _ => Left "Incompatible branch types."
|
||||||
|
|
||||||
|
eval : SafeExpr t -> repr t
|
||||||
|
eval (IntLiteral i) = i
|
||||||
|
eval (BoolLiteral b) = b
|
||||||
|
eval (StringLiteral s) = s
|
||||||
|
eval (BinOperation f l r) = f (eval l) (eval r)
|
||||||
|
eval (IfThenElse c t e) = if (eval c) then (eval t) else (eval e)
|
||||||
|
|
||||||
|
resultStr : {t : ExprType} -> repr t -> String
|
||||||
|
resultStr {t=IntType} i = show i
|
||||||
|
resultStr {t=BoolType} b = show b
|
||||||
|
resultStr {t=StringType} s = show s
|
||||||
|
|
||||||
|
tryEval : Expr -> String
|
||||||
|
tryEval ex =
|
||||||
|
case typecheck ex of
|
||||||
|
Left err => "Type error: " ++ err
|
||||||
|
Right (t ** e) => resultStr $ eval {t} e
|
||||||
|
|
||||||
|
main : IO ()
|
||||||
|
main = putStrLn $ tryEval $ BinOp Add (IfElse (BoolLit True) (IntLit 6) (IntLit 7)) (BinOp Multiply (IntLit 160) (IntLit 2))
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: About
|
title: About
|
||||||
---
|
---
|
||||||
I'm Daniel, a Computer Science student currently in my third (and final) undergraduate year at Oregon State University.
|
I'm Daniel, a Computer Science student currently working towards my Master's Degree at Oregon State University.
|
||||||
Due my initial interest in calculators and compilers, I got involved in the Programming Language Theory research
|
Due to my initial interest in calculators and compilers, I got involved in the Programming Language Theory research
|
||||||
group, gaining same experience in formal verification, domain specific language, and explainable computing.
|
group, gaining same experience in formal verification, domain specific language, and explainable computing.
|
||||||
|
|
||||||
For work, school, and hobby projects, I use a variety of programming languages, most commonly C/C++,
|
For work, school, and hobby projects, I use a variety of programming languages, most commonly C/C++,
|
||||||
|
|||||||
283
content/blog/backend_math_rendering.md
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
---
|
||||||
|
title: Rendering Mathematics On The Back End
|
||||||
|
date: 2020-07-15T15:27:19-07:00
|
||||||
|
draft: true
|
||||||
|
tags: ["Website", "Nix", "Ruby", "KaTeX", "Hugo"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Due to something of a streak of bad luck when it came to computers, I spent a
|
||||||
|
significant amount of time using a Linux-based Chromebook, and then a
|
||||||
|
Pinebook Pro. It was, in some way, enlightening. The things that I used to take
|
||||||
|
for granted with a 'powerful' machine now became a rare luxury: StackOverflow,
|
||||||
|
and other relatively static websites, took upwards of ten seconds to finish
|
||||||
|
loading. On Slack, each of my keypresses could take longer than 500ms to
|
||||||
|
appear on the screen, and sometimes, it would take several seconds. Some
|
||||||
|
websites would present me with a white screen, and remain that way for much
|
||||||
|
longer than I had time to wait. It was awful.
|
||||||
|
|
||||||
|
At one point, I installed uMatrix, and made it the default policy to block
|
||||||
|
all JavaScript. For the most part, this worked well. Of course, I had to
|
||||||
|
enable JavaScript for applications that needed to be interactive, like
|
||||||
|
Slack, and Discord. But for the most part, I was able to browse the majority
|
||||||
|
of the websites I normally browse. This went on until I started working
|
||||||
|
on the [compiler series]({{< relref "00_compiler_intro.md" >}}) again,
|
||||||
|
and discovered that the LaTeX math on my page, which was required
|
||||||
|
for displaying things like inference rules, didn't work without
|
||||||
|
JavaScript. I was left with two options:
|
||||||
|
|
||||||
|
* Allow JavaScript, and continue using MathJax to render my math.
|
||||||
|
* Make it so that the mathematics is rendered on the back end.
|
||||||
|
|
||||||
|
I've [previously written about math rendering]({{< relref "math_rendering_is_wrong.md" >}}),
|
||||||
|
and made the observation that MathJax's output for LaTeX is __identical__
|
||||||
|
on every computer. From the MathJax 2.6 change log:
|
||||||
|
|
||||||
|
> _Improved CommonHTML output_. The CommonHTML output now provides the same layout quality and MathML support as the HTML-CSS and SVG output. It is on average 40% faster than the other outputs and the markup it produces are identical on all browsers and thus can also be pre-generated on the server via MathJax-node.
|
||||||
|
|
||||||
|
It seems absurd, then, to offload this kind of work into the users, to
|
||||||
|
be done over and over again. As should be clear from the title of
|
||||||
|
this post, this made me settle for the second option: it was
|
||||||
|
__obviously within reach__, especially for a statically-generated website
|
||||||
|
like mine, to render math on the backend.
|
||||||
|
|
||||||
|
I settled on the following architecture:
|
||||||
|
|
||||||
|
* As before, I would generate my pages using Hugo.
|
||||||
|
* I would use the KaTeX NPM package to render math.
|
||||||
|
* To build the website no matter what system I was on, I would use Nix.
|
||||||
|
|
||||||
|
It so happens that Nix isn't really required for using my approach in general.
|
||||||
|
I will give my setup here, but feel free to skip ahead.
|
||||||
|
|
||||||
|
### Setting Up A Nix Build
|
||||||
|
My `default.nix` file looks like this:
|
||||||
|
|
||||||
|
```Nix {linenos=table}
|
||||||
|
{ stdenv, hugo, fetchgit, pkgs, nodejs, ruby }:
|
||||||
|
|
||||||
|
let
|
||||||
|
url = "https://dev.danilafe.com/Web-Projects/blog-static.git";
|
||||||
|
rev = "<commit>";
|
||||||
|
sha256 = "<hash>";
|
||||||
|
requiredPackages = import ./required-packages.nix {
|
||||||
|
inherit pkgs nodejs;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
name = "blog-static";
|
||||||
|
version = rev;
|
||||||
|
src = fetchgit {
|
||||||
|
inherit url rev sha256;
|
||||||
|
};
|
||||||
|
builder = ./builder.sh;
|
||||||
|
converter = ./convert.rb;
|
||||||
|
buildInputs = [
|
||||||
|
hugo
|
||||||
|
requiredPackages.katex
|
||||||
|
(ruby.withPackages (ps: [ ps.nokogiri ]))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
I'm using `node2nix` to generate the `required-packages.nix` file, which allows me,
|
||||||
|
even from a sandboxed Nix build, to download and install `npm` packages. This is needed
|
||||||
|
so that I have access to the `katex` binary at build time. I fed the following JSON file
|
||||||
|
to `node2nix`:
|
||||||
|
|
||||||
|
```JSON {linenos=table}
|
||||||
|
[
|
||||||
|
"katex"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
The Ruby script I wrote for this (more on that soon) required the `nokigiri` gem, which
|
||||||
|
I used for traversing the HTML generated for my site. Hugo was obviously required to
|
||||||
|
generate the HTML.
|
||||||
|
|
||||||
|
### Converting LaTeX To HTML
|
||||||
|
After my first post complaining about the state of mathematics on the web, I received
|
||||||
|
the following email (which the author allowed me to share):
|
||||||
|
|
||||||
|
> Sorry for having a random stranger email you, but in your blog post
|
||||||
|
[(link)](https://danilafe.com/blog/math_rendering_is_wrong) you seem to focus on MathJax's
|
||||||
|
difficulty in rendering things server-side, while quietly ignoring that KaTeX's front
|
||||||
|
page advertises server-side rendering. Their documentation [(link)](https://katex.org/docs/options.html)
|
||||||
|
even shows (at least as of the time this email was sent) that it renders both HTML
|
||||||
|
(to be arranged nicely with their CSS) for visuals and MathML for accessibility.
|
||||||
|
|
||||||
|
This is a great point, and KaTeX is indeed usable for server-side rendering. But I've
|
||||||
|
seen few people who do actually use it. Unfortunately, as I pointed out in my previous post on the subject,
|
||||||
|
few tools remain that provide the software that actually takes your HTML page and substitutes
|
||||||
|
LaTeX for math.
|
||||||
|
|
||||||
|
> [In MathJax,] The bigger issue, though, was that the `page2html`
|
||||||
|
program, which rendered all the mathematics in a single HTML page,
|
||||||
|
was gone. I found `tex2html` and `text2htmlcss`, which could only
|
||||||
|
render equations without the surrounding HTML. I also found `mjpage`,
|
||||||
|
which replaced mathematical expressions in a page with their SVG forms.
|
||||||
|
|
||||||
|
This is still the case, in both MathJax and KaTeX. The ability
|
||||||
|
to render math in one step is the main selling point of front-end LaTeX renderers:
|
||||||
|
all you have to do is drop in a file from a CDN, and voila, you have your
|
||||||
|
math. There are no such easy answers for back-end rendering. I decided
|
||||||
|
to write my own Ruby script to get the job done. From this script, I
|
||||||
|
would call the `katex` command-line program, which would perform
|
||||||
|
the heavy lifting of rendering the mathematics.
|
||||||
|
|
||||||
|
There are two types of math on my website: inline math and display math.
|
||||||
|
On the command line ([here are the docs](https://katex.org/docs/cli.html)),
|
||||||
|
the distinction is made using the `--display-mode` argument. So, the general algorithm
|
||||||
|
is to replace the code inside the `$$...$$` with their display-rendered version,
|
||||||
|
and the code inside the `\(...\)` with the inline-rendered version. I came up with
|
||||||
|
the following Ruby function:
|
||||||
|
|
||||||
|
```Ruby {linenos=table}
|
||||||
|
def render_cached(cache, command, string, render_comment = nil)
|
||||||
|
cache.fetch(string) do |new|
|
||||||
|
puts " Rendering #{render_comment || new}"
|
||||||
|
cache[string] = Open3.popen3(command) do |i, o, e, t|
|
||||||
|
i.write new
|
||||||
|
i.close
|
||||||
|
o.read.force_encoding(Encoding::UTF_8).strip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, the `cache` argument is used to prevent re-running the `katex` command
|
||||||
|
on an equation that was already rendered before (the output is the same, after all).
|
||||||
|
The `command` is the specific shell command that we want to invoke; this would
|
||||||
|
be either `katex` or `katex -d`. The `string` is the math equation to render,
|
||||||
|
and the `render_comment` is the string to print to the console instead of the equation
|
||||||
|
(so that long, display math equations are not printed out to standard out).
|
||||||
|
|
||||||
|
Then, given a substring of the HTML file, we use regular expressions
|
||||||
|
to find the `\(...\)` and `$$...$$`s, and use the `render_cached` method
|
||||||
|
on the LaTeX code inside.
|
||||||
|
|
||||||
|
```Ruby {linenos=table}
|
||||||
|
def perform_katex_sub(inline_cache, display_cache, content)
|
||||||
|
rendered = content.gsub /\\\(((?:[^\\]|\\[^\)])*)\\\)/ do |match|
|
||||||
|
render_cached(inline_cache, "katex", $~[1])
|
||||||
|
end
|
||||||
|
rendered = rendered.gsub /\$\$((?:[^\$]|$[^\$])*)\$\$/ do |match|
|
||||||
|
render_cached(display_cache, "katex -d", $~[1], "display")
|
||||||
|
end
|
||||||
|
return rendered
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
There's a bit of a trick to the final layer of this script. We want to be
|
||||||
|
really careful about where we replace LaTeX, and where we don't. In
|
||||||
|
particular, we _don't_ want to go into the `code` tags. Otherwise,
|
||||||
|
it wouldn't be possible to talk about LaTeX code! Thus, we can't just
|
||||||
|
search-and-replace over the entire HTML document; we need to be context
|
||||||
|
aware. This is where `nokigiri` comes in. We parse the HTML, and iterate
|
||||||
|
over all of the 'text' nodes, calling `perform_katex_sub` on all
|
||||||
|
of those that _aren't_ inside code tags.
|
||||||
|
|
||||||
|
Fortunately, this is pretty easy to specify thanks to something called XPath.
|
||||||
|
This was my first time encountering it, but it seems extremely useful: it's
|
||||||
|
a sort of language for selecting XML nodes. First, you provide an 'axis',
|
||||||
|
which is used to specify the positions of the nodes you want to look at
|
||||||
|
relative to the root node. The axis `/` looks at the immediate children
|
||||||
|
(this would be the `html` tag in a properly formatted document, I would imagine).
|
||||||
|
The axis `//` looks at all the transitive children. That is, it will look at the
|
||||||
|
children of the root, then its children, and so on. There's also the `self` axis,
|
||||||
|
which looks at the node itself.
|
||||||
|
|
||||||
|
After you provide an axis, you need to specify the type of node that you want to
|
||||||
|
select. We can write `code`, for instance, to pick only the `<code>....</code>` tags
|
||||||
|
from the axis we've chosen. We can also use `*` to select any node, and we can
|
||||||
|
use `text()` to select text nodes, such as the `Hello` inside of `<b>Hello</b>`.
|
||||||
|
|
||||||
|
We can also apply some more conditions to the nodes we pick using `[]`.
|
||||||
|
For us, the relevant feature here is `not(...)`, which allows us to
|
||||||
|
select nodes that do __not__ match a particular condition. This is all
|
||||||
|
we need to know.
|
||||||
|
|
||||||
|
We write:
|
||||||
|
|
||||||
|
* `//`, starting to search for nodes everywhere, not just the root of the document.
|
||||||
|
* `*`, to match _any_ node. We want to replace math inside of `div`s, `span`s, `nav`s,
|
||||||
|
all of the `h`s, and so on.
|
||||||
|
* `[not(self::code)]`, cutting out all the `code` tags.
|
||||||
|
* `/`, now selecting the nodes that are immediate descendants of the nodes we've selected.
|
||||||
|
* `text()`, giving us the text contents of all the nodes we've selected.
|
||||||
|
|
||||||
|
All in all:
|
||||||
|
|
||||||
|
```
|
||||||
|
//*[not(self::code)]/text()
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, we use this XPath from `nokigiri`:
|
||||||
|
|
||||||
|
```Ruby {linenos=table}
|
||||||
|
files = ARGV[0..-1]
|
||||||
|
inline_cache, display_cache = {}, {}
|
||||||
|
|
||||||
|
files.each do |file|
|
||||||
|
puts "Rendering file: #{file}"
|
||||||
|
document = Nokogiri::HTML.parse(File.open(file))
|
||||||
|
document.search('//*[not(self::code)]/text()').each do |t|
|
||||||
|
t.replace(perform_katex_sub(inline_cache, display_cache, t.content))
|
||||||
|
end
|
||||||
|
File.write(file, document.to_html)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
I named this script `convert.rb`; it's used from inside of the Nix expression
|
||||||
|
and its builder, which we will cover below.
|
||||||
|
|
||||||
|
### Tying it All Together
|
||||||
|
Finally, I wanted an end-to-end script to generate HTML pages and render the LaTeX in them.
|
||||||
|
I used Nix for this, but the below script will largely be compatible with a non-Nix system.
|
||||||
|
I came up with the following, commenting on Nix-specific commands:
|
||||||
|
|
||||||
|
```Bash {linenos=table}
|
||||||
|
source $stdenv/setup # Nix-specific; set up paths.
|
||||||
|
|
||||||
|
# Build site with Hugo
|
||||||
|
# The cp is Nix-specific; it copies the blog source into the current directory.
|
||||||
|
cp -r $src/* .
|
||||||
|
hugo --baseUrl="https://danilafe.com"
|
||||||
|
|
||||||
|
# Render math in HTML and XML files.
|
||||||
|
# $converter is Nix-specific; you can just use convert.rb.
|
||||||
|
find public/ -regex "public/.*\.html" | xargs ruby $converter
|
||||||
|
|
||||||
|
# Output result
|
||||||
|
# $out is Nix-specific; you can replace it with your destination folder.
|
||||||
|
mkdir $out
|
||||||
|
cp -r public/* $out/
|
||||||
|
```
|
||||||
|
|
||||||
|
This is it! Using the two scripts, `convert.rb` and `builder.sh`, I
|
||||||
|
was able to generate my blog with the math rendered on the back-end.
|
||||||
|
Please note, though, that I had to add the KaTeX CSS to my website's
|
||||||
|
`<head>`.
|
||||||
|
|
||||||
|
### Caveats
|
||||||
|
The main caveat of my approach is performance. For every piece of
|
||||||
|
mathematics that I render, I invoke the `katex` command. This incurs
|
||||||
|
the penalty of Node's startup time, every time, and makes my approach
|
||||||
|
take a few dozen seconds to run on my relatively small site. The
|
||||||
|
better approach would be to use a NodeJS script, rather than a Ruby one,
|
||||||
|
to perform the conversion. KaTeX also provides an API, so such a NodeJS
|
||||||
|
script can find the files, parse the HTML, and perform the substitutions.
|
||||||
|
I did quite like using `nokigiri` here, though, and I hope that an equivalently
|
||||||
|
pleasant solution exists in JavaScript.
|
||||||
|
|
||||||
|
Re-rendering the whole website is also pretty wasteful. I rarely change the
|
||||||
|
mathematics on more than one page at a time, but every time I do so, I have
|
||||||
|
to re-run the script, and therefore re-render every page. This makes sense
|
||||||
|
for me, since I use Nix, and my builds are pretty much always performed
|
||||||
|
from scratch. On the other hand, for others, this may not be the best solution.
|
||||||
|
|
||||||
|
### Conclusion
|
||||||
|
With the removal of MathJax from my site, it is now completely JavaScript free,
|
||||||
|
and contains virtually the same HTML that it did beforehand. This, I hope,
|
||||||
|
makes it work better on devices where computational power is more limited.
|
||||||
|
I also hope that it illustrates a general principle - it's very possible,
|
||||||
|
and plausible, to render LaTeX on the back-end for a static site.
|
||||||
BIN
content/blog/dell_is_horrible/brokenkey.jpg
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
content/blog/dell_is_horrible/brokenlcd.jpg
Normal file
|
After Width: | Height: | Size: 476 KiB |
BIN
content/blog/dell_is_horrible/dm_1.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
content/blog/dell_is_horrible/dm_2.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
content/blog/dell_is_horrible/dm_3.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
content/blog/dell_is_horrible/dm_4.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
content/blog/dell_is_horrible/dm_5.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
384
content/blog/dell_is_horrible/index.md
Normal file
@@ -0,0 +1,384 @@
|
|||||||
|
---
|
||||||
|
title: DELL Is A Horrible Company And You Should Avoid Them At All Costs
|
||||||
|
date: 2020-07-17T16:23:34-07:00
|
||||||
|
draft: true
|
||||||
|
tags: ["Electronics"]
|
||||||
|
---
|
||||||
|
|
||||||
|
I really do not want this to be a consumer electronics blog. Such things
|
||||||
|
aren't interesting to me, and nor do I have much knowledge
|
||||||
|
about them. However, sometimes, ripples from these areas make their way
|
||||||
|
into my life, and this is one such instance. Let me tell you
|
||||||
|
{{< sidenote "right" "source-note" "a story" >}}
|
||||||
|
I originally wrote about this in
|
||||||
|
<a href="https://www.dell.com/community/XPS/Ridiculously-Bad-Support-Experience/td-p/7554383">a thread on DELL's support website</a>. Some of this post is
|
||||||
|
going to be adapted from the support website, but some things have happened
|
||||||
|
since. You will probably notice the change between the terse language I used
|
||||||
|
in the original post and the fresh text that I'm writing now.
|
||||||
|
{{< /sidenote >}} of
|
||||||
|
my experience with DELL and their XPS 2-in-1 laptop, which has gone on since
|
||||||
|
around January of 2020, and is still going at the time of writing, in July
|
||||||
|
2020, half a year later.
|
||||||
|
|
||||||
|
I was, until recently, an undergraduate student in Computer Science. I will
|
||||||
|
soon be starting my Masters in Computer Science, too. I say this to make one
|
||||||
|
thing clear: I need a computer. Not only is it a necessity for my major,
|
||||||
|
but the majority of my hobbies -- including this blog -- are digital, too.
|
||||||
|
Since my university is a couple of hours from my home, I travel back and forth
|
||||||
|
a lot. I also have a cozy little spot in the
|
||||||
|
{{< sidenote "right" "offices-note" "graduate student offices" >}}
|
||||||
|
They're a bunch of cubicles in a keycard-protected room, really. Nothing fancy.
|
||||||
|
{{< /sidenote >}}at my university, but travel by bus, so I find myself spending
|
||||||
|
roughly equal portions of my work time at home and 'elsewhere'. A laptop
|
||||||
|
as my primary machine, I thought, made sense. But it had to be a decent one.
|
||||||
|
Persuaded by one of my instructors, who stressed the importance of vision and
|
||||||
|
a decent screen, I settled on a DELL XPS, which at the time came with a 4k
|
||||||
|
display.
|
||||||
|
|
||||||
|
As is commonplace, things went great at first. The screen _was_ really nice,
|
||||||
|
all of my code compiled swiftly, and even the games I occasionally played ran
|
||||||
|
at a solid 60fps. I was happy with my purchase.
|
||||||
|
|
||||||
|
There was one hiccup before things went really downhill, a sort of
|
||||||
|
foreshadowing of things to come. My trackpad didn't work at peculiar times.
|
||||||
|
|
||||||
|
### Prologue: Trackpad Hiccups
|
||||||
|
While working, booted into Linux, I noticed that my trackpad was having some
|
||||||
|
trouble. It was stuttering, and occasionally wouldn't work at all for seconds
|
||||||
|
at a time. I assumed that this was a problem with the trackpad drivers on
|
||||||
|
Linux, or perhaps the whole system was freezing up. I rebooted, and the
|
||||||
|
problem went away.
|
||||||
|
|
||||||
|
Until it came back.
|
||||||
|
|
||||||
|
A few days later, my trackpad was freezing virtually every minute.
|
||||||
|
It was strange, but fortunately, I'm used to a keyboard-based workflow, and
|
||||||
|
the malfunctions did not affect me too much. It was just a little troubling.
|
||||||
|
What soon made it more troubling, was that I noticed this exact same issue
|
||||||
|
occurring on Windows. To me, this meant one dreadful thing: it was a hardware
|
||||||
|
issue.
|
||||||
|
|
||||||
|
I poked and prodded for a little bit, and finally discovered the cause:
|
||||||
|
whenever I put my hand on the left palmrest, the trackpad would reliably stop
|
||||||
|
working. Knowing what the issue was, I called DELL. I spoke to a guy on the
|
||||||
|
other end, who had me run through diagnostics, driver updates, and BIOS
|
||||||
|
settings (I imagined this was procedure, so I didn't mind doing the extra
|
||||||
|
work to make the other guy's job easier). Finally, he scheduled a repair
|
||||||
|
appointment. A technician came into my house, took off the laptop cover,
|
||||||
|
and said something along the lines of:
|
||||||
|
|
||||||
|
> Now look. They gave me a whole new motherboard and case to replace yours,
|
||||||
|
but in my personal opinion, this is a bad idea. Things are bound to break
|
||||||
|
when you do this. See how the replacement case has an insulating piece
|
||||||
|
of fabric under the left palmrest, and yours doesn't? Why don't we rip
|
||||||
|
the fabric off the replacement case, and tape it in place on your machine,
|
||||||
|
without any reassembly?
|
||||||
|
|
||||||
|
This man was wiser than any of the other DELL technicians, I now understand.
|
||||||
|
The repair went without a hitch. He grilled me for going to college instead of
|
||||||
|
just picking up a trade, which was cheaper and offered more job security.
|
||||||
|
In the end, I felt a little weird about having a piece of fabric duct taped
|
||||||
|
inside my computer, but the trackpad had no more issues ever since. All was
|
||||||
|
well.
|
||||||
|
|
||||||
|
### Service Request 1: Broken D Key
|
||||||
|
All was well, that is, until the middle of winter term. I was typing up an
|
||||||
|
assignment for a university class. I was working as usual, when I suddenly
|
||||||
|
noticed that the "d" key stopped working - it had to be pressed rather weird
|
||||||
|
to register on the computer. I looked down, and discovered that the key had
|
||||||
|
snapped in half. The top part of the key fell off shortly thereafter.
|
||||||
|
|
||||||
|
{{< figure src="brokenkey.jpg" caption="The broken D key shortly after the above events." >}}
|
||||||
|
|
||||||
|
At that point, I was more surprised than anything. I hadn't heard of something
|
||||||
|
like this ever happening, especially under circumstances as normal as typing.
|
||||||
|
Regardless, I contacted support, and set up a repair appointment. Things only
|
||||||
|
went downhill from there.
|
||||||
|
|
||||||
|
Again, the appointment was scheduled, and only a few days later, another
|
||||||
|
technician arrived at my house. The only way to repair the key, he said,
|
||||||
|
was to replace the whole keyboard. They keyboard happens to be located
|
||||||
|
underneath all the other hardware, and so, the entire laptop had to be
|
||||||
|
disassembled and reassembled from scratch. He worked for about an hour, and
|
||||||
|
eventually, he put the machine together. The words of the previous
|
||||||
|
technician, who wanted to avoid doing exactly what had just been done, echoed
|
||||||
|
in my head:
|
||||||
|
|
||||||
|
> Things are bound to break when you do this.
|
||||||
|
|
||||||
|
I asked him to test it, just to make sure everything works. Sure enough,
|
||||||
|
not everything did work: the machine no longer had sound!
|
||||||
|
|
||||||
|
### Service Request 2: No sound
|
||||||
|
During diagnostics, the laptop did not emit the "beep" it usually does. This
|
||||||
|
was the first sign. Booting into Windows, the sound icon was crossed out in
|
||||||
|
red, and no sound was present. Booting into Linux led to similar results.
|
||||||
|
The microphone on the machine did not seem to work either. The service
|
||||||
|
technician said that he didn't have the parts to repair it, told me he'd call
|
||||||
|
it in, and left. Soon after, I got an email asking for times I'm available to
|
||||||
|
call: I said "any time except for 1-4 pacific time". DELL support proceeded
|
||||||
|
to call me at 3pm pacific time, when I had no service. Unable to reach me,
|
||||||
|
they promptly notified me that they are archiving my service request.
|
||||||
|
|
||||||
|
This all occurred near finals week at my university, so I had to put the issue
|
||||||
|
on hold. I had to maintain my grades, and I had to grade heaps of assignments
|
||||||
|
from other students. Though the lack of sound was annoying, it wasn't as
|
||||||
|
pressing as preparing for exams, so it was during spring break that I finally
|
||||||
|
called again, and scheduled the service appointment. By then,
|
||||||
|
{{< sidenote "right" "pandemic-note" "the pandemic was in full swing," >}}
|
||||||
|
Just for posterity, in 2020, there had been an outbreak of COVID-19,
|
||||||
|
a Coronavirus. Many states in the U.S., including my own, issued
|
||||||
|
the orders for lockdown and social distancing, which meant the closing
|
||||||
|
of schools, restaurants, and, apparently, the cessation of in-person
|
||||||
|
repairs.
|
||||||
|
{{< /sidenote >}}and DELL told me they'd mail me a box to put my laptop in, and
|
||||||
|
I'd have to mail it off to their service center. Sure, I thought, that's
|
||||||
|
fine. If it's at the service center, they won't ever "not have the required
|
||||||
|
parts". I told the tech support person my address, he read it back to me, and
|
||||||
|
so it was settled.
|
||||||
|
|
||||||
|
Until, that is, the box arrived at the wrong address.
|
||||||
|
|
||||||
|
I had received the machine as a gift from my family, who purchased the
|
||||||
|
computer to arrive at their address. The box arrived at that address too,
|
||||||
|
despite my explicit instructions to have it deliver to my current residence.
|
||||||
|
Since my family and I live 2 hours apart, it took 4 total hours to get the box
|
||||||
|
to me (a drive that couldn't be made right away!), and by the time I had it,
|
||||||
|
DELL was already threatening me again with closing the service request.
|
||||||
|
Eventually, I was able to mail the machine off, and about 5 business days
|
||||||
|
later (business days during which I did not have a working machine, which is
|
||||||
|
very necessary for my school and job) I received it back. I was excited to
|
||||||
|
have the machine back, but that didn't last very long. As I was using the
|
||||||
|
computer with Wolfram Mathematica (a rather heavy piece of software running
|
||||||
|
under Linux), I noticed that it was discharging even while plugged in. I
|
||||||
|
booted into Windows, and was greeted with a warning, something along the
|
||||||
|
lines of: "you are using a slow charger. Please use the official adapter".
|
||||||
|
But I was using the official adapter! I also tried to plug my mouse into the
|
||||||
|
relevant USB-C port, only to discover that it did not work. I had to make
|
||||||
|
another service requests.
|
||||||
|
|
||||||
|
### Service Request 3: Broken Charging Port
|
||||||
|
This time, I made sure to tell the person on the other end of the support
|
||||||
|
call to please send it to my address. I asked if there was anything I can do,
|
||||||
|
or anyone I can contact, and was told "no, just mail the computer in again."
|
||||||
|
I obliged. The box arrived at the right address this time, so I was able to
|
||||||
|
ship it off.
|
||||||
|
|
||||||
|
In the "describe your issue" field on the provided form, I begged the
|
||||||
|
technicians to send me a working machine. "Please", I wrote "Last time I got
|
||||||
|
a machine back from support, it was still broken. I really need it for school
|
||||||
|
and work!". 5 business days later, I received the machine back. I plugged it
|
||||||
|
in to make sure it worked, only to find out . . . that the very same charging
|
||||||
|
port that I requested be repaired, is still broken! It would've been funny,
|
||||||
|
if it wasn't infuriating. How is it possible for me to receive a machine from
|
||||||
|
repairs, without the thing I asked to repair being as much as improved?!
|
||||||
|
|
||||||
|
Worse, a day after I received the machine back (I was able to keep using it
|
||||||
|
thanks to it having two USB-C ports capable of charging), the LCD suddenly
|
||||||
|
flashed, and started flickering. Thinking it was a software glitch, I
|
||||||
|
restarted the machine, only to discover the same flickering during the boot
|
||||||
|
animation and menu. Not only was the charging port not repaired, but now my
|
||||||
|
LCD was broken! (in the below picture, the screen is meant to be blue, but
|
||||||
|
the bottom part of the display is purple and flickering).
|
||||||
|
|
||||||
|
{{< figure src="brokenlcd.jpg" caption="The broken LCD." >}}
|
||||||
|
|
||||||
|
### Service Request 4: Broken LCD
|
||||||
|
I called in to support again, and they once again told me to ship the machine
|
||||||
|
off. What's worse, they accused me of breaking the port myself, and told me
|
||||||
|
this was no longer covered under basic warranty. I had to explain all over
|
||||||
|
again that the port worked fine before the fateful day the D-key snapped. They
|
||||||
|
told me they'd "look into it". Eventually, I received a box in the mail. I
|
||||||
|
wasn't told I would be receiving a box, but that wasn't a big deal. I mailed
|
||||||
|
off the machine.
|
||||||
|
|
||||||
|
The UPS shipping was always the most streamlined part of the process. A day
|
||||||
|
later, I was told my machine was received intact. Another day, and I was
|
||||||
|
informed that the technicians are starting to work on it. And then,
|
||||||
|
a few hours later:
|
||||||
|
|
||||||
|
> __Current Status:__
|
||||||
|
> The part(s) needed to repair your system are not currently in stock.
|
||||||
|
> __What's Next:__
|
||||||
|
> In most cases the parts are available is less than five days.
|
||||||
|
|
||||||
|
A few days is no big deal, and it made sense that DELL wouldn't just
|
||||||
|
have screens lying around. So I waited. And waited. And waited. Two weeks
|
||||||
|
later, I got a little tired of waiting, and called the repair center.
|
||||||
|
An automated message told me:
|
||||||
|
|
||||||
|
> We're currently experiencing heavy call volumes. Please try again later. Goodbye.
|
||||||
|
|
||||||
|
And the call was dropped. This happened every time I tried to call, no matter
|
||||||
|
the hour. The original status update -- the one that notified me about the
|
||||||
|
part shortage -- came on May 8th, but the machine finally arrived to me
|
||||||
|
(without prior warning) on June 2nd, almost a month later.
|
||||||
|
|
||||||
|
The charging port worked. Sound
|
||||||
|
worked. The screen wasn't flickering. I was happy for the brief moments that
|
||||||
|
my computer was loading. As soon as I started vim, though, I noticed something
|
||||||
|
was off: the fonts looked more pixelated. The DPI settings I'd painstakingly
|
||||||
|
tweaked were wrong. Now that I thought about it, even the GRUB menu was
|
||||||
|
larger. My suspicion growing, I booted into Windows, and looked at the display
|
||||||
|
settings. Noticeably fewer resolutions were listed in the drop-down menu;
|
||||||
|
worse, the highest resolution was 1080p. After almost a month of waiting,
|
||||||
|
DELL replaced my 4k laptop display with a 1080p one.
|
||||||
|
|
||||||
|
### System Replacement: Worse LCD Screen
|
||||||
|
|
||||||
|
I admit, I was angry. At the same time, the absurdity of it all was also
|
||||||
|
unbearable. Was this constant loop of hardware damage, the endless number of
|
||||||
|
support calls filled with hoarse jazz music, part of some kind of Kafkaesque
|
||||||
|
dream? I didn't know. I was at the end of my wits as to what to do. As a last
|
||||||
|
resort, I made a tweet from my almost-abandoned account. DELL Support's Twitter
|
||||||
|
account quickly responded, eager as always to destroy any semblance of
|
||||||
|
transparency by switching to private messages:
|
||||||
|
|
||||||
|
{{< tweet 1268064691416334344 >}}
|
||||||
|
|
||||||
|
I let them know my thoughts on the matter. I wanted a new machine.
|
||||||
|
|
||||||
|
{{< figure src="dm_1.png" caption="The first real exchange between me and DELL support." >}}
|
||||||
|
|
||||||
|
Of course we can proceed further. I wanted to know what kind of machine I was getting,
|
||||||
|
though. As long as it was the same model that I originally bought,
|
||||||
|
{{< sidenote "right" "replacement-note" "it would be better than what I have." >}}
|
||||||
|
At least in principle, it would be. Perhaps the wear and tear on the replacement
|
||||||
|
parts would be greater, but at least I would have, presumably, a machine
|
||||||
|
in good condition that had the 4k screen that made me buy it in the first place.
|
||||||
|
{{< /sidenote >}}
|
||||||
|
Despite this, I knew that the machine I was getting was likely refurbished.
|
||||||
|
This _had_ to mean that some of the parts would come from other, used, machines.
|
||||||
|
This irked me, because, well, I payed for a new machine.
|
||||||
|
|
||||||
|
{{< figure src="dm_2.png" caption="Ah, the classic use of canned responses." >}}
|
||||||
|
|
||||||
|
Their use of the canned response, and their unwillingness to answer this simple
|
||||||
|
question, was transparent. Indeed, the machine would be made of used
|
||||||
|
parts. I still wanted to proceed. DELL requested that I sent an image of
|
||||||
|
my machine which included its service tag, together with a piece of
|
||||||
|
paper which included my name and email address. I obliged, and quickly got a response:
|
||||||
|
|
||||||
|
{{< figure src="dm_3.png" caption="If it was me who was silent for 4 days, my request would've long been cancelled. " >}}
|
||||||
|
|
||||||
|
Thanks, Kalpana. Try to remember that name; you will never hear it again, not in this post.
|
||||||
|
About a week later, I get the following beauty:
|
||||||
|
|
||||||
|
{{< figure src="dm_4.png" caption="Excuse me? What's going on?" >}}
|
||||||
|
|
||||||
|
My initial request was cancelled? Why wasn't I told? What was the reason?
|
||||||
|
What the heck was going on at DELL Support? Should I be worried?
|
||||||
|
My question of "Why" was answered with the apt response of "Yes",
|
||||||
|
and a message meant to pacify me. While this was going on, I ordered
|
||||||
|
a
|
||||||
|
{{< sidenote "right" "pinebook-note" "Pinebook Pro." >}}
|
||||||
|
The Pinebook – a $200 machine – has, thus far, worked more reliably than any DELL product
|
||||||
|
I've had the misfortune of owning.
|
||||||
|
{{< /sidenote >}} It was not a replacement for the DELL machine, but rather
|
||||||
|
the first step towards migrating my setup to a stationary computer,
|
||||||
|
and a small, lightweight SSH device. At this point,
|
||||||
|
there was no more faith in DELL left in my mind.
|
||||||
|
|
||||||
|
Soon, DELL required my attention, only to tell me that they put in
|
||||||
|
a request to see that status of my request. How bureaucratic. Two
|
||||||
|
more names -- Kareem and JKC -- flickered through the chats,
|
||||||
|
also never to be seen again.
|
||||||
|
|
||||||
|
{{< figure src="dm_5.png" caption="Not much of a conversation, really." >}}
|
||||||
|
|
||||||
|
Finally, on July 9th (a month and six days after my first real message to DELL
|
||||||
|
support), I was notified by my roommates that FedEx tried to deliver a package
|
||||||
|
to our house, but gave up when no one came to sign for it. On one hand, this
|
||||||
|
is great: FedEx didn't just leave my laptop on the porch. On the other hand,
|
||||||
|
though, this was the first time I heard about receiving the machine. I got
|
||||||
|
to the house the next day, unpacked the new computer, and tested all the things
|
||||||
|
that had, at one point, failed. Everything seemed to work. I transfered all my
|
||||||
|
files, wiped the old computer clean, and mailed it off. I also spent some
|
||||||
|
time dealing with the fallout of DELL PremierColor starting on its own,
|
||||||
|
and permanently altering the color profile of my display. I don't have the
|
||||||
|
special, physical calibration device, and therefore still suspect that my
|
||||||
|
screen is somewhat green.
|
||||||
|
|
||||||
|
Today, I discovered that the microphone of the replacement machine didn't work.
|
||||||
|
|
||||||
|
### Am I The Problem?
|
||||||
|
When the mysterious FedEx package arrived at my door on July 9th, I did some
|
||||||
|
digging to verify my suspicion that it was from DELL. I discovered their
|
||||||
|
HQ in Lebanon, TN. This gave me an opportunity to
|
||||||
|
{{< sidenote "right" "reviews-note" "see" >}}
|
||||||
|
See, of course, modulo whatever bias arises when only those who feel strongly leave reviews.
|
||||||
|
{{< /sidenote >}} whether or not I was alone in this situation. I was genuinely
|
||||||
|
worried that I was suffering from the technical variant of
|
||||||
|
[Munchausen Syndrome](https://www.webmd.com/mental-health/munchausen-syndrome#1),
|
||||||
|
and that I was compulsively breaking my electronics. These worries were
|
||||||
|
dispelled by the reviews on Google:
|
||||||
|
|
||||||
|
{{< figure src="reviews_1.png" caption="Most of the reviews are pretty terse, but the ratings convey the general idea." >}}
|
||||||
|
|
||||||
|
There were even some that were shockingly similar in terms of the apparent
|
||||||
|
incompetence of the DELL technicians:
|
||||||
|
|
||||||
|
{{< figure src="reviews_2.png" caption="Now, now, Maggie, I wouldn't go as far as recommending Apple." >}}
|
||||||
|
|
||||||
|
So, this is not uncommon. This is how DELL deals with customers now. It's
|
||||||
|
awfully tiring, really; I've been in and out of repairs continuously for
|
||||||
|
almost half a year, now. That's 2.5% of my life at the time of writing,
|
||||||
|
all non-stop since the D-key. And these people probably have spent considerable
|
||||||
|
amounts of time, too.
|
||||||
|
|
||||||
|
### It's About the Principle
|
||||||
|
The microphone on my machine is rather inconsequential to me. I can, and regularly do,
|
||||||
|
teleconference from my phone (a habit that I developed thanks to DELL, since
|
||||||
|
my computer was so often unavailable). I don't need to dictate anything. Most
|
||||||
|
of my communication is via chat.
|
||||||
|
|
||||||
|
Really, compared to the other issues (keyboard, sound, charging, USB ports, the broken or low-resolution screen)
|
||||||
|
the microphone is a benign problem. As I have now learned, things could be worse.
|
||||||
|
|
||||||
|
But why should the thought, _"It could be worse"_, even cross my mind
|
||||||
|
when dealing with such a matter? Virtually every issue that has
|
||||||
|
occurred with my computer thus far could -- should! -- have been diagnosed
|
||||||
|
at the repair center. The 'slow charger' warning shows up in BIOS,
|
||||||
|
so just turning the computer on while plugged in should make it obvious something
|
||||||
|
is wrong; doubly so when the very reason that the laptop was in repairs
|
||||||
|
in the first place was because of the faulty charger. I refuse to believe
|
||||||
|
that screens with different resolutions have the same part identifier,
|
||||||
|
either. How have the standards of service from DELL fallen so low?
|
||||||
|
How come this absurd scenario plays out not just for me, but
|
||||||
|
for others as well? It would be comforting, in a way, to think
|
||||||
|
that I was just the 'exceptional case'. But apparently, I'm not.
|
||||||
|
This is standard practice.
|
||||||
|
|
||||||
|
### Tl;DR
|
||||||
|
Here are he problems I've had with DELL:
|
||||||
|
|
||||||
|
* The machine shipped, apparently, with a missing piece of insulation.
|
||||||
|
* The "D" key on the keyboard snapped after only a few months of use.
|
||||||
|
* While repairing the "D" key, the DELL technician broke the computer's sound and microphone.
|
||||||
|
* While repairing the sound and microphone, the DELL technicians broke a charging port.
|
||||||
|
* The DELL technicians failed to repair the charging port, mailing me back a machine
|
||||||
|
exhibiting the same issues, in addition to a broken LCD screen.
|
||||||
|
* The repair of the LCD screen took almost a month, and concluded
|
||||||
|
with me receiving a worse quality screen than I originally had.
|
||||||
|
* The system replacement that followed the botched LCD repair took
|
||||||
|
over a month to go through.
|
||||||
|
* The replaced system was made partially of used parts, which
|
||||||
|
DELL refused to admit.
|
||||||
|
* The microphone on the replacement system was broken.
|
||||||
|
|
||||||
|
### Closing Thoughts
|
||||||
|
I will not be sending my system in again. It doesn't make sense to do so -
|
||||||
|
after mailing my system in for repairs three times, I've measured empirically that
|
||||||
|
the chance of failure is 100%. Every service request is a lottery, dutifully
|
||||||
|
giving out a random prize of another broken part. I no longer wish to play;
|
||||||
|
as any person who gambles should, I will quit while I'm ahead, and cut my losses.
|
||||||
|
However, I hope for this story, which may be unusual in its level of detail,
|
||||||
|
but not its content, to be seen by seen by someone. I hope to prevent
|
||||||
|
someone out there from feeling the frustration, and anger, and peculiar amusement
|
||||||
|
that I felt during this process. I hope for someone else to purchase a computer
|
||||||
|
with money, and not with their sanity. A guy can hope.
|
||||||
|
|
||||||
|
If you're reading this, please take this as a warning. __DELL is a horrible
|
||||||
|
company. They have the lowest standards of customer support of any
|
||||||
|
U.S. company that I've encountered. Their technicians are largely incompetent.
|
||||||
|
Their quality assurance is non-existent. Stay away from them.__
|
||||||
BIN
content/blog/dell_is_horrible/reviews_1.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
content/blog/dell_is_horrible/reviews_2.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
static/Resume-Danila-Fedorin.pdf
Normal file
@@ -242,3 +242,7 @@ figure {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.twitter-tweet {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="https://github.com/DanilaFe">GitHub</a>
|
|
||||||
<a href="/about">About</a>
|
<a href="/about">About</a>
|
||||||
|
<a href="https://github.com/DanilaFe">GitHub</a>
|
||||||
|
<a href="/Resume-Danila-Fedorin.pdf">Resume</a>
|
||||||
<a href="/tags">Tags</a>
|
<a href="/tags">Tags</a>
|
||||||
<a href="/blog">All Posts</a>
|
<a href="/blog">All Posts</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||