Add Doro Wat WPA post + misc other updates
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Garrett Mills 2024-08-02 00:38:48 -04:00
parent 51aa1f8b41
commit 02d28491dc
10 changed files with 210 additions and 3 deletions

View File

@ -72,6 +72,7 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"style-loader": "^4.0.0", "style-loader": "^4.0.0",
"webpack": "^5.93.0", "webpack": "^5.93.0",
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4",
"wtfnode": "^0.9.3"
} }
} }

View File

@ -100,6 +100,9 @@ devDependencies:
webpack-cli: webpack-cli:
specifier: ^5.1.4 specifier: ^5.1.4
version: 5.1.4(webpack@5.93.0) version: 5.1.4(webpack@5.93.0)
wtfnode:
specifier: ^0.9.3
version: 0.9.3
packages: packages:
@ -4674,6 +4677,11 @@ packages:
optional: true optional: true
dev: false dev: false
/wtfnode@0.9.3:
resolution: {integrity: sha512-MXjgxJovNVYUkD85JBZTKT5S5ng/e56sNuRZlid7HcGTNrIODa5UPtqE3i0daj7fJ2SGj5Um2VmiphQVyVKK5A==}
hasBin: true
dev: true
/xml-js@1.6.11: /xml-js@1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true hasBin: true

View File

@ -73,6 +73,36 @@ export class Dash2 extends Controller {
const config = this.getResourceConfigOrFail(resourceName) const config = this.getResourceConfigOrFail(resourceName)
return one({ return one({
actions: {
top: [
{
type: 'navigate',
route: `/dash2/cobalt/resource/${resourceName}/form`,
title: 'Create New',
color: '#94AE89',
icon: 'fa-solid fa-plus',
},
],
inline: [
{
type: 'navigate',
route: `/dash2/cobalt/resource/${resourceName}/form`,
title: 'Open',
color: '#50808E',
icon: 'fa-solid fa-eye',
// fixme: gather
},
{
type: 'route',
route: `/dash2/cobalt/resource/${resourceName}/form`,
method: 'delete',
title: 'Delete',
color: '#B02E0C',
icon: 'fa-solid fa-trash-can',
// fixme: gather
},
],
},
loadActions: [ loadActions: [
{ {
target: { target: {

View File

@ -25,8 +25,10 @@ export class PageView extends Middleware {
const view = make<PageViewModule.PageView>(PageViewModule.PageView) const view = make<PageViewModule.PageView>(PageViewModule.PageView)
const user = this.security.getUser() const user = this.security.getUser()
const realIp = this.request.getHeader('X-GM-Real-IP')
// hostname // hostname
view.ip = `${this.request.address.address}:${this.request.address.port}` view.ip = `${this.getIP()}:${this.request.address.port}`
view.method = this.request.method view.method = this.request.method
view.endpoint = this.request.path view.endpoint = this.request.path
view.userId = user ? (user as any).userId : undefined view.userId = user ? (user as any).userId : undefined
@ -36,4 +38,17 @@ export class PageView extends Middleware {
await view.save() await view.save()
} }
private getIP(): string {
const realIp = this.request.getHeader('X-GM-Real-IP')
if ( !realIp ) {
return this.request.address.address
}
if ( Array.isArray(realIp) ) {
return realIp[0] || this.request.address.address
}
return realIp
}
} }

View File

@ -50,8 +50,19 @@ export type CobaltAction = CobaltActionBase & (
route: string, route: string,
method: string, method: string,
} }
| {
type: 'navigate',
route: string,
}
) )
export type ActionButton = CobaltAction & {
title: string,
description?: string,
color?: string,
icon?: string,
}
export type LoadAction = { export type LoadAction = {
target: SourceRef, target: SourceRef,
action: CobaltAction, action: CobaltAction,
@ -110,6 +121,10 @@ export type ListConfig = {
sourceName: string, sourceName: string,
rowKey: string, rowKey: string,
fields: FieldDefinition[], fields: FieldDefinition[],
actions?: {
top?: ActionButton[],
inline?: ActionButton[],
},
} }
/*export interface ResourceConfiguration { /*export interface ResourceConfiguration {

View File

@ -0,0 +1 @@
Ethiopian cuisine has deep, sometimes ancient, traditions. Characterized by vibrant flavors and its communal dining style, Ethiopian cuisine combines region-specific plants and spices with those incorporated from historical trade partners, like India. Our initial exploration of Ethiopia focuses on widely-loved traditional dishes.

View File

@ -0,0 +1,130 @@
---
title: Doro Wat
date: 2024-07-27 19:00:00
slug: ET-Doro-Wat
country: ET
tags:
- chicken
- stew
---
<img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-1.jpg">
For a [change of pace](/food/c/FR), we begin our exploration of Ethiopian cuisine with what's widely considered the national dish: _doro wat_. A _wat_ (or _wet_) is a type of Ethiopian stew whose technique stands out by calling for chopped onions slow-cooked initially without any fat. _Doro wat_ is a spicy chicken variant, typically reserved for special occasions.
I had quite a difficult time tracking down any information on the origins of _doro_, so I won't suppose to know much about its history other than its consideration as a deeply beloved traditional dish.
<div style="display: flex; flex-direction: row">
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-2.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-3.jpg"></div>
</div>
This recipe comes from the book "Ethiopia: Recipes and Traditions From the Horn of Africa" by Yohanis Gebreyesus, a chef based in _Addis Ababa_. This book is perhaps the most famous and comprehensive collection of traditional Ethiopian recipes.[^1]
I was excited for this recipe because, unlike western cuisine, or even a some Asian cuisine, I have absolutely zero familiarity with Ethiopian cooking techniques and ingredients. Aside from its time commitment, the _doro_ was fairly straightforward, with some interesting unfamiliar techniques.
<img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-4.jpg">
<center>The ingredients I used. Not pictured: salt, <i>injera</i> bread (more on that later).</center>
Aside from the technique, the most interesting thing about this recipe, to me, was the spices. The main spice and flavor comes from _berbere_, a spice mixture of chili powder and smaller amounts of several other spices. This is what gives the sauce its characteristic hue. The recipe also includes ground nigella (seeds of the black caraway flower), and ground ajowan.[^2]
I found these spices at my local international grocer, [Saraga](https://saragaindy.com/), an absolute treasure. The recipe also calls for _niter kebbeh_, a spiced clarified butter, however I substituted some homemade ghee I had, adjusted with some spices used in the _niter kebbeh_.[^3]
<img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-5.jpg">
The recipe begins with preparing the chicken, which is done by removing the skin[^4] and placing it in water with lemon to soak. Then, it's time to prep the spices.
<div style="display: flex; flex-direction: row">
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-6.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-7.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-8.jpg"></div>
</div>
The berbere is blended with a small amount of water to allow it to "mellow slightly." I also ground my ajowan and created a _mekelesha_ blend.[^5] Next is the onion, which Yohanis says "gives the final sauce its body and texture." The onions are chopped finely, then cooked over a low fire, slowly, to drive out moisture.[^6]
<div style="display: flex; flex-direction: row">
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-9.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-10.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-11.jpg"></div>
</div>
<center>I'm not crying; you're crying...</center>
Once the onions have softened and dried out some, more familiarly we add ginger, garlic, and some clarified butter, followed shortly by the berbere. This smells _heavenly_.
<div style="display: flex; flex-direction: row">
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-12.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-13.jpg"></div>
</div>
The berbere is simmered with a bit of water to "let the aromas smooth out." The chicken is then browned in the pan shortly, then simmered for about 15 minutes until nearly tender.
<div style="display: flex; flex-direction: row">
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-14.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-15.jpg"></div>
</div>
After the chicken is removed, the sauce is simmered over low heat for an hour. Then, the _mekelesha_ spice is added and it is simmered some more.[^7] Here, the iconic layer of rich fat begins to appear on top of the sauce.
Once the sauce has simmered, the chicken is added back along with some hard-boiled eggs (with slits cut in them to allow the sauce to soak in).
<div style="display: flex; flex-direction: row">
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-16.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-17.jpg"></div>
</div>
_Doro wat_, and indeed many, many other Ethiopian dishes are served with _injera_. _Injera_ is a fermented flatbread originating in Ethiopia and neighboring Eritrea. It is traditionally made with teff flour, a grain native to the Horn of Africa, and has a very pronounced sour flavor.[^8] Foods are often served on top of _injera_ with more pieces of _injera_ being used as utensil for actually eating.
Making _injera_ the traditional way is a week-long process, and cooking it requires a large flat-top cooking surface. I was already in a bit over my head, and I really wanted to get an "authentic" taste. Rather than mess it up, I decided to pay a visit to my local Ethiopian restaurant and see if they'd sell me some.
<div style="display: flex; flex-direction: row">
<div style="width: 50%"><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-19.jpg"></div>
<div style="width: 50%"><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-18.jpg"></div>
</div>
I live near the [Axum Ethiopian Restaurant](https://maps.app.goo.gl/YiHhGryyzoCcHe4PA) in downtown Indy, a fantastic hole-in-the-wall with traditional Ethiopian food. Despite my odd request, the owner very kindly sold me _just_ the _injera_ and even provided some tips for cooking the _doro_.
<img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-20.jpg">
This was delicious. While it was definitely _spicy_, surprisingly it wasn't overwhelmingly so. The chicken was tender, and the _injera_, quite sour on its own, played very nicely with the rich, fatty sauce. (I will say, with no shame, that I did break out a fork and knife.)
All things considered, my first crack at Ethiopian cooking was reasonably successful. Though, I do think that my sauce was runnier than it ought to have been. This is due to a couple things: first, I should have cut the onions finer and slow-cooked them longer[^9]; second, when the berbere was cooking before the chicken, I added too much water for fear of it catching and burning.
This recipe is definitely one I'd make again -- not just because I got 200g of ground nigella for a recipe that calls for "a pinch" -- but also because, now that I've tasted the potential, I think I can get a much thicker richer sauce with a few tweaks.
<div style="display: flex; flex-direction: row">
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-21.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-22.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-23.jpg"></div>
</div>
<div style="display: flex; flex-direction: row">
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-24.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-25.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-26.jpg"></div>
</div>
<div style="display: flex; flex-direction: row">
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-27.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-28.jpg"></div>
<div><img src="https://static.garrettmills.dev/assets/blog-images/food/et-doro/optimized/doro-29.jpg"></div>
</div>
All images in this post (including the bonus picture of a cool storm cloud) were taken by me, except for the picture of the Axum Ethiopian Restaurant, which came from their [Facebook page](https://www.facebook.com/Aaxum21/). I borrowed "Ethiopia: Recipes and Traditions From the Horn of Africa" from the fantastic Indianapolis Public Library.
[^1]: Page 170, ya filthy animal.
[^2]: The influence of the spice trade on Ethiopia is really interesting to me. Ajowan, for example, also known as _ajwain_ is an herb grown primarily in India, a country with <a href="https://en.wikipedia.org/wiki/Ethiopia%E2%80%93India_relations" target="_blank">deep historical trade roots</a> with Ethiopia. Other spices in berbere, however, originate from the region itself, or parts of eastern Europe and <a href="https://en.wikipedia.org/wiki/Ruta_chalepensis" target="_blank">the Mediterranean</a>.
[^3]: Waste not.
[^4]: Easily one of the most disturbing kitchen tasks I've done in a long time.
[^5]: Ground cinnamon, clove, pepper, and cardamom. Page 44.
[^6]: I chatted with the extremely helpful owner of a local Ethiopian restaurant (more on that later) who told me the secret to <i>doro</i> is to use a very low heat and take your time. (As an aside, cooking the onions without fat has the side effect of tear-gassing your entire apartment.)
[^7]: Yohanis notes that you should add water as needed to "keep the consistency moist," though I found that with the pot covered this was unnecessary.
[^8]: To my American palette, it tastes like an extremely sour sourdough bread, but with a soft, spongy, pancake-like consistency.
[^9]: Yohanis recommends a food processor, surprisingly.

View File

@ -68,6 +68,7 @@ block append script
noDataText: 'Not yet explored', noDataText: 'Not yet explored',
flagType: 'emoji', flagType: 'emoji',
colorNoData: 'var(--c-background)', colorNoData: 'var(--c-background)',
colorMin: '#FF7474',
data: { data: {
data: { data: {
posts: { posts: {
@ -92,6 +93,7 @@ block append script
noDataText: '', noDataText: '',
flagType: 'emoji', flagType: 'emoji',
colorNoData: 'var(--c-background)', colorNoData: 'var(--c-background)',
colorMin: '#FF7474',
initialZoom: #{mapPosition.zoom}, initialZoom: #{mapPosition.zoom},
initialPan: { initialPan: {
x: #{mapPosition.x}, x: #{mapPosition.x},

View File

@ -251,7 +251,7 @@ export const countryNames = {
export const countries = Object.keys(countryNames) as Country[] export const countries = Object.keys(countryNames) as Country[]
export type Country = keyof (typeof countryNames) export type Country = keyof (typeof countryNames)
export type MapPosition = { country: 'FR', zoom: number, x: number, y: number } export type MapPosition = { country: keyof (typeof countryNames), zoom: number, x: number, y: number }
export const isCountry = (what: unknown): what is Country => ( export const isCountry = (what: unknown): what is Country => (
typeof what === 'string' typeof what === 'string'
@ -260,4 +260,5 @@ export const isCountry = (what: unknown): what is Country => (
export const mapPositions: Partial<Record<Country, MapPosition>> = { export const mapPositions: Partial<Record<Country, MapPosition>> = {
FR: { country: 'FR', zoom: 4, x: 500, y: 100 }, FR: { country: 'FR', zoom: 4, x: 500, y: 100 },
ET: { country: 'ET', zoom: 4, x: 630, y: 275 },
} }

View File

@ -22,4 +22,8 @@ globalRegistry.run(async () => {
Error.stackTraceLimit = 50 Error.stackTraceLimit = 50
await appInstance.run() await appInstance.run()
setTimeout(() => {
process.exit(0)
}, 3000)
}) })