diff --git a/package.json b/package.json index 39edb42..ef26d86 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "rimraf": "^3.0.2", "style-loader": "^4.0.0", "webpack": "^5.93.0", - "webpack-cli": "^5.1.4" + "webpack-cli": "^5.1.4", + "wtfnode": "^0.9.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a411dfd..33afd1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,9 @@ devDependencies: webpack-cli: specifier: ^5.1.4 version: 5.1.4(webpack@5.93.0) + wtfnode: + specifier: ^0.9.3 + version: 0.9.3 packages: @@ -4674,6 +4677,11 @@ packages: optional: true dev: false + /wtfnode@0.9.3: + resolution: {integrity: sha512-MXjgxJovNVYUkD85JBZTKT5S5ng/e56sNuRZlid7HcGTNrIODa5UPtqE3i0daj7fJ2SGj5Um2VmiphQVyVKK5A==} + hasBin: true + dev: true + /xml-js@1.6.11: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true diff --git a/src/app/http/controllers/Dash2.controller.ts b/src/app/http/controllers/Dash2.controller.ts index e239588..3ac523f 100644 --- a/src/app/http/controllers/Dash2.controller.ts +++ b/src/app/http/controllers/Dash2.controller.ts @@ -73,6 +73,36 @@ export class Dash2 extends Controller { const config = this.getResourceConfigOrFail(resourceName) 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: [ { target: { diff --git a/src/app/http/middlewares/PageView.middleware.ts b/src/app/http/middlewares/PageView.middleware.ts index 229fc22..ec25ce7 100644 --- a/src/app/http/middlewares/PageView.middleware.ts +++ b/src/app/http/middlewares/PageView.middleware.ts @@ -25,8 +25,10 @@ export class PageView extends Middleware { const view = make(PageViewModule.PageView) const user = this.security.getUser() + const realIp = this.request.getHeader('X-GM-Real-IP') + // 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.endpoint = this.request.path view.userId = user ? (user as any).userId : undefined @@ -36,4 +38,17 @@ export class PageView extends Middleware { 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 + } } diff --git a/src/app/resources/assets/app/cobalt/types.ts b/src/app/resources/assets/app/cobalt/types.ts index 2f66e93..a2dcab4 100644 --- a/src/app/resources/assets/app/cobalt/types.ts +++ b/src/app/resources/assets/app/cobalt/types.ts @@ -50,8 +50,19 @@ export type CobaltAction = CobaltActionBase & ( route: string, method: string, } + | { + type: 'navigate', + route: string, + } ) +export type ActionButton = CobaltAction & { + title: string, + description?: string, + color?: string, + icon?: string, +} + export type LoadAction = { target: SourceRef, action: CobaltAction, @@ -110,6 +121,10 @@ export type ListConfig = { sourceName: string, rowKey: string, fields: FieldDefinition[], + actions?: { + top?: ActionButton[], + inline?: ActionButton[], + }, } /*export interface ResourceConfiguration { diff --git a/src/app/resources/food-blog-countries/ET.md b/src/app/resources/food-blog-countries/ET.md new file mode 100644 index 0000000..c071de9 --- /dev/null +++ b/src/app/resources/food-blog-countries/ET.md @@ -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. diff --git a/src/app/resources/food-blog/ET-Doro-Wat.md b/src/app/resources/food-blog/ET-Doro-Wat.md new file mode 100644 index 0000000..5db00e8 --- /dev/null +++ b/src/app/resources/food-blog/ET-Doro-Wat.md @@ -0,0 +1,130 @@ +--- +title: Doro Wat +date: 2024-07-27 19:00:00 +slug: ET-Doro-Wat +country: ET +tags: + - chicken + - stew +--- + + + +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. + +
+
+
+
+ +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. + + +
The ingredients I used. Not pictured: salt, injera bread (more on that later).
+ +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] + + + +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. + +
+
+
+
+
+ +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] + +
+
+
+
+
+
I'm not crying; you're crying...
+ +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_. + +
+
+
+
+ +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. + +
+
+
+
+ +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). + +
+
+
+
+ +_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. + +
+
+
+
+ +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_. + + + +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. + +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +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 deep historical trade roots with Ethiopia. Other spices in berbere, however, originate from the region itself, or parts of eastern Europe and the Mediterranean. + +[^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 doro 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. diff --git a/src/app/resources/views/food/page.pug b/src/app/resources/views/food/page.pug index a2d72ff..3060649 100644 --- a/src/app/resources/views/food/page.pug +++ b/src/app/resources/views/food/page.pug @@ -68,6 +68,7 @@ block append script noDataText: 'Not yet explored', flagType: 'emoji', colorNoData: 'var(--c-background)', + colorMin: '#FF7474', data: { data: { posts: { @@ -92,6 +93,7 @@ block append script noDataText: '', flagType: 'emoji', colorNoData: 'var(--c-background)', + colorMin: '#FF7474', initialZoom: #{mapPosition.zoom}, initialPan: { x: #{mapPosition.x}, diff --git a/src/app/services/blog/countries.ts b/src/app/services/blog/countries.ts index a6500cc..b7a10bc 100644 --- a/src/app/services/blog/countries.ts +++ b/src/app/services/blog/countries.ts @@ -251,7 +251,7 @@ export const countryNames = { export const countries = Object.keys(countryNames) as Country[] 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 => ( typeof what === 'string' @@ -260,4 +260,5 @@ export const isCountry = (what: unknown): what is Country => ( export const mapPositions: Partial> = { FR: { country: 'FR', zoom: 4, x: 500, y: 100 }, + ET: { country: 'ET', zoom: 4, x: 630, y: 275 }, } diff --git a/src/index.ts b/src/index.ts index bc96619..6b63f7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,4 +22,8 @@ globalRegistry.run(async () => { Error.stackTraceLimit = 50 await appInstance.run() + + setTimeout(() => { + process.exit(0) + }, 3000) })